@vitessce/neuroglancer 3.9.5 → 3.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { N } from "./index-Wdrc02VW.js";
1
+ import { N } from "./index-DvhFVdN_.js";
2
2
  export {
3
3
  N as NeuroglancerSubscriber
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAkDA,gEAgeC"}
1
+ {"version":3,"file":"NeuroglancerSubscriber.d.ts","sourceRoot":"","sources":["../src/NeuroglancerSubscriber.js"],"names":[],"mappings":"AAsEA,gEAusBC"}
@@ -1,11 +1,14 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* eslint-disable max-len */
2
3
  /* eslint-disable no-unused-vars */
3
- import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react';
4
- import { TitleInfo, useCoordination, useObsSetsData, useLoaders, useObsEmbeddingData, useCoordinationScopes, } from '@vitessce/vit-s';
5
- import { ViewHelpMapping, ViewType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
4
+ import React, { useCallback, useMemo, useRef, useEffect, useState, useReducer } from 'react';
5
+ import { TitleInfo, useReady, useInitialCoordination, useCoordination, useCoordinationScopes, useCoordinationScopesBy, useComplexCoordination, useMultiCoordinationScopesNonNull, useMultiCoordinationScopesSecondaryNonNull, useComplexCoordinationSecondary, useLoaders, useMergeCoordination, useMultiObsPoints, usePointMultiObsFeatureMatrixIndices, useMultiObsSegmentations, useSegmentationMultiFeatureSelection, useSegmentationMultiObsFeatureMatrixIndices, useSegmentationMultiObsSets, useGridItemSize, } from '@vitessce/vit-s';
6
+ import { ViewHelpMapping, ViewType, CoordinationType, COMPONENT_COORDINATION_TYPES, } from '@vitessce/constants-internal';
6
7
  import { mergeObsSets, getCellColors, setObsSelection } from '@vitessce/sets-utils';
8
+ import { MultiLegend } from '@vitessce/legend';
7
9
  import { NeuroglancerComp } from './Neuroglancer.js';
8
10
  import { useNeuroglancerViewerState } from './data-hook-ng-utils.js';
11
+ import { useMemoCustomComparison, customIsEqualForCellColors, } from './use-memo-custom-comparison.js';
9
12
  import { useStyles } from './styles.js';
10
13
  import { quaternionToEuler, eulerToQuaternion, valueGreaterThanEpsilon, nearEq, makeVitNgZoomCalibrator, conjQuat, multiplyQuat, rad2deg, deg2rad, Q_Y_UP, } from './utils.js';
11
14
  const VITESSCE_INTERACTION_DELAY = 50;
@@ -24,21 +27,169 @@ function rgbToHex(rgb) {
24
27
  : `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`);
25
28
  }
26
29
  export function NeuroglancerSubscriber(props) {
27
- const { coordinationScopes: coordinationScopesRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER, } = props;
30
+ const { uuid, coordinationScopes: coordinationScopesRaw, coordinationScopesBy: coordinationScopesByRaw, closeButtonVisible, downloadButtonVisible, removeGridComponent, theme, title = 'Spatial', subtitle = 'Powered by Neuroglancer', helpText = ViewHelpMapping.NEUROGLANCER,
31
+ // Note: this is a temporary mechanism
32
+ // to pass an initial NG camera state.
33
+ // Ideally, all camera state should be passed via
34
+ // the existing spatialZoom, spatialTargetX, spatialRotationOrbit, etc,
35
+ // and then NeuroglancerSubscriber should internally convert
36
+ // to NG-compatible values, which would eliminate the need for this.
37
+ initialNgCameraState, } = props;
28
38
  const loaders = useLoaders();
39
+ const mergeCoordination = useMergeCoordination();
40
+ // Acccount for possible meta-coordination.
29
41
  const coordinationScopes = useCoordinationScopes(coordinationScopesRaw);
42
+ const coordinationScopesBy = useCoordinationScopesBy(coordinationScopes, coordinationScopesByRaw);
30
43
  const [{ dataset, obsType, spatialZoom, spatialTargetX, spatialTargetY, spatialRotationX, spatialRotationY, spatialRotationZ, spatialRotationOrbit,
31
44
  // spatialOrbitAxis, // always along Y-axis - not used in conversion
32
45
  embeddingType: mapping, obsSetColor: cellSetColor, obsSetSelection: cellSetSelection, additionalObsSets: additionalCellSets, }, { setAdditionalObsSets: setAdditionalCellSets, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialRotationX: setRotationX,
33
46
  // setSpatialRotationY: setRotationY,
34
47
  // setSpatialRotationZ: setRotationZ,
35
48
  setSpatialRotationOrbit: setRotationOrbit, setSpatialZoom: setZoom, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.NEUROGLANCER], coordinationScopes);
49
+ const [ngWidth, ngHeight, containerRef] = useGridItemSize();
50
+ const [segmentationLayerScopes, segmentationChannelScopesByLayer,] = useMultiCoordinationScopesSecondaryNonNull(CoordinationType.SEGMENTATION_CHANNEL, CoordinationType.SEGMENTATION_LAYER, coordinationScopes, coordinationScopesBy);
51
+ const pointLayerScopes = useMultiCoordinationScopesNonNull(CoordinationType.POINT_LAYER, coordinationScopes);
52
+ // Object keys are coordination scope names for spatialSegmentationLayer.
53
+ const segmentationLayerCoordination = useComplexCoordination([
54
+ CoordinationType.FILE_UID,
55
+ CoordinationType.SEGMENTATION_CHANNEL,
56
+ CoordinationType.SPATIAL_LAYER_VISIBLE,
57
+ CoordinationType.SPATIAL_LAYER_OPACITY,
58
+ ], coordinationScopes, coordinationScopesBy, CoordinationType.SEGMENTATION_LAYER);
59
+ // Object keys are coordination scope names for spatialSegmentationChannel.
60
+ const segmentationChannelCoordination = useComplexCoordinationSecondary([
61
+ CoordinationType.OBS_TYPE,
62
+ CoordinationType.SPATIAL_TARGET_C,
63
+ CoordinationType.SPATIAL_CHANNEL_VISIBLE,
64
+ CoordinationType.SPATIAL_CHANNEL_OPACITY,
65
+ CoordinationType.SPATIAL_CHANNEL_COLOR,
66
+ CoordinationType.SPATIAL_SEGMENTATION_FILLED,
67
+ CoordinationType.SPATIAL_SEGMENTATION_STROKE_WIDTH,
68
+ CoordinationType.OBS_COLOR_ENCODING,
69
+ CoordinationType.FEATURE_SELECTION,
70
+ CoordinationType.FEATURE_AGGREGATION_STRATEGY,
71
+ CoordinationType.FEATURE_VALUE_COLORMAP,
72
+ CoordinationType.FEATURE_VALUE_COLORMAP_RANGE,
73
+ CoordinationType.OBS_SET_COLOR,
74
+ CoordinationType.OBS_SET_SELECTION,
75
+ CoordinationType.ADDITIONAL_OBS_SETS,
76
+ CoordinationType.OBS_HIGHLIGHT,
77
+ CoordinationType.TOOLTIPS_VISIBLE,
78
+ CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
79
+ CoordinationType.LEGEND_VISIBLE,
80
+ ], coordinationScopes, coordinationScopesBy, CoordinationType.SEGMENTATION_LAYER, CoordinationType.SEGMENTATION_CHANNEL);
81
+ // Point layer
82
+ const pointLayerCoordination = useComplexCoordination([
83
+ CoordinationType.OBS_TYPE,
84
+ CoordinationType.SPATIAL_LAYER_VISIBLE,
85
+ CoordinationType.SPATIAL_LAYER_OPACITY,
86
+ CoordinationType.OBS_COLOR_ENCODING,
87
+ CoordinationType.FEATURE_COLOR,
88
+ CoordinationType.FEATURE_FILTER_MODE,
89
+ CoordinationType.FEATURE_SELECTION,
90
+ CoordinationType.FEATURE_VALUE_COLORMAP,
91
+ CoordinationType.FEATURE_VALUE_COLORMAP_RANGE,
92
+ CoordinationType.SPATIAL_LAYER_COLOR,
93
+ CoordinationType.OBS_HIGHLIGHT,
94
+ CoordinationType.TOOLTIPS_VISIBLE,
95
+ CoordinationType.TOOLTIP_CROSSHAIRS_VISIBLE,
96
+ CoordinationType.LEGEND_VISIBLE,
97
+ ], coordinationScopes, coordinationScopesBy, CoordinationType.POINT_LAYER);
98
+ // Points data
99
+ const [obsPointsData, obsPointsDataStatus, obsPointsUrls, obsPointsErrors] = useMultiObsPoints(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
100
+ const [pointMultiIndicesData, pointMultiIndicesDataStatus, pointMultiIndicesDataErrors] = usePointMultiObsFeatureMatrixIndices(coordinationScopes, coordinationScopesBy, loaders, dataset);
101
+ // Segmentations data
102
+ const [obsSegmentationsData, obsSegmentationsDataStatus, obsSegmentationsUrls, obsSegmentationsDataErrors] = useMultiObsSegmentations(coordinationScopes, coordinationScopesBy, loaders, dataset, mergeCoordination, uuid);
103
+ const [obsSegmentationsSetsData, obsSegmentationsSetsDataStatus, obsSegmentationsSetsDataErrors] = useSegmentationMultiObsSets(coordinationScopes, coordinationScopesBy, loaders, dataset);
104
+ const [segmentationMultiExpressionData, segmentationMultiLoadedFeatureSelection, segmentationMultiExpressionExtents, segmentationMultiExpressionNormData, segmentationMultiFeatureSelectionStatus, segmentationMultiFeatureSelectionErrors,] = useSegmentationMultiFeatureSelection(coordinationScopes, coordinationScopesBy, loaders, dataset);
105
+ const [segmentationMultiIndicesData, segmentationMultiIndicesDataStatus, segmentationMultiIndicesDataErrors] = useSegmentationMultiObsFeatureMatrixIndices(coordinationScopes, coordinationScopesBy, loaders, dataset);
106
+ const errors = [
107
+ ...obsPointsErrors,
108
+ ...obsSegmentationsDataErrors,
109
+ ...obsSegmentationsSetsDataErrors,
110
+ ...pointMultiIndicesDataErrors,
111
+ ...segmentationMultiFeatureSelectionErrors,
112
+ ...segmentationMultiIndicesDataErrors,
113
+ ];
114
+ const isReady = useReady([
115
+ // Points
116
+ obsPointsDataStatus,
117
+ pointMultiIndicesDataStatus,
118
+ // Segmentations
119
+ obsSegmentationsDataStatus,
120
+ obsSegmentationsSetsDataStatus,
121
+ segmentationMultiFeatureSelectionStatus,
122
+ segmentationMultiIndicesDataStatus,
123
+ ]);
36
124
  // console.log("NG Subs Render orbit", spatialRotationX, spatialRotationY, spatialRotationOrbit);
37
125
  const { classes } = useStyles();
38
- const [{ obsSets: cellSets }] = useObsSetsData(loaders, dataset, false, { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor }, { cellSetSelection, obsSetColor: cellSetColor }, { obsType });
39
- const [{ obsIndex }] = useObsEmbeddingData(loaders, dataset, true, {}, {}, { obsType, embeddingType: mapping });
40
- const [initalViewerState] = useNeuroglancerViewerState(loaders, dataset, false, undefined, undefined, { obsType: 'cell' });
41
- const latestViewerStateRef = useRef(initalViewerState);
126
+ const segmentationColorMapping = useMemoCustomComparison(() => {
127
+ // TODO: ultimately, segmentationColorMapping becomes cellColorMapping, and makes its way into the viewerState.
128
+ // It may make sense to merge the multiple useMemoCustomComparisons upstream of derivedViewerState into one.
129
+ // This would complicate the comparison function, but the multiple separate useMemos are not really necessary.
130
+ const result = {};
131
+ segmentationLayerScopes?.forEach((layerScope) => {
132
+ result[layerScope] = {};
133
+ segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
134
+ const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData?.[layerScope]?.[channelScope] || {};
135
+ if (layerSets && layerIndex) {
136
+ const { obsSetColor, obsColorEncoding, obsSetSelection, additionalObsSets, } = segmentationChannelCoordination[0][layerScope][channelScope];
137
+ const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
138
+ const cellColors = getCellColors({
139
+ cellSets: mergedCellSets,
140
+ cellSetSelection: obsSetSelection,
141
+ cellSetColor: obsSetColor,
142
+ obsIndex: layerIndex,
143
+ theme,
144
+ });
145
+ // Convert the list of colors to an object of hex strings, which NG requires.
146
+ const ngCellColors = {};
147
+ cellColors.forEach((color, i) => {
148
+ ngCellColors[i] = rgbToHex(color);
149
+ });
150
+ /* // TODO: Is this necessary?
151
+ const obsColorIndices = treeToCellSetColorIndicesBySetNames(
152
+ mergedLayerSets,
153
+ obsSetSelection,
154
+ obsSetColor,
155
+ );
156
+ */
157
+ result[layerScope][channelScope] = ngCellColors;
158
+ }
159
+ });
160
+ });
161
+ return result;
162
+ }, {
163
+ // The dependencies for the comparison,
164
+ // used by the custom equality function.
165
+ segmentationLayerScopes,
166
+ segmentationChannelScopesByLayer,
167
+ obsSegmentationsSetsData,
168
+ segmentationChannelCoordination,
169
+ theme,
170
+ }, customIsEqualForCellColors);
171
+ // Obtain the Neuroglancer viewerState object.
172
+ const initalViewerState = useNeuroglancerViewerState(theme, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData);
173
+ const [latestViewerStateIteration, incrementLatestViewerStateIteration] = useReducer(x => x + 1, 0);
174
+ const latestViewerStateRef = useRef({
175
+ ...initalViewerState,
176
+ ...(initialNgCameraState ?? {}),
177
+ });
178
+ useEffect(() => {
179
+ const prevNgCameraState = {
180
+ position: latestViewerStateRef.current.position,
181
+ projectionOrientation: latestViewerStateRef.current.projectionOrientation,
182
+ projectionScale: latestViewerStateRef.current.projectionScale,
183
+ };
184
+ latestViewerStateRef.current = {
185
+ ...initalViewerState,
186
+ ...prevNgCameraState,
187
+ };
188
+ // Force a re-render by incrementing a piece of state.
189
+ // This works because we have made latestViewerStateIteration
190
+ // a dependency for derivedViewerState, triggering the useMemo downstream.
191
+ incrementLatestViewerStateIteration();
192
+ }, [initalViewerState]);
42
193
  const initialRotationPushedRef = useRef(false);
43
194
  const ngRotPushAtRef = useRef(0);
44
195
  const lastInteractionSource = useRef(null);
@@ -67,15 +218,6 @@ export function NeuroglancerSubscriber(props) {
67
218
  tx: spatialTargetX,
68
219
  ty: spatialTargetY,
69
220
  });
70
- const mergedCellSets = useMemo(() => mergeObsSets(cellSets, additionalCellSets), [cellSets, additionalCellSets]);
71
- const cellColors = useMemo(() => getCellColors({
72
- cellSets: mergedCellSets,
73
- cellSetSelection,
74
- cellSetColor,
75
- obsIndex,
76
- theme,
77
- }), [mergedCellSets, theme,
78
- cellSetColor, cellSetSelection, obsIndex]);
79
221
  /*
80
222
  * handleStateUpdate - Interactions from NG to Vitessce are pushed here
81
223
  */
@@ -190,35 +332,29 @@ export function NeuroglancerSubscriber(props) {
190
332
  if (alreadySelectedId) {
191
333
  return;
192
334
  }
335
+ // TODO: update this now that we are using layer/channel-based organization of segmentations.
336
+ // There is no more "top-level" obsSets coordination; it is only on a per-layer basis.
337
+ // We should probably just assume the first segmentation layer/channel when updating the logic,
338
+ // since it is not clear how we would determine which layer/channel to update if there are multiple.
193
339
  setObsSelection(selectedCellIds, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, 'Selection ', `: based on selected segments ${value}`);
194
340
  }
195
341
  }, [additionalCellSets, cellSetColor, setAdditionalCellSets,
196
342
  setCellColorEncoding, setCellSetColor, setCellSetSelection,
197
343
  ]);
198
- const batchedUpdateTimeoutRef = useRef(null);
199
- const [batchedCellColors, setBatchedCellColors] = useState(cellColors);
200
- useEffect(() => {
201
- if (batchedUpdateTimeoutRef.current) {
202
- clearTimeout(batchedUpdateTimeoutRef.current);
203
- }
204
- batchedUpdateTimeoutRef.current = setTimeout(() => {
205
- setBatchedCellColors(cellColors);
206
- }, 100);
207
- // TODO: look into deferredValue from React
208
- // startTransition(() => {
209
- // setBatchedCellColors(cellColors);
210
- // });
211
- }, [cellColors]);
212
- // TODO use a ref if slow - see prev commits
213
- const cellColorMapping = useMemo(() => {
214
- const colorMapping = {};
215
- batchedCellColors.forEach((color, cell) => {
216
- colorMapping[cell] = rgbToHex(color);
217
- });
218
- return colorMapping;
219
- }, [batchedCellColors]);
344
+ // Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
345
+ // For now, we take the first layer and channel for cell colors.
346
+ const cellColorMapping = useMemo(() => (segmentationColorMapping?.[segmentationLayerScopes?.[0]]?.[segmentationChannelScopesByLayer?.[segmentationLayerScopes?.[0]]?.[0]]
347
+ ?? {}), [segmentationColorMapping]);
348
+ // TODO: try to simplify using useMemoCustomComparison?
349
+ // This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
350
+ // simplify some of the manual bookkeeping like with prevCoordsRef and lastInteractionSource,
351
+ // and would allow us to potentially remove usage of some refs (e.g., latestViewerStateRef)
352
+ // by relying on the memoization to prevent unnecessary updates.
220
353
  const derivedViewerState = useMemo(() => {
221
354
  const { current } = latestViewerStateRef;
355
+ if (current.layers.length <= 0) {
356
+ return current;
357
+ }
222
358
  const nextSegments = Object.keys(cellColorMapping);
223
359
  const prevLayer = current?.layers?.[0] || {};
224
360
  const prevSegments = prevLayer.segments || [];
@@ -351,13 +487,16 @@ export function NeuroglancerSubscriber(props) {
351
487
  };
352
488
  return updated;
353
489
  }, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
354
- spatialRotationZ, spatialTargetX, spatialTargetY]);
490
+ spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
491
+ latestViewerStateIteration]);
355
492
  const onSegmentHighlight = useCallback((obsId) => {
356
493
  setCellHighlight(String(obsId));
357
- }, [obsIndex, setCellHighlight]);
494
+ }, [setCellHighlight]);
358
495
  // TODO: if all cells are deselected, a black view is shown, rather we want to show empty NG view?
359
496
  // if (!cellColorMapping || Object.keys(cellColorMapping).length === 0) {
360
497
  // return;
361
498
  // }
362
- return (_jsx(TitleInfo, { title: title, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: true, withPadding: false, children: _jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMapping, setViewerState: handleStateUpdate }) }));
499
+ const hasLayers = derivedViewerState?.layers?.length > 0;
500
+ // console.log(derivedViewerState);
501
+ return (_jsx(TitleInfo, { title: title, info: subtitle, helpText: helpText, isSpatial: true, theme: theme, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, isReady: isReady, errors: errors, withPadding: false, children: _jsxs("div", { style: { position: 'relative', width: '100%', height: '100%' }, ref: containerRef, children: [_jsx("div", { style: { position: 'absolute', top: 0, right: 0, zIndex: 50 }, children: _jsx(MultiLegend, { theme: "dark", maxHeight: ngHeight, segmentationLayerScopes: segmentationLayerScopes, segmentationLayerCoordination: segmentationLayerCoordination, segmentationChannelScopesByLayer: segmentationChannelScopesByLayer, segmentationChannelCoordination: segmentationChannelCoordination }) }), hasLayers ? (_jsx(NeuroglancerComp, { classes: classes, onSegmentClick: onSegmentClick, onSelectHoveredCoords: onSegmentHighlight, viewerState: derivedViewerState, cellColorMapping: cellColorMapping, setViewerState: handleStateUpdate })) : null] }) }));
363
502
  }
@@ -1,22 +1,14 @@
1
- export function extractDataTypeEntities(loaders: any, dataset: any, dataType: any): {
2
- key: any;
3
- type: string;
4
- fileUid: any;
5
- layout: string;
6
- url: any;
7
- source: string | undefined;
8
- name: any;
9
- dimensions: {
10
- x: [number, "nm"];
11
- y: [number, "nm"];
12
- z: [number, "nm"];
13
- };
14
- position: any[];
15
- projectionOrientation: any[];
16
- projectionScale: any;
17
- crossSectionScale: any;
18
- }[];
19
- export function useExtractOptionsForNg(loaders: any, dataset: any, dataType: any): any[];
1
+ /**
2
+ * Normalize dimensionX/Y/Z to nanometers.
3
+ * @param {object} opts
4
+ * @returns {{ x:[number,'nm'], y:[number,'nm'], z:[number,'nm'] }}
5
+ */
6
+ export function normalizeDimensionsToNanometers(opts: object): {
7
+ x: [number, "nm"];
8
+ y: [number, "nm"];
9
+ z: [number, "nm"];
10
+ };
11
+ export function toNgLayerName(dataType: any, layerScope: any, channelScope?: null): string;
20
12
  /**
21
13
  * Get the parameters for NG's viewerstate.
22
14
  * @param {object} loaders The object mapping
@@ -29,12 +21,18 @@ export function useExtractOptionsForNg(loaders: any, dataset: any, dataType: any
29
21
  /**
30
22
  * @returns [viewerState]
31
23
  */
32
- export function useNeuroglancerViewerState(loaders: any, dataset: any, isRequired: any, coordinationSetters: any, initialCoordinationValues: any, matchOn: any): any[];
24
+ export function useNeuroglancerViewerState(theme: any, segmentationLayerScopes: any, segmentationChannelScopesByLayer: any, segmentationLayerCoordination: any, segmentationChannelCoordination: any, obsSegmentationsUrls: any, obsSegmentationsData: any, pointLayerScopes: any, pointLayerCoordination: any, obsPointsUrls: any, obsPointsData: any, pointMultiIndicesData: any): any;
33
25
  export namespace DEFAULT_NG_PROPS {
34
26
  let layout: string;
35
27
  let position: number[];
36
28
  let projectionOrientation: number[];
37
29
  let projectionScale: number;
38
30
  let crossSectionScale: number;
31
+ namespace dimensions {
32
+ let x: (string | number)[];
33
+ let y: (string | number)[];
34
+ let z: (string | number)[];
35
+ }
36
+ let layers: never[];
39
37
  }
40
38
  //# sourceMappingURL=data-hook-ng-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"data-hook-ng-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-ng-utils.js"],"names":[],"mappings":"AAgEA;;;;;;;;;WArBmB,CAAC,MAAM,EAAC,IAAI,CAAC;WAAI,CAAC,MAAM,EAAC,IAAI,CAAC;WAAI,CAAC,MAAM,EAAC,IAAI,CAAC;;;;;;IA6DjE;AAED,yFAyBC;AAGD;;;;;;;;GAQG;AACH;;GAEG;AACH,uKAKC"}
1
+ {"version":3,"file":"data-hook-ng-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-ng-utils.js"],"names":[],"mappings":"AAgDA;;;;KAIK;AACL,sDAHa,MAAM,GACJ;IAAE,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAC;IAAC,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAC;IAAC,CAAC,EAAC,CAAC,MAAM,EAAC,IAAI,CAAC,CAAA;CAAE,CA+BnE;AAED,2FAQC;AAED;;;;;;;;GAQG;AACH;;GAEG;AACH,yXAiJC"}
@@ -1,15 +1,23 @@
1
- import { useMemo } from 'react';
2
1
  import { DataType } from '@vitessce/constants-internal';
2
+ import { cloneDeep } from 'lodash-es';
3
+ import { useMemoCustomComparison, customIsEqualForInitialViewerState } from './use-memo-custom-comparison.js';
4
+ import { getPointsShader } from './shader-utils.js';
3
5
  export const DEFAULT_NG_PROPS = {
4
6
  layout: '3d',
5
7
  position: [0, 0, 0],
6
8
  projectionOrientation: [0, 0, 0, 1],
7
9
  projectionScale: 1024,
8
10
  crossSectionScale: 1,
11
+ dimensions: {
12
+ x: [1, 'nm'],
13
+ y: [1, 'nm'],
14
+ z: [1, 'nm'],
15
+ },
16
+ layers: [],
9
17
  };
10
18
  function toPrecomputedSource(url) {
11
19
  if (!url) {
12
- return undefined;
20
+ throw new Error('toPrecomputedSource: URL is required');
13
21
  }
14
22
  return `precomputed://${url}`;
15
23
  }
@@ -36,8 +44,8 @@ function isInNanometerRange(value, unit, minNm = 1, maxNm = 100) {
36
44
  * @param {object} opts
37
45
  * @returns {{ x:[number,'nm'], y:[number,'nm'], z:[number,'nm'] }}
38
46
  */
39
- function normalizeDimensionsToNanometers(opts) {
40
- const { dimensionUnit, dimensionX, dimensionY, dimensionZ } = opts;
47
+ export function normalizeDimensionsToNanometers(opts) {
48
+ const { dimensionUnit, dimensionX, dimensionY, dimensionZ, ...otherOptions } = opts;
41
49
  if (!dimensionUnit || !dimensionX || !dimensionY || !dimensionZ) {
42
50
  console.warn('Missing dimension info');
43
51
  }
@@ -48,70 +56,25 @@ function normalizeDimensionsToNanometers(opts) {
48
56
  console.warn('Dimension was converted to nm units');
49
57
  }
50
58
  return {
51
- x: xNm ? [dimensionX, dimensionUnit] : [1, 'nm'],
52
- y: yNm ? [dimensionY, dimensionUnit] : [1, 'nm'],
53
- z: zNm ? [dimensionZ, dimensionUnit] : [1, 'nm'],
59
+ // The dimension-related fields are formatted differently in the fileDef.options
60
+ // vs. what the viewerState expects.
61
+ dimensions: {
62
+ x: xNm ? [dimensionX, dimensionUnit] : [1, 'nm'],
63
+ y: yNm ? [dimensionY, dimensionUnit] : [1, 'nm'],
64
+ z: zNm ? [dimensionZ, dimensionUnit] : [1, 'nm'],
65
+ },
66
+ // The non-dimension-related options can be passed through without modification.
67
+ ...otherOptions,
54
68
  };
55
69
  }
56
- export function extractDataTypeEntities(loaders, dataset, dataType) {
57
- const datasetEntry = loaders?.[dataset];
58
- const internMap = datasetEntry?.loaders?.[dataType];
59
- if (!internMap || typeof internMap.entries !== 'function')
60
- return [];
61
- return Array.from(internMap.entries()).map(([key, loader]) => {
62
- const url = loader?.url ?? loader?.dataSource?.url ?? undefined;
63
- const fileUid = key?.fileUid
64
- ?? loader?.coordinationValues?.fileUid
65
- ?? undefined;
66
- const { position, projectionOrientation, projectionScale, crossSectionScale } = loader?.options ?? {};
67
- const isPrecomputed = loader?.fileType.includes('precomputed');
68
- if (!isPrecomputed) {
69
- console.warn('Filetype needs to be precomputed');
70
- }
71
- return {
72
- key,
73
- type: 'segmentation',
74
- fileUid,
75
- layout: DEFAULT_NG_PROPS.layout,
76
- url,
77
- source: toPrecomputedSource(url),
78
- name: fileUid ?? key?.name ?? 'segmentation',
79
- // For precomputed: nm is the unit used
80
- dimensions: normalizeDimensionsToNanometers(loader?.options),
81
- // If not provided, no error, but difficult to see the data
82
- position: Array.isArray(position) && position.length === 3
83
- ? position : DEFAULT_NG_PROPS.position,
84
- // If not provided, will have a default orientation
85
- projectionOrientation: Array.isArray(projectionOrientation)
86
- && projectionOrientation.length === 4
87
- ? projectionOrientation : DEFAULT_NG_PROPS.projectionOrientation,
88
- projectionScale: Number.isFinite(projectionScale)
89
- ? projectionScale : DEFAULT_NG_PROPS.projectionScale,
90
- crossSectionScale: Number.isFinite(crossSectionScale)
91
- ? crossSectionScale : DEFAULT_NG_PROPS.crossSectionScale,
92
- };
93
- });
94
- }
95
- export function useExtractOptionsForNg(loaders, dataset, dataType) {
96
- const extractedEntities = useMemo(() => extractDataTypeEntities(loaders, dataset, dataType), [loaders, dataset, dataType]);
97
- const layers = useMemo(() => extractedEntities
98
- .filter(t => t.source)
99
- .map(t => ({
100
- type: t.type,
101
- source: t.source,
102
- segments: [],
103
- name: t.name || 'segmentation',
104
- })), [extractedEntities]);
105
- const viewerState = useMemo(() => ({
106
- dimensions: extractedEntities[0]?.dimensions,
107
- position: extractedEntities[0]?.position,
108
- crossSectionScale: extractedEntities[0]?.crossSectionScale,
109
- projectionOrientation: extractedEntities[0]?.projectionOrientation,
110
- projectionScale: extractedEntities[0]?.projectionScale,
111
- layers,
112
- layout: extractedEntities[0].layout,
113
- }));
114
- return [viewerState];
70
+ export function toNgLayerName(dataType, layerScope, channelScope = null) {
71
+ if (dataType === DataType.OBS_SEGMENTATIONS) {
72
+ return `obsSegmentations-${layerScope}-${channelScope}`;
73
+ }
74
+ if (dataType === DataType.OBS_POINTS) {
75
+ return `obsPoints-${layerScope}`;
76
+ }
77
+ throw new Error(`Unsupported data type: ${dataType}`);
115
78
  }
116
79
  /**
117
80
  * Get the parameters for NG's viewerstate.
@@ -125,6 +88,111 @@ export function useExtractOptionsForNg(loaders, dataset, dataType) {
125
88
  /**
126
89
  * @returns [viewerState]
127
90
  */
128
- export function useNeuroglancerViewerState(loaders, dataset, isRequired, coordinationSetters, initialCoordinationValues, matchOn) {
129
- return useExtractOptionsForNg(loaders, dataset, DataType.OBS_SEGMENTATIONS, matchOn);
91
+ export function useNeuroglancerViewerState(theme, segmentationLayerScopes, segmentationChannelScopesByLayer, segmentationLayerCoordination, segmentationChannelCoordination, obsSegmentationsUrls, obsSegmentationsData, pointLayerScopes, pointLayerCoordination, obsPointsUrls, obsPointsData, pointMultiIndicesData) {
92
+ const viewerState = useMemoCustomComparison(() => {
93
+ let result = cloneDeep(DEFAULT_NG_PROPS);
94
+ // ======= SEGMENTATIONS =======
95
+ // Iterate over segmentation layers and channels.
96
+ segmentationLayerScopes.forEach((layerScope) => {
97
+ const layerCoordination = segmentationLayerCoordination[0][layerScope];
98
+ const channelScopes = segmentationChannelScopesByLayer[layerScope] || [];
99
+ const layerData = obsSegmentationsData[layerScope];
100
+ const layerUrl = obsSegmentationsUrls[layerScope]?.[0]?.url;
101
+ if (layerUrl && layerData) {
102
+ const { spatialLayerVisible, } = layerCoordination || {};
103
+ channelScopes.forEach((channelScope) => {
104
+ const channelCoordination = segmentationChannelCoordination[0]?.[layerScope]?.[channelScope];
105
+ const { spatialChannelVisible, } = channelCoordination || {};
106
+ result = {
107
+ ...result,
108
+ layers: [
109
+ ...result.layers,
110
+ {
111
+ type: 'segmentation',
112
+ source: toPrecomputedSource(layerUrl),
113
+ segments: [],
114
+ name: toNgLayerName(DataType.OBS_SEGMENTATIONS, layerScope, channelScope),
115
+ visible: spatialLayerVisible && spatialChannelVisible, // Both layer and channel
116
+ // visibility must be true for the layer to be visible.
117
+ // TODO: update this to extract specific properties from
118
+ // neuroglancerOptions as needed.
119
+ ...(layerData.neuroglancerOptions ?? {}),
120
+ },
121
+ ],
122
+ };
123
+ });
124
+ }
125
+ });
126
+ // ======= POINTS =======
127
+ // Iterate over point layers.
128
+ pointLayerScopes.forEach((layerScope) => {
129
+ const layerCoordination = pointLayerCoordination[0][layerScope];
130
+ const layerData = obsPointsData[layerScope];
131
+ const layerUrl = obsPointsUrls[layerScope]?.[0]?.url;
132
+ const featureIndex = pointMultiIndicesData[layerScope]?.featureIndex;
133
+ if (layerUrl && layerData) {
134
+ const { spatialLayerVisible, spatialLayerOpacity, obsColorEncoding, spatialLayerColor, featureSelection, featureFilterMode, featureColor, } = layerCoordination || {};
135
+ // Dynamically construct the shader based on the color encoding
136
+ // and other coordination values.
137
+ const shader = getPointsShader({
138
+ theme,
139
+ featureIndex,
140
+ spatialLayerOpacity,
141
+ obsColorEncoding,
142
+ spatialLayerColor,
143
+ featureSelection,
144
+ featureFilterMode,
145
+ featureColor,
146
+ featureIndexProp: layerData.neuroglancerOptions?.featureIndexProp,
147
+ pointIndexProp: layerData.neuroglancerOptions?.pointIndexProp,
148
+ });
149
+ result = {
150
+ ...result,
151
+ layers: [
152
+ ...result.layers,
153
+ {
154
+ type: 'annotation',
155
+ source: {
156
+ url: toPrecomputedSource(layerUrl),
157
+ subsources: {
158
+ default: true,
159
+ },
160
+ enableDefaultSubsources: false,
161
+ },
162
+ tab: 'annotations',
163
+ shader,
164
+ name: toNgLayerName(DataType.OBS_POINTS, layerScope),
165
+ visible: spatialLayerVisible,
166
+ // Options from layerData.neuroglancerOptions
167
+ // like projectionAnnotationSpacing:
168
+ projectionAnnotationSpacing: layerData.neuroglancerOptions
169
+ ?.projectionAnnotationSpacing ?? 1.0,
170
+ },
171
+ ],
172
+ // TODO: is this needed?
173
+ // The selected layer here will overwrite anything
174
+ // that was previously specified.
175
+ selectedLayer: {
176
+ // size: ? // TODO: is this needed?
177
+ layer: toNgLayerName(DataType.OBS_POINTS, layerScope),
178
+ },
179
+ };
180
+ }
181
+ });
182
+ return result;
183
+ }, {
184
+ theme,
185
+ segmentationLayerScopes,
186
+ segmentationChannelScopesByLayer,
187
+ segmentationLayerCoordination,
188
+ segmentationChannelCoordination,
189
+ obsSegmentationsUrls,
190
+ obsSegmentationsData,
191
+ pointLayerScopes,
192
+ pointLayerCoordination,
193
+ obsPointsUrls,
194
+ obsPointsData,
195
+ pointMultiIndicesData,
196
+ }, customIsEqualForInitialViewerState);
197
+ return viewerState;
130
198
  }