vitessce
Version:
Vitessce app and React component library
386 lines (370 loc) • 14.4 kB
JavaScript
/* eslint-disable dot-notation */
import React, {
useEffect, useCallback, useRef, forwardRef,
} from 'react';
import Grid from '@material-ui/core/Grid';
import TitleInfo from '../TitleInfo';
import RasterChannelController from './RasterChannelController';
import BitmaskChannelController from './BitmaskChannelController';
import VectorLayerController from './VectorLayerController';
import LayerController from './LayerController';
import ImageAddButton from './ImageAddButton';
import { useReady, useClosestVitessceContainerSize, useWindowDimensions } from '../hooks';
import { useCellsData, useMoleculesData, useRasterData } from '../data-hooks';
import {
useCoordination,
useLoaders,
useAuxiliaryCoordination,
useComponentLayout,
} from '../../app/state/hooks';
import { COMPONENT_COORDINATION_TYPES } from '../../app/state/coordination';
import { initializeLayerChannels } from '../spatial/utils';
import { DEFAULT_RASTER_LAYER_PROPS } from '../spatial/constants';
const LAYER_CONTROLLER_DATA_TYPES = ['raster'];
// LayerController is memoized to prevent updates from prop changes that
// are caused by view state updates i.e zooming and panning within
// the actual Spatial component. Re-rendering this component is very
// expensive so we have to be careful with props in this file in general.
const LayerControllerMemoized = React.memo(
forwardRef((props, ref) => {
const {
title,
removeGridComponent,
theme,
isReady,
moleculesLayer,
dataset,
setMoleculesLayer,
cellsLayer,
canShowCellVecmask,
setCellsLayer,
rasterLayers,
imageLayerLoaders,
imageLayerMeta,
rasterLayersCallbacks,
setRasterLayersCallbacks,
areLoadingRasterChannnels,
setAreLoadingRasterChannnels,
handleRasterLayerChange,
handleRasterLayerRemove,
disable3d,
globalDisable3d,
disableChannelsIfRgbDetected,
layerIs3DIndex,
setZoom,
setTargetX,
setTargetY,
setTargetZ,
setRotationX,
setRotationOrbit,
componentHeight,
componentWidth,
spatialLayout,
handleImageAdd,
enableLayerButtonsWithOneLayer,
} = props;
const shouldShowImageLayerButton = Boolean(
enableLayerButtonsWithOneLayer || imageLayerLoaders?.length > 1,
);
return (
<TitleInfo
title={title}
isScroll
removeGridComponent={removeGridComponent}
theme={theme}
isReady={isReady}
>
<div className="layer-controller-container" ref={ref}>
{moleculesLayer && (
<VectorLayerController
key={`${dataset}-molecules`}
label="Molecules"
layerType="molecules"
layer={moleculesLayer}
handleLayerChange={setMoleculesLayer}
/>
)}
{cellsLayer && canShowCellVecmask && (
<VectorLayerController
key={`${dataset}-cells`}
label="Cell Segmentations"
layerType="cells"
layer={cellsLayer}
handleLayerChange={setCellsLayer}
/>
)}
{rasterLayers
&& rasterLayers.map((layer, i) => {
const { index } = layer;
const loader = imageLayerLoaders[index];
const layerMeta = imageLayerMeta[index];
// Could also be bitmask at the moment.
const isRaster = !layerMeta?.metadata?.isBitmask;
const ChannelController = isRaster
? RasterChannelController
: BitmaskChannelController;
// Set up the call back mechanism so that each layer manages
// callbacks/loading state for itself and its channels.
const setRasterLayerCallback = (cb) => {
const newRasterLayersCallbacks = [
...(rasterLayersCallbacks || []),
];
newRasterLayersCallbacks[i] = cb;
setRasterLayersCallbacks(newRasterLayersCallbacks);
};
const areLayerChannelsLoading = (areLoadingRasterChannnels || [])[i] || [];
const setAreLayerChannelsLoading = (v) => {
const newAreLoadingRasterChannnels = [
...(areLoadingRasterChannnels || []),
];
newAreLoadingRasterChannnels[i] = v;
setAreLoadingRasterChannnels(newAreLoadingRasterChannnels);
};
return loader && layerMeta ? (
<Grid
// eslint-disable-next-line react/no-array-index-key
key={`${dataset}-raster-${index}-${i}`}
item
style={{ marginTop: '10px' }}
>
<LayerController
name={layerMeta.name}
layer={layer}
loader={loader}
theme={theme}
handleLayerChange={v => handleRasterLayerChange(v, i)}
handleLayerRemove={() => handleRasterLayerRemove(i)}
ChannelController={ChannelController}
shouldShowTransparentColor={isRaster}
shouldShowDomain={isRaster}
shouldShowColormap={isRaster}
// Disable 3D if given explicit instructions to do so
// or if another layer is using 3D mode.
disable3d={
globalDisable3d
|| (disable3d || []).indexOf(layerMeta.name) >= 0
|| (typeof layerIs3DIndex === 'number'
&& layerIs3DIndex !== -1
&& layerIs3DIndex !== i)
}
disabled={
typeof layerIs3DIndex === 'number'
&& layerIs3DIndex !== -1
&& layerIs3DIndex !== i
}
disableChannelsIfRgbDetected={disableChannelsIfRgbDetected}
rasterLayersCallbacks={rasterLayersCallbacks}
setRasterLayerCallback={setRasterLayerCallback}
setViewState={({
zoom: newZoom,
target,
rotationX: newRotationX,
rotationOrbit: newRotationOrbit,
}) => {
setZoom(newZoom);
setTargetX(target[0]);
setTargetY(target[1]);
setTargetZ(target[2]);
setRotationX(newRotationX);
setRotationOrbit(newRotationOrbit);
}}
setAreLayerChannelsLoading={setAreLayerChannelsLoading}
areLayerChannelsLoading={areLayerChannelsLoading}
spatialHeight={(componentHeight * (spatialLayout ? spatialLayout.h : 1)) / 12}
spatialWidth={(componentWidth * (spatialLayout ? spatialLayout.w : 1)) / 12}
shouldShowRemoveLayerButton={shouldShowImageLayerButton}
/>
</Grid>
) : null;
})}
{shouldShowImageLayerButton
? (
<Grid item>
<ImageAddButton
imageOptions={imageLayerMeta}
handleImageAdd={handleImageAdd}
/>
</Grid>
) : null}
</div>
</TitleInfo>
);
}),
);
/**
* A subscriber component for the spatial layer controller.
* @param {object} props
* @param {string} props.theme The current theme name.
* @param {object} props.coordinationScopes The mapping from coordination types to coordination
* scopes.
* @param {function} props.removeGridComponent The callback function to pass to TitleInfo,
* to call when the component has been removed from the grid.
* @param {string} props.title The component title.
* @param {Object} props.disable3d Which layers should have 3D disabled (from `raster.json` names).
* @param {boolean} props.globalDisable3d Disable 3D for all layers. Overrides the `disable3d` prop.
* @param {boolean} props.disableChannelsIfRgbDetected Disable channel controls if an
* RGB image is detected i.e 3 channel 8 bit.
* @param {boolean} props.enableLayerButtonsWithOneLayer If there is only one layer,
* show the the layer add/remove buttons.
*/
function LayerControllerSubscriber(props) {
const {
coordinationScopes,
removeGridComponent,
theme,
title = 'Spatial Layers',
disable3d,
globalDisable3d,
disableChannelsIfRgbDetected,
enableLayerButtonsWithOneLayer,
} = props;
const loaders = useLoaders();
// Get "props" from the coordination space.
const [
{
dataset,
spatialImageLayer: rasterLayers,
spatialSegmentationLayer: cellsLayer,
spatialPointLayer: moleculesLayer,
},
{
setSpatialImageLayer: setRasterLayers,
setSpatialSegmentationLayer: setCellsLayer,
setSpatialPointLayer: setMoleculesLayer,
setSpatialTargetX: setTargetX,
setSpatialTargetY: setTargetY,
setSpatialTargetZ: setTargetZ,
setSpatialRotationX: setRotationX,
setSpatialRotationOrbit: setRotationOrbit,
setSpatialZoom: setZoom,
},
] = useCoordination(
COMPONENT_COORDINATION_TYPES.layerController,
coordinationScopes,
);
const [
{
rasterLayersCallbacks,
areLoadingRasterChannnels,
},
{
setRasterLayersCallbacks,
setAreLoadingRasterChannnels,
},
] = useAuxiliaryCoordination(
COMPONENT_COORDINATION_TYPES.layerController,
coordinationScopes,
);
// Spatial layout + window size is needed for the "re-center" button to work properly.
// Dimensions of the Spatial component can be inferred and used for resetting view state to
// a nice, centered view.
const [spatialLayout] = useComponentLayout('spatial', ['spatialImageLayer'], coordinationScopes);
const layerControllerRef = useRef();
const [componentWidth, componentHeight] = useClosestVitessceContainerSize(layerControllerRef);
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
const [
isReady,
setItemIsReady,
setItemIsNotReady, // eslint-disable-line no-unused-vars
resetReadyItems,
] = useReady(
LAYER_CONTROLLER_DATA_TYPES,
);
// Reset loader progress when the dataset has changed.
useEffect(() => {
resetReadyItems();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loaders, dataset]);
// Get data from loaders using the data hooks.
// eslint-disable-next-line no-unused-vars
const [raster, imageLayerLoaders, imageLayerMeta] = useRasterData(
loaders, dataset, setItemIsReady, () => { }, false,
{ setSpatialImageLayer: setRasterLayers },
{ spatialImageLayer: rasterLayers },
);
useCellsData(
loaders, dataset, setItemIsReady, () => {}, false,
{ setSpatialSegmentationLayer: setCellsLayer },
{ spatialSegmentationLayer: cellsLayer },
);
useMoleculesData(
loaders, dataset, setItemIsReady, () => {}, false,
{ setSpatialPointLayer: setMoleculesLayer },
{ spatialPointLayer: moleculesLayer },
);
// useCallback prevents new functions from propogating
// changes to the underlying component.
const handleImageAdd = useCallback(async (index) => {
const loader = imageLayerLoaders[index];
const newChannels = await initializeLayerChannels(
loader,
(rasterLayers[index] || {}).use3d,
);
const newLayer = {
index,
modelMatrix: imageLayerMeta[index]?.metadata?.transform?.matrix,
...DEFAULT_RASTER_LAYER_PROPS,
channels: newChannels,
type: imageLayerMeta[index]?.metadata?.isBitmask ? 'bitmask' : 'raster',
};
const newLayers = [...rasterLayers, newLayer];
setRasterLayers(newLayers);
}, [imageLayerLoaders, imageLayerMeta, rasterLayers, setRasterLayers]);
const handleRasterLayerChange = useCallback((newLayer, i) => {
const newLayers = [...rasterLayers];
newLayers[i] = newLayer;
setRasterLayers(newLayers);
}, [rasterLayers, setRasterLayers]);
const handleRasterLayerRemove = useCallback((i) => {
const newLayers = [...rasterLayers];
newLayers.splice(i, 1);
setRasterLayers(newLayers);
}, [rasterLayers, setRasterLayers]);
const hasNoBitmask = (
imageLayerMeta.length ? imageLayerMeta : [{ metadata: { isBitmask: true } }]
).every(l => !l?.metadata?.isBitmask);
// Only want to show vector cells controller if there is no bitmask
const canShowCellVecmask = hasNoBitmask;
const layerIs3DIndex = rasterLayers?.findIndex && rasterLayers.findIndex(layer => layer.use3d);
return (
<LayerControllerMemoized
ref={layerControllerRef}
title={title}
removeGridComponent={removeGridComponent}
theme={theme}
isReady={isReady}
moleculesLayer={moleculesLayer}
dataset={dataset}
setMoleculesLayer={setMoleculesLayer}
cellsLayer={cellsLayer}
canShowCellVecmask={canShowCellVecmask}
setCellsLayer={setCellsLayer}
rasterLayers={rasterLayers}
imageLayerLoaders={imageLayerLoaders}
imageLayerMeta={imageLayerMeta}
rasterLayersCallbacks={rasterLayersCallbacks}
setRasterLayersCallbacks={setRasterLayersCallbacks}
areLoadingRasterChannnels={areLoadingRasterChannnels}
setAreLoadingRasterChannnels={setAreLoadingRasterChannnels}
handleRasterLayerChange={handleRasterLayerChange}
handleRasterLayerRemove={handleRasterLayerRemove}
disable3d={disable3d}
globalDisable3d={globalDisable3d}
layerIs3DIndex={layerIs3DIndex}
disableChannelsIfRgbDetected={disableChannelsIfRgbDetected}
enableLayerButtonsWithOneLayer={enableLayerButtonsWithOneLayer}
setZoom={setZoom}
setTargetX={setTargetX}
setTargetY={setTargetY}
setTargetZ={setTargetZ}
setRotationX={setRotationX}
setRotationOrbit={setRotationOrbit}
// Fall back to window for height and width.
componentHeight={componentHeight || windowHeight}
componentWidth={componentWidth || windowWidth}
spatialLayout={spatialLayout}
handleImageAdd={handleImageAdd}
/>
);
}
export default LayerControllerSubscriber;