Skip to content
Closed
170 changes: 152 additions & 18 deletions site/src/examples/omezarr/omezarr-demo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box2D, type Interval, PLANE_XY, type box2D, type vec2 } from '@alleninstitute/vis-geometry';
import { Box2D, type Interval, PLANE_XY, Vec2, type box2D, type vec2 } from '@alleninstitute/vis-geometry';
import { type OmeZarrMetadata, loadMetadata, sizeInUnits } from '@alleninstitute/vis-omezarr';
import type { RenderSettings, RenderSettingsChannels } from '@alleninstitute/vis-omezarr';
import { logger, type WebResource } from '@alleninstitute/vis-core';
Expand All @@ -8,6 +8,7 @@ import { pan, zoom } from '../common/camera';
import { RenderServerProvider } from '../common/react/render-server-provider';
import { OmezarrViewer } from './omezarr-viewer';
import { SliceView } from './sliceview';

type DemoOption = { value: string; label: string; res: WebResource };

const demoOptions: DemoOption[] = [
Expand Down Expand Up @@ -46,23 +47,30 @@ const demoOptions: DemoOption[] = [
];

const screenSize: vec2 = [800, 800];
const zoomStep = 0.1;

const defaultInterval: Interval = { min: 0, max: 80 };

function makeZarrSettings(screenSize: vec2, view: box2D, orthoVal: number, omezarr: OmeZarrMetadata): RenderSettings {
function makeZarrSettings(
screenSize: vec2,
view: box2D,
orthoVal: number,
omezarr: OmeZarrMetadata,
colorOverrides: (number | undefined)[],
): RenderSettings {
const omezarrChannels = omezarr.colorChannels.reduce((acc, val, index) => {
acc[val.label ?? `${index}`] = {
rgb: val.rgb,
rgb: [colorOverrides[0] ?? val.rgb[0], colorOverrides[1] ?? val.rgb[1], colorOverrides[2] ?? val.rgb[2]],
gamut: val.range,
index,
};
return acc;
}, {} as RenderSettingsChannels);

const fallbackChannels: RenderSettingsChannels = {
R: { rgb: [1.0, 0, 0], gamut: defaultInterval, index: 0 },
G: { rgb: [0, 1.0, 0], gamut: defaultInterval, index: 1 },
B: { rgb: [0, 0, 1.0], gamut: defaultInterval, index: 2 },
R: { rgb: [colorOverrides[0] ?? 1.0, 0, 0], gamut: defaultInterval, index: 0 },
G: { rgb: [0, colorOverrides[1] ?? 1.0, 0], gamut: defaultInterval, index: 1 },
B: { rgb: [0, 0, colorOverrides[2] ?? 1.0], gamut: defaultInterval, index: 2 },
};

return {
Expand All @@ -83,11 +91,21 @@ export function OmezarrDemo() {
const [planeIndex, setPlaneIndex] = useState(0);
const [dragging, setDragging] = useState(false);

const [firstChannel, setFirstChannel] = useState<number | undefined>(undefined);
const [secondChannel, setSecondChannel] = useState<number | undefined>(undefined);
const [thirdChannel, setThirdChannel] = useState<number | undefined>(undefined);

const settings: RenderSettings | undefined = useMemo(
() => (omezarr ? makeZarrSettings(screenSize, view, planeIndex, omezarr) : undefined),
[omezarr, view, planeIndex],
() =>
omezarr
? makeZarrSettings(screenSize, view, planeIndex, omezarr, [firstChannel, secondChannel, thirdChannel])
: undefined,
[omezarr, view, planeIndex, firstChannel, secondChannel, thirdChannel],
);

const dataset = omezarr?.getFirstShapedDataset(0);
const size = omezarr?.attrs && dataset ? sizeInUnits(PLANE_XY, omezarr.attrs.multiscales[0].axes, dataset) : [0, 0];

const load = (res: WebResource) => {
loadMetadata(res).then((v) => {
setOmezarr(v);
Expand All @@ -99,15 +117,20 @@ export function OmezarrDemo() {
}
const size = sizeInUnits(PLANE_XY, v.attrs.multiscales[0].axes, dataset);
if (size) {
logger.info('size', size);
setView(Box2D.create([0, 0], size));
logger.info('dataset size', size);
const aspectRatio = screenSize[0] / screenSize[1];
const adjustedSize: vec2 = [size[0], size[0] / aspectRatio];
setView(Box2D.create([0, 0], adjustedSize));
}
});
};

const handleOptionSelected = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = e.target.value;
setOmezarr(null);
setFirstChannel(undefined);
setSecondChannel(undefined);
setThirdChannel(undefined);
setSelectedDemoOptionValue(selectedValue);
if (selectedValue && selectedValue !== 'custom') {
const option = demoOptions.find((v) => v.value === selectedValue);
Expand Down Expand Up @@ -135,6 +158,18 @@ export function OmezarrDemo() {
setPlaneIndex((prev) => Math.max(0, Math.min(prev + next, (omezarr?.maxOrthogonal(PLANE_XY) ?? 1) - 1)));
};

const zoomIn = () => {
const zoomFactor = 1 - zoomStep; // Zoom in factor
const v = zoom(view, screenSize, zoomFactor, Vec2.div(screenSize, [2, 2]));
setView(v);
};

const zoomOut = () => {
const zoomFactor = 1 / (1 - zoomStep); // Reciprocal of zoom in factor
const v = zoom(view, screenSize, zoomFactor, Vec2.div(screenSize, [2, 2]));
setView(v);
};

const handleZoom = (e: WheelEvent) => {
e.preventDefault();
const zoomScale = e.deltaY > 0 ? 1.1 : 0.9;
Expand All @@ -157,6 +192,18 @@ export function OmezarrDemo() {
setDragging(false);
};

const channelsArray = Object.entries(settings?.channels ?? {});
const firstChannelData = channelsArray[0];
const secondChannelData = channelsArray[1];
const thirdChannelData = channelsArray[2];

const firstChannelMin = firstChannelData?.[1]?.gamut.min ?? 0;
const firstChannelMax = firstChannelData?.[1]?.gamut.max ?? 255;
const secondChannelMin = secondChannelData?.[1]?.gamut.min ?? 0;
const secondChannelMax = secondChannelData?.[1]?.gamut.max ?? 255;
const thirdChannelMin = thirdChannelData?.[1]?.gamut.min ?? 0;
const thirdChannelMax = thirdChannelData?.[1]?.gamut.max ?? 255;

return (
<RenderServerProvider>
<div style={{ display: 'flex', flexDirection: 'column' }}>
Expand Down Expand Up @@ -236,14 +283,101 @@ export function OmezarrDemo() {
Slide {planeIndex + 1} of {omezarr?.maxOrthogonal(PLANE_XY) ?? 0}
</span>
)) || <span>No image loaded</span>}
<div style={{}}>
<button type="button" onClick={() => handlePlaneIndex(-1)}>
&#9664;
</button>
<button type="button" onClick={() => handlePlaneIndex(1)}>
&#9654;
</button>
</div>
{omezarr && (
<div>
<button type="button" onClick={() => handlePlaneIndex(-1)}>
&#9664;
</button>
<button type="button" onClick={() => handlePlaneIndex(1)}>
&#9654;
</button>
</div>
)}
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: '8px',
justifyContent: 'space-between',
}}
>
{omezarr && size && (
<>
<span>
Zoom{' '}
{((size[0] / (view.maxCorner[0] - view.minCorner[0])) * 100).toFixed(2)}%
</span>
<div>
<button type="button" onClick={zoomIn}>
&#43;
</button>
<button type="button" onClick={zoomOut}>
&#8722;
</button>
<button
type="button"
onClick={() => {
const aspectRatio = screenSize[0] / screenSize[1];
const adjustedSize: vec2 = [size[0], size[0] / aspectRatio];
setView(Box2D.create([0, 0], adjustedSize));
}}
>
Reset
</button>
</div>
</>
)}
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: '8px',
}}
>
{omezarr && (
<>
<div>
<div>
{firstChannelData[0]} ({firstChannel}):{' '}
</div>
<input
type="range"
min={firstChannelMin}
max={firstChannelMax}
onChange={(e) => setFirstChannel(Number(e.target.value))}
style={{ accentColor: 'red' }}
/>
</div>
<div>
<div>
{secondChannelData[0]} ({secondChannel}):{' '}
</div>
<input
type="range"
min={secondChannelMin}
max={secondChannelMax}
onChange={(e) => setSecondChannel(Number(e.target.value))}
style={{ accentColor: 'green' }}
/>
</div>
{thirdChannelData && (
<div>
<div>
{thirdChannelData?.[0]} ({thirdChannel}):{' '}
</div>
<input
type="range"
min={thirdChannelMin}
max={thirdChannelMax}
onChange={(e) => setThirdChannel(Number(e.target.value))}
style={{ accentColor: 'blue' }}
/>
</div>
)}
</>
)}
</div>
</div>
</div>
Expand Down