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