@vitessce/neuroglancer 3.6.18 → 3.7.1

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.
@@ -1,6 +1,820 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /* *****
3
+ This react wrapper is originally developed by janelia-flyem group now called neuroglancerHub
4
+ code source: https://github.com/neuroglancerhub/react-neuroglancer/blob/master/src/index.jsx
5
+
6
+ The following modifications were added for Vitessce integration and
7
+ are marked with a comment referring to Vitessce
8
+ 1. applyColorsAndVisibility() adds the cellColorMapping (prop) from the cell-set selection
9
+ 2. componentDidMount() and componentDidUpdate() renders and updates the viewerState using
10
+ the restoreState() for updating the camera setting and segments.
11
+ 3. A set of functions to avoid frequent state updates and provide smoother animations
12
+ within the rendering cycling using requestAnimationFrame()
13
+ */
14
+ /* eslint-disable max-len, consistent-return, react/destructuring-assignment, class-methods-use-this, no-restricted-syntax, no-continue, no-unused-vars, react/forbid-prop-types, no-dupe-keys */
2
15
  import React from 'react';
3
- import ReactNeuroglancer from '@janelia-flyem/react-neuroglancer';
4
- const Component = typeof ReactNeuroglancer.default === 'function' ? ReactNeuroglancer.default : ReactNeuroglancer;
5
- const ReactNeuroglancerWrapper = React.forwardRef((props, ref) => (_jsx(Component, { ref: ref, ...props })));
6
- export default ReactNeuroglancerWrapper;
16
+ import { AnnotationUserLayer } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/annotation/user_layer.js';
17
+ import { getObjectColor } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/segmentation_display_state/frontend.js';
18
+ import { SegmentationUserLayer } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/segmentation_user_layer.js';
19
+ import { serializeColor } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/util/color.js';
20
+ import { setupDefaultViewer } from '@janelia-flyem/neuroglancer';
21
+ import { Uint64 } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/util/uint64.js';
22
+ import { urlSafeParse } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/util/json.js';
23
+ /* eslint-disable max-len */
24
+ // import { encodeFragment } from '@janelia-flyem/neuroglancer/dist/module/neuroglancer/ui/url_hash_binding';
25
+ import { diffCameraState } from './utils.js';
26
+ // TODO: Grey color used by Vitessce - maybe set globally
27
+ const GREY_HEX = '#323232';
28
+ const viewersKeyed = {};
29
+ let viewerNoKey;
30
+ /**
31
+ * @typedef {Object} NgProps
32
+ * @property {number} perspectiveZoom
33
+ * @property {Object} viewerState
34
+ * @property {string} brainMapsClientId
35
+ * @property {string} key
36
+ * @property {Object<string, string>} cellColorMapping
37
+ *
38
+ * @property {Array} eventBindingsToUpdate
39
+ * An array of event bindings to change in Neuroglancer. The array format is as follows:
40
+ * [[old-event1, new-event1], [old-event2], old-event3]
41
+ * Here, `old-event1`'s will be unbound and its action will be re-bound to `new-event1`.
42
+ * The bindings for `old-event2` and `old-event3` will be removed.
43
+ * Neuroglancer has its own syntax for event descriptors, and here are some examples:
44
+ * 'keya', 'shift+keyb' 'control+keyc', 'digit4', 'space', 'arrowleft', 'comma', 'period',
45
+ * 'minus', 'equal', 'bracketleft'.
46
+ *
47
+ * @property {(segment:any|null, layer:any) => void} onSelectedChanged
48
+ * A function of the form `(segment, layer) => {}`, called each time there is a change to
49
+ * the segment the user has 'selected' (i.e., hovered over) in Neuroglancer.
50
+ * The `segment` argument will be a Neuroglancer `Uint64` with the ID of the now-selected
51
+ * segment, or `null` if no segment is now selected.
52
+ * The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
53
+ * will be a Neuroglancer `SegmentationUserLayer`.
54
+ *
55
+ * @property {(segments:any, layer:any) => void} onVisibleChanged
56
+ * A function of the form `(segments, layer) => {}`, called each time there is a change to
57
+ * the segments the user has designated as 'visible' (i.e., double-clicked on) in Neuroglancer.
58
+ * The `segments` argument will be a Neuroglancer `Uint64Set` whose elements are `Uint64`
59
+ * instances for the IDs of the now-visible segments.
60
+ * The `layer` argument will be a Neuroglaner `ManagedUserLayer`, whose `layer` property
61
+ * will be a Neuroglancer `SegmentationUserLayer`.
62
+ *
63
+ * @property {() => void} onSelectionDetailsStateChanged
64
+ * A function of the form `() => {}` to respond to selection changes in the viewer.
65
+ * @property {() => void} onViewerStateChanged
66
+ *
67
+ * @property {Array<Object>} callbacks
68
+ * // ngServer: string,
69
+ */
70
+ // Adopted from neuroglancer/ui/url_hash_binding.ts
71
+ export function parseUrlHash(url) {
72
+ let state = null;
73
+ let s = url.replace(/^[^#]+/, '');
74
+ if (s === '' || s === '#' || s === '#!') {
75
+ s = '#!{}';
76
+ }
77
+ if (s.startsWith('#!+')) {
78
+ s = s.slice(3);
79
+ // Firefox always %-encodes the URL even if it is not typed that way.
80
+ s = decodeURIComponent(s);
81
+ state = urlSafeParse(s);
82
+ }
83
+ else if (s.startsWith('#!')) {
84
+ s = s.slice(2);
85
+ s = decodeURIComponent(s);
86
+ state = urlSafeParse(s);
87
+ }
88
+ else {
89
+ throw new Error('URL hash is expected to be of the form \'#!{...}\' or \'#!+{...}\'.');
90
+ }
91
+ return state;
92
+ }
93
+ export function getNeuroglancerViewerState(key) {
94
+ const v = key ? viewersKeyed[key] : viewerNoKey;
95
+ return v ? v.state.toJSON() : {};
96
+ }
97
+ export function getNeuroglancerColor(idStr, key) {
98
+ try {
99
+ const id = Uint64.parseString(idStr);
100
+ const v = key ? viewersKeyed[key] : viewerNoKey;
101
+ if (v) {
102
+ // eslint-disable-next-line no-restricted-syntax
103
+ for (const layer of v.layerManager.managedLayers) {
104
+ if (layer.layer instanceof SegmentationUserLayer) {
105
+ const { displayState } = layer.layer;
106
+ const colorVec = getObjectColor(displayState, id);
107
+ // To get the true color, undo how getObjectColor() indicates hovering.
108
+ if (displayState.segmentSelectionState.isSelected(id)) {
109
+ for (let i = 0; i < 3; i += 1) {
110
+ colorVec[i] = (colorVec[i] - 0.5) / 0.5;
111
+ }
112
+ }
113
+ const colorStr = serializeColor(colorVec);
114
+ return colorStr;
115
+ }
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // suppress eslint no-empty
121
+ }
122
+ return '';
123
+ }
124
+ export function closeSelectionTab(key) {
125
+ const v = key ? viewersKeyed[key] : viewerNoKey;
126
+ if (v && v.closeSelectionTab) {
127
+ v.closeSelectionTab();
128
+ }
129
+ }
130
+ export function getLayerManager(key) {
131
+ const v = key ? viewersKeyed[key] : viewerNoKey;
132
+ if (v) {
133
+ return v.layerManager;
134
+ }
135
+ return undefined;
136
+ }
137
+ export function getManagedLayer(key, name) {
138
+ const layerManager = getLayerManager(key);
139
+ if (layerManager) {
140
+ return layerManager.managedLayers.filter(layer => layer.name === name)[0];
141
+ }
142
+ return undefined;
143
+ }
144
+ export function getAnnotationLayer(key, name) {
145
+ const layer = getManagedLayer(key, name);
146
+ if (layer && layer.layer instanceof AnnotationUserLayer) {
147
+ return layer.layer;
148
+ }
149
+ return undefined;
150
+ }
151
+ export function getAnnotationSource(key, name) {
152
+ const layer = getAnnotationLayer(key, name);
153
+ /* eslint-disable-next-line no-underscore-dangle */
154
+ if (layer && layer.dataSources && layer.dataSources[0].loadState_) {
155
+ /* eslint-disable-next-line no-underscore-dangle */
156
+ const { dataSource } = layer.dataSources[0].loadState_;
157
+ if (dataSource) {
158
+ return dataSource.subsources[0].subsource.annotation;
159
+ }
160
+ }
161
+ return undefined;
162
+ }
163
+ export function addLayerSignalRemover(key, name, remover) {
164
+ const layerManager = getLayerManager(key);
165
+ if (layerManager && name && remover) {
166
+ if (!layerManager.customSignalHandlerRemovers) {
167
+ layerManager.customSignalHandlerRemovers = {};
168
+ }
169
+ if (!layerManager.customSignalHandlerRemovers[name]) {
170
+ layerManager.customSignalHandlerRemovers[name] = [];
171
+ }
172
+ layerManager.customSignalHandlerRemovers[name].push(remover);
173
+ }
174
+ }
175
+ export function unsubscribeLayersChangedSignals(layerManager, signalKey) {
176
+ if (layerManager) {
177
+ if (layerManager.customSignalHandlerRemovers) {
178
+ if (layerManager.customSignalHandlerRemovers[signalKey]) {
179
+ layerManager.customSignalHandlerRemovers[signalKey].forEach((remover) => {
180
+ remover();
181
+ });
182
+ // eslint-disable-next-line no-param-reassign
183
+ delete layerManager.customSignalHandlerRemovers[signalKey];
184
+ }
185
+ }
186
+ }
187
+ }
188
+ export function configureLayersChangedSignals(key, layerConfig) {
189
+ const layerManager = getLayerManager(key);
190
+ if (layerManager) {
191
+ const { layerName } = layerConfig;
192
+ unsubscribeLayersChangedSignals(layerManager, layerName);
193
+ if (layerConfig.process) {
194
+ const recordRemover = remover => addLayerSignalRemover(undefined, layerName, remover);
195
+ recordRemover(layerManager.layersChanged.add(() => {
196
+ const layer = getManagedLayer(undefined, layerName);
197
+ if (layer) {
198
+ layerConfig.process(layer);
199
+ }
200
+ }));
201
+ const layer = getManagedLayer(undefined, layerName);
202
+ if (layer) {
203
+ layerConfig.process(layer);
204
+ }
205
+ return () => {
206
+ if (layerConfig.cancel) {
207
+ layerConfig.cancel();
208
+ }
209
+ unsubscribeLayersChangedSignals(layerManager, layerName);
210
+ };
211
+ }
212
+ }
213
+ return layerConfig.cancel;
214
+ }
215
+ function configureAnnotationSource(source, props, recordRemover) {
216
+ if (source && !source.signalReady) {
217
+ if (props.onAnnotationAdded) {
218
+ recordRemover(source.childAdded.add((annotation) => {
219
+ props.onAnnotationAdded(annotation);
220
+ }));
221
+ }
222
+ if (props.onAnnotationDeleted) {
223
+ recordRemover(source.childDeleted.add((id) => {
224
+ props.onAnnotationDeleted(id);
225
+ }));
226
+ }
227
+ if (props.onAnnotationUpdated) {
228
+ recordRemover(source.childUpdated.add((annotation) => {
229
+ props.onAnnotationUpdated(annotation);
230
+ }));
231
+ }
232
+ if (props.onAnnotationChanged && source.referencesChanged) {
233
+ recordRemover(source.referencesChanged.add(props.onAnnotationChanged));
234
+ }
235
+ // eslint-disable-next-line no-param-reassign
236
+ source.signalReady = true;
237
+ recordRemover(() => {
238
+ // eslint-disable-next-line no-param-reassign
239
+ source.signalReady = false;
240
+ });
241
+ }
242
+ }
243
+ function getLoadedDataSource(layer) {
244
+ /* eslint-disable-next-line no-underscore-dangle */
245
+ if (layer.dataSources
246
+ && layer.dataSources.length > 0
247
+ /* eslint-disable-next-line no-underscore-dangle */
248
+ && layer.dataSources[0].loadState_
249
+ /* eslint-disable-next-line no-underscore-dangle */
250
+ && layer.dataSources[0].loadState_.dataSource) {
251
+ /* eslint-disable-next-line no-underscore-dangle */
252
+ return layer.dataSources[0].loadState_.dataSource;
253
+ }
254
+ /* eslint-disable-consistent-return */
255
+ }
256
+ function getAnnotationSourceFromLayer(layer) {
257
+ const dataSource = getLoadedDataSource(layer);
258
+ if (dataSource) {
259
+ return dataSource.subsources[0].subsource.annotation;
260
+ }
261
+ }
262
+ function configureAnnotationSourceChange(annotationLayer, props, recordRemover) {
263
+ const configure = () => {
264
+ const source = getAnnotationSourceFromLayer(annotationLayer);
265
+ if (source) {
266
+ configureAnnotationSource(source, props, recordRemover);
267
+ }
268
+ };
269
+ const sourceChanged = annotationLayer.dataSourcesChanged;
270
+ if (sourceChanged && !sourceChanged.signalReady) {
271
+ recordRemover(sourceChanged.add(configure));
272
+ sourceChanged.signalReady = true;
273
+ recordRemover(() => {
274
+ sourceChanged.signalReady = false;
275
+ });
276
+ configure();
277
+ }
278
+ }
279
+ export function configureAnnotationLayer(layer, props, recordRemover) {
280
+ if (layer) {
281
+ // eslint-disable-next-line no-param-reassign
282
+ layer.expectingExternalTable = true;
283
+ if (layer.selectedAnnotation
284
+ && !layer.selectedAnnotation.changed.signalReady) {
285
+ if (props.onAnnotationSelectionChanged) {
286
+ recordRemover(layer.selectedAnnotation.changed.add(() => {
287
+ props.onAnnotationSelectionChanged(layer.selectedAnnotation.value);
288
+ }));
289
+ recordRemover(() => {
290
+ // eslint-disable-next-line no-param-reassign
291
+ layer.selectedAnnotation.changed.signalReady = false;
292
+ });
293
+ // eslint-disable-next-line no-param-reassign
294
+ layer.selectedAnnotation.changed.signalReady = true;
295
+ }
296
+ }
297
+ configureAnnotationSourceChange(layer, props, recordRemover);
298
+ }
299
+ }
300
+ export function configureAnnotationLayerChanged(layer, props, recordRemover) {
301
+ if (!layer.layerChanged.signalReady) {
302
+ const remover = layer.layerChanged.add(() => {
303
+ configureAnnotationLayer(layer.layer, props, recordRemover);
304
+ });
305
+ // eslint-disable-next-line no-param-reassign
306
+ layer.layerChanged.signalReady = true;
307
+ recordRemover(remover);
308
+ recordRemover(() => {
309
+ // eslint-disable-next-line no-param-reassign
310
+ layer.layerChanged.signalReady = false;
311
+ });
312
+ configureAnnotationLayer(layer.layer, props, recordRemover);
313
+ }
314
+ }
315
+ export function getAnnotationSelectionHost(key) {
316
+ const viewer = key ? viewersKeyed[key] : viewerNoKey;
317
+ if (viewer) {
318
+ if (viewer.selectionDetailsState) {
319
+ return 'viewer';
320
+ }
321
+ return 'layer';
322
+ }
323
+ return null;
324
+ }
325
+ export function getSelectedAnnotationId(key, layerName) {
326
+ const viewer = key ? viewersKeyed[key] : viewerNoKey;
327
+ if (viewer) {
328
+ if (viewer.selectionDetailsState) {
329
+ // New neurolgancer version
330
+ // v.selectionDetailsState.value.layers[0].layer.managedLayer.name
331
+ if (viewer.selectionDetailsState.value) {
332
+ const { layers } = viewer.selectionDetailsState.value;
333
+ if (layers) {
334
+ const layer = layers.find(_layer => _layer.layer.managedLayer.name === layerName);
335
+ if (layer && layer.state) {
336
+ return layer.state.annotationId;
337
+ }
338
+ }
339
+ }
340
+ }
341
+ else {
342
+ const layer = getAnnotationLayer(undefined, layerName);
343
+ if (layer && layer.selectedAnnotation && layer.selectedAnnotation.value) {
344
+ return layer.selectedAnnotation.value.id;
345
+ }
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+ /** @extends {React.Component<NgProps>} */
351
+ export default class Neuroglancer extends React.Component {
352
+ static defaultProps = {
353
+ perspectiveZoom: 20,
354
+ eventBindingsToUpdate: null,
355
+ brainMapsClientId: 'NOT_A_VALID_ID',
356
+ viewerState: null,
357
+ onSelectedChanged: null,
358
+ onVisibleChanged: null,
359
+ onSelectionDetailsStateChanged: null,
360
+ onViewerStateChanged: null,
361
+ key: null,
362
+ callbacks: [],
363
+ ngServer: 'https://neuroglancer-demo.appspot.com/',
364
+ };
365
+ constructor(props) {
366
+ super(props);
367
+ this.ngContainer = React.createRef();
368
+ this.viewer = null;
369
+ /* ** Vitessce Integration update start ** */
370
+ this.muteViewerChanged = false;
371
+ this.prevVisibleIds = new Set();
372
+ this.prevColorMap = null;
373
+ this.disposers = [];
374
+ this.prevColorOverrides = new Set();
375
+ this.overrideColorsById = Object.create(null);
376
+ this.allKnownIds = new Set();
377
+ }
378
+ minimalPoseSnapshot = () => {
379
+ const v = this.viewer;
380
+ const projScale = v.projectionScale?.value;
381
+ const projQuat = v.projectionOrientation?.orientation;
382
+ return {
383
+ position: Array.from(v.position.value || []),
384
+ projectionScale: projScale,
385
+ projectionOrientation: Array.from(projQuat || []),
386
+ };
387
+ };
388
+ // Coalesce many NG changes → one upstream update per frame.
389
+ scheduleEmit = () => {
390
+ let raf = null;
391
+ return () => {
392
+ if (this.muteViewerChanged)
393
+ return; // muted when we push changes
394
+ if (raf !== null)
395
+ return;
396
+ raf = requestAnimationFrame(() => {
397
+ raf = null;
398
+ // console.log('Minimal', this.minimalPoseSnapshot())
399
+ this.props.onViewerStateChanged?.(this.minimalPoseSnapshot());
400
+ });
401
+ };
402
+ };
403
+ // Guard to mute outgoing emits we are programmatically making changes
404
+ withoutEmitting = (fn) => {
405
+ this.muteViewerChanged = true;
406
+ try {
407
+ fn();
408
+ }
409
+ finally {
410
+ requestAnimationFrame(() => { this.muteViewerChanged = false; });
411
+ }
412
+ };
413
+ // Only consider actual changes in camera settings, i.e., position/rotation/zoom
414
+ didLayersChange = (prevVS, nextVS) => {
415
+ const stripColors = layers => (layers || []).map((l) => {
416
+ if (!l)
417
+ return l;
418
+ const { segmentColors, ...rest } = l;
419
+ return rest;
420
+ });
421
+ const prevLayers = stripColors(prevVS?.layers);
422
+ const nextLayers = stripColors(nextVS?.layers);
423
+ return JSON.stringify(prevLayers) !== JSON.stringify(nextLayers);
424
+ };
425
+ /* To add colors to the segments, turning unselected to grey */
426
+ applyColorsAndVisibility = (cellColorMapping) => {
427
+ if (!this.viewer)
428
+ return;
429
+ // Track all ids we've ever seen so we can grey the ones
430
+ // that drop out of the current selection.
431
+ const selected = { ...(cellColorMapping || {}) }; // clone, don't mutate props
432
+ for (const id of Object.keys(selected))
433
+ this.allKnownIds.add(id);
434
+ // If empty on first call, seed from initial segmentColors (if present)
435
+ if (this.allKnownIds.size === 0) {
436
+ const init = this.props.viewerState?.layers?.[0]?.segmentColors || {};
437
+ for (const id of Object.keys(init))
438
+ this.allKnownIds.add(id);
439
+ }
440
+ // Build a full color table: selected keep their hex, others grey
441
+ const fullSegmentColors = {};
442
+ for (const id of this.allKnownIds) {
443
+ fullSegmentColors[id] = selected[id] || GREY_HEX;
444
+ }
445
+ // Patch layers with the new segmentColors (pose untouched)
446
+ const baseLayers = (this.props.viewerState?.layers)
447
+ ?? (this.viewer.state.toJSON().layers || []);
448
+ const newLayers = baseLayers.map((layer, idx) => {
449
+ // if only one layer, take that or check layer.type === 'segmentation'.
450
+ if (idx === 0 || layer?.type === 'segmentation') {
451
+ return { ...layer, segmentColors: fullSegmentColors };
452
+ }
453
+ return layer;
454
+ });
455
+ this.withoutEmitting(() => {
456
+ this.viewer.state.restoreState({ layers: newLayers });
457
+ });
458
+ /* ** Vitessce integration update end ** */
459
+ };
460
+ componentDidMount() {
461
+ const { viewerState, brainMapsClientId, eventBindingsToUpdate, callbacks,
462
+ // ngServer,
463
+ key, bundleRoot, } = this.props;
464
+ this.viewer = setupDefaultViewer({
465
+ brainMapsClientId,
466
+ target: this.ngContainer.current,
467
+ bundleRoot,
468
+ });
469
+ this.setCallbacks(callbacks);
470
+ if (eventBindingsToUpdate) {
471
+ this.updateEventBindings(eventBindingsToUpdate);
472
+ }
473
+ this.viewer.expectingExternalUI = true;
474
+ // if (ngServer) {
475
+ // this.viewer.makeUrlFromState = (state) => {
476
+ // const newState = { ...state };
477
+ // if (state.layers) {
478
+ // // Do not include clio annotation layers
479
+ // newState.layers = state.layers.filter((layer) => {
480
+ // if (layer.source) {
481
+ // const sourceUrl = layer.source.url || layer.source;
482
+ // if (typeof sourceUrl === 'string') {
483
+ // return !sourceUrl.startsWith('clio://');
484
+ // }
485
+ // }
486
+ // return true;
487
+ // });
488
+ // }
489
+ // return `${ngServer}/#!${encodeFragment(JSON.stringify(newState))}`;
490
+ // };
491
+ // }
492
+ if (this.viewer.selectionDetailsState) {
493
+ this.viewer.selectionDetailsState.changed.add(this.selectionDetailsStateChanged);
494
+ }
495
+ this.viewer.layerManager.layersChanged.add(this.layersChanged);
496
+ /* ** Vitessce Integration update start ** */
497
+ const emit = this.scheduleEmit();
498
+ // Disposers to unsubscribe handles for NG signals to prevent leaks/duplicates.
499
+ this.disposers.push(this.viewer.projectionScale.changed.add(emit));
500
+ this.disposers.push(this.viewer.projectionOrientation.changed.add(emit));
501
+ this.disposers.push(this.viewer.position.changed.add(emit));
502
+ // Initial restore ONLY if provided
503
+ if (viewerState) {
504
+ // restore state only when all the changes are added -
505
+ // avoids calling .changed() for each change and leads to smooth updates
506
+ this.withoutEmitting(() => {
507
+ this.viewer.state.restoreState(viewerState);
508
+ });
509
+ }
510
+ /* ** Vitessce Integration update end ** */
511
+ // if (viewerState) {
512
+ // const newViewerState = viewerState;
513
+ // if (newViewerState.projectionScale === null) {
514
+ // delete newViewerState.projectionScale;
515
+ // }
516
+ // if (newViewerState.crossSectionScale === null) {
517
+ // delete newViewerState.crossSectionScale;
518
+ // }
519
+ // if (newViewerState.projectionOrientation === null) {
520
+ // delete newViewerState.projectionOrientation;
521
+ // }
522
+ // if (newViewerState.crossSectionOrientation === null) {
523
+ // delete newViewerState.crossSectionOrientation;
524
+ // }
525
+ // this.viewer.state.restoreState(newViewerState);
526
+ // } else {
527
+ // this.viewer.state.restoreState({
528
+ // layers: {
529
+ // grayscale: {
530
+ // type: "image",
531
+ // source:
532
+ // "dvid://https://flyem.dvid.io/ab6e610d4fe140aba0e030645a1d7229/grayscalejpeg"
533
+ // },
534
+ // segmentation: {
535
+ // type: "segmentation",
536
+ // source:
537
+ // "dvid://https://flyem.dvid.io/d925633ed0974da78e2bb5cf38d01f4d/segmentation"
538
+ // }
539
+ // },
540
+ // perspectiveZoom,
541
+ // navigation: {
542
+ // zoomFactor: 8
543
+ // }
544
+ // });
545
+ // }
546
+ // Make the Neuroglancer viewer accessible from getNeuroglancerViewerState().
547
+ // That function can be used to synchronize an external Redux store with any
548
+ // state changes made internally by the viewer.
549
+ if (key) {
550
+ viewersKeyed[key] = this.viewer;
551
+ }
552
+ else {
553
+ viewerNoKey = this.viewer;
554
+ }
555
+ // TODO: This is purely for debugging and we need to remove it.
556
+ // window.viewer = this.viewer;
557
+ }
558
+ componentDidUpdate(prevProps, prevState) {
559
+ const { viewerState, cellColorMapping } = this.props;
560
+ // The restoreState() call clears the 'selected' (hovered on) segment, which is needed
561
+ // by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
562
+ // from having to move the mouse before clicking, save the selected segment and restore
563
+ // it after restoreState().
564
+ const selectedSegments = {};
565
+ // eslint-disable-next-line no-restricted-syntax
566
+ for (const layer of this.viewer.layerManager.managedLayers) {
567
+ if (layer.layer instanceof SegmentationUserLayer) {
568
+ const { segmentSelectionState } = layer.layer.displayState;
569
+ selectedSegments[layer.name] = segmentSelectionState.selectedSegment;
570
+ }
571
+ }
572
+ // if (viewerState) {
573
+ // let newViewerState = { ...viewerState };
574
+ // let restoreStates = [
575
+ // () => {
576
+ // this.viewer.state.restoreState(newViewerState);
577
+ // },
578
+ // ];
579
+ // if (viewerState.projectionScale === null) {
580
+ // delete newViewerState.projectionScale;
581
+ // restoreStates.push(() => {
582
+ // this.viewer.projectionScale.reset();
583
+ // });
584
+ // }
585
+ // if (viewerState.crossSectionScale === null) {
586
+ // delete newViewerState.crossSectionScale;
587
+ // }
588
+ // restoreStates.forEach((restore) => restore());
589
+ // }
590
+ // eslint-disable-next-line no-restricted-syntax
591
+ for (const layer of this.viewer.layerManager.managedLayers) {
592
+ if (layer.layer instanceof SegmentationUserLayer) {
593
+ const { segmentSelectionState } = layer.layer.displayState;
594
+ segmentSelectionState.set(selectedSegments[layer.name]);
595
+ }
596
+ }
597
+ // For some reason setting position to an empty array doesn't reset
598
+ // the position in the viewer. This should handle those cases by looking
599
+ // for the empty position array and calling the position reset function if
600
+ // found.
601
+ // if ('position' in viewerState) {
602
+ // if (Array.isArray(viewerState.position)) {
603
+ // if (viewerState.position.length === 0) {
604
+ // this.viewer.position.reset();
605
+ // }
606
+ // }
607
+ // }
608
+ /* ** Vitessce Integration update start ** */
609
+ if (!viewerState)
610
+ return;
611
+ // updates NG's viewerstate by calling `restoreState() for segment and position changes separately
612
+ const prevVS = prevProps.viewerState;
613
+ const camState = diffCameraState(prevVS, viewerState);
614
+ // Restore pose ONLY if it actually changed
615
+ if (camState.changed) {
616
+ const patch = {};
617
+ if (camState.scale) {
618
+ patch.projectionScale = viewerState.projectionScale;
619
+ // Couple position with zoom even if it didn’t cross the hard epsilon
620
+ if (Array.isArray(viewerState.position))
621
+ patch.position = viewerState.position;
622
+ }
623
+ else if (camState.pos) {
624
+ patch.position = viewerState.position;
625
+ }
626
+ if (camState.rot)
627
+ patch.projectionOrientation = viewerState.projectionOrientation;
628
+ // Restore the state with updated camera setting/position changes
629
+ this.withoutEmitting(() => this.viewer.state.restoreState(patch));
630
+ }
631
+ // If layers changed (segment list / sources etc.): restore ONLY layers, then colors
632
+ if (this.didLayersChange(prevVS, viewerState)) {
633
+ this.withoutEmitting(() => {
634
+ const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
635
+ this.viewer.state.restoreState({ layers });
636
+ if (cellColorMapping && Object.keys(cellColorMapping).length) {
637
+ this.applyColorsAndVisibility(cellColorMapping);
638
+ }
639
+ });
640
+ }
641
+ // If colors changed (but layers didn’t): re-apply colors
642
+ // this was to avid NG randomly assigning colors to the segments by resetting them
643
+ const prevSize = prevProps.cellColorMapping
644
+ ? Object.keys(prevProps.cellColorMapping).length : 0;
645
+ const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
646
+ const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
647
+ if (!this.didLayersChange(prevVS, viewerState)
648
+ && (mappingRefChanged || prevSize !== currSize)) {
649
+ this.withoutEmitting(() => {
650
+ this.applyColorsAndVisibility(cellColorMapping);
651
+ });
652
+ }
653
+ // Treat "real" layer source/type changes differently from segment list changes.
654
+ // We only restore layers (not pose) when sources change OR on the first time segments appear.
655
+ const stripSegFields = layers => (layers || []).map((l) => {
656
+ if (!l)
657
+ return l;
658
+ const { segments, segmentColors, ...rest } = l;
659
+ return rest; // ignore segments + segmentColors for comparison
660
+ });
661
+ const prevLayers = prevProps.viewerState?.layers;
662
+ const nextLayers = viewerState?.layers;
663
+ const prevCore = JSON.stringify(stripSegFields(prevLayers));
664
+ const nextCore = JSON.stringify(stripSegFields(nextLayers));
665
+ const sourcesChanged = prevCore !== nextCore; // real structural change?
666
+ const prevSegCount = (prevLayers && prevLayers[0] && Array.isArray(prevLayers[0].segments))
667
+ ? prevLayers[0].segments.length : 0;
668
+ const nextSegCount = (nextLayers && nextLayers[0] && Array.isArray(nextLayers[0].segments))
669
+ ? nextLayers[0].segments.length : 0;
670
+ // first-time seeding – from 0 segments → N segments
671
+ const initialSegmentsAdded = prevSegCount === 0 && nextSegCount > 0;
672
+ if (sourcesChanged || initialSegmentsAdded) {
673
+ this.withoutEmitting(() => {
674
+ // restore only the layers to avoid clobbering pose/rotation/zoom.
675
+ this.viewer.state.restoreState({ layers: nextLayers });
676
+ });
677
+ }
678
+ /* ** Vitessce Integration update end ** */
679
+ }
680
+ componentWillUnmount() {
681
+ /* eslint-disable no-empty */
682
+ this.disposers.forEach((off) => { try {
683
+ off();
684
+ }
685
+ catch { } });
686
+ this.disposers = [];
687
+ const { key } = this.props;
688
+ if (key) {
689
+ delete viewersKeyed[key];
690
+ }
691
+ else {
692
+ viewerNoKey = undefined;
693
+ }
694
+ }
695
+ /* setCallbacks allows us to set a callback on a neuroglancer event
696
+ * each callback created should be in the format:
697
+ * [
698
+ * {
699
+ * name: 'unique-name',
700
+ * event: 'the neuroglancer event to target, eg: click0, keyt',
701
+ * function: (slice) => { slice.whatever }
702
+ * },
703
+ * {...}
704
+ * ]
705
+ *
706
+ */
707
+ setCallbacks(callbacks) {
708
+ callbacks.forEach((callback) => {
709
+ this.viewer.bindCallback(callback.name, callback.function);
710
+ this.viewer.inputEventBindings.sliceView.set(callback.event, callback.name);
711
+ });
712
+ }
713
+ updateEventBindings = (eventBindingsToUpdate) => {
714
+ const root = this.viewer.inputEventBindings;
715
+ const traverse = (current) => {
716
+ const replace = (eaMap, event0, event1) => {
717
+ const action = eaMap.get(event0);
718
+ if (action) {
719
+ eaMap.delete(event0);
720
+ if (event1) {
721
+ eaMap.set(event1, action);
722
+ }
723
+ }
724
+ };
725
+ const eventActionMap = current.bindings;
726
+ eventBindingsToUpdate.forEach((oldNewBinding) => {
727
+ const eventOldBase = Array.isArray(oldNewBinding)
728
+ ? oldNewBinding[0]
729
+ : oldNewBinding;
730
+ const eventOldA = `at:${eventOldBase}`;
731
+ const eventNewA = oldNewBinding[1]
732
+ ? `at:${oldNewBinding[1]}`
733
+ : undefined;
734
+ replace(eventActionMap, eventOldA, eventNewA);
735
+ const eventOldB = `bubble:${eventOldBase}`;
736
+ const eventNewB = oldNewBinding[1]
737
+ ? `bubble:${oldNewBinding[1]}`
738
+ : undefined;
739
+ replace(eventActionMap, eventOldB, eventNewB);
740
+ });
741
+ current.parents.forEach((parent) => {
742
+ traverse(parent);
743
+ });
744
+ };
745
+ traverse(root.global);
746
+ traverse(root.perspectiveView);
747
+ traverse(root.sliceView);
748
+ };
749
+ selectionDetailsStateChanged = () => {
750
+ if (this.viewer) {
751
+ const { onSelectionDetailsStateChanged } = this.props;
752
+ if (onSelectionDetailsStateChanged) {
753
+ onSelectionDetailsStateChanged();
754
+ }
755
+ }
756
+ };
757
+ layersChanged = () => {
758
+ if (this.handlerRemovers) {
759
+ // If change handlers have been added already, call the function to remove each one,
760
+ // so there won't be duplicates when new handlers are added below.
761
+ this.handlerRemovers.forEach(remover => remover());
762
+ }
763
+ if (this.viewer) {
764
+ const { onSelectedChanged, onVisibleChanged } = this.props;
765
+ if (onSelectedChanged || onVisibleChanged) {
766
+ this.handlerRemovers = [];
767
+ // eslint-disable-next-line no-restricted-syntax
768
+ for (const layer of this.viewer.layerManager.managedLayers) {
769
+ if (layer.layer instanceof SegmentationUserLayer) {
770
+ const { segmentSelectionState } = layer.layer.displayState;
771
+ const { visibleSegments } = layer.layer.displayState.segmentationGroupState.value;
772
+ if (segmentSelectionState && onSelectedChanged) {
773
+ // Bind the layer so it will be an argument to the handler when called.
774
+ const selectedChanged = this.selectedChanged.bind(undefined, layer);
775
+ const remover = segmentSelectionState.changed.add(selectedChanged);
776
+ this.handlerRemovers.push(remover);
777
+ layer.registerDisposer(remover);
778
+ }
779
+ if (visibleSegments && onVisibleChanged) {
780
+ const visibleChanged = this.visibleChanged.bind(undefined, layer);
781
+ const remover = visibleSegments.changed.add(visibleChanged);
782
+ this.handlerRemovers.push(remover);
783
+ layer.registerDisposer(remover);
784
+ }
785
+ }
786
+ }
787
+ }
788
+ }
789
+ };
790
+ /* ** Vitessce Integration update start ** */
791
+ selectedChanged = (layer) => {
792
+ if (!this.viewer)
793
+ return;
794
+ const { onSelectedChanged } = this.props;
795
+ if (onSelectedChanged) {
796
+ const { segmentSelectionState } = layer.layer.displayState;
797
+ if (!segmentSelectionState)
798
+ return;
799
+ const selected = segmentSelectionState.selectedSegment;
800
+ if (selected) {
801
+ onSelectedChanged(selected, layer);
802
+ }
803
+ }
804
+ };
805
+ visibleChanged = (layer) => {
806
+ if (this.viewer) {
807
+ const { onVisibleChanged } = this.props;
808
+ if (onVisibleChanged) {
809
+ const { visibleSegments } = layer.layer.displayState.segmentationGroupState.value;
810
+ if (visibleSegments) {
811
+ onVisibleChanged(visibleSegments, layer);
812
+ }
813
+ }
814
+ }
815
+ };
816
+ render() {
817
+ const { perspectiveZoom } = this.props;
818
+ return (_jsx("div", { className: "neuroglancer-container", ref: this.ngContainer, children: _jsxs("p", { children: ["Neuroglancer here with zoom ", perspectiveZoom] }) }));
819
+ }
820
+ }