From 54cfd89260ae796cc3ca472df9b13bc8230bf650 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Thu, 28 Sep 2023 18:30:42 -0400 Subject: [PATCH 1/3] feat(annotation): AnnotationLayerView now shows currently displayed annotations in MultiscaleAnnotationSource --- .../annotation/annotation_layer_state.ts | 9 +-- .../annotation/frontend_source.ts | 73 ++++++++++++++++++- src/neuroglancer/ui/annotations.ts | 35 ++++++--- 3 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/neuroglancer/annotation/annotation_layer_state.ts b/src/neuroglancer/annotation/annotation_layer_state.ts index 2eb87af552..9ca8b7b632 100644 --- a/src/neuroglancer/annotation/annotation_layer_state.ts +++ b/src/neuroglancer/annotation/annotation_layer_state.ts @@ -55,14 +55,7 @@ export class WatchableAnnotationRelationshipStates extends nestedContext.registerDisposer(segmentationGroupState.changed.add(this.changed.dispatch)); nestedContext.registerDisposer(registerNested((groupContext, groupState) => { const {visibleSegments} = groupState; - let wasEmpty = visibleSegments.size === 0; - groupContext.registerDisposer(visibleSegments.changed.add(() => { - const isEmpty = visibleSegments.size === 0; - if (isEmpty !== wasEmpty) { - wasEmpty = isEmpty; - this.changed.dispatch(); - } - })); + groupContext.registerDisposer(visibleSegments.changed.add(this.changed.dispatch)); }, segmentationGroupState)); }, segmentationState)); }); diff --git a/src/neuroglancer/annotation/frontend_source.ts b/src/neuroglancer/annotation/frontend_source.ts index d495a3c1bf..3e8ad9f371 100644 --- a/src/neuroglancer/annotation/frontend_source.ts +++ b/src/neuroglancer/annotation/frontend_source.ts @@ -18,7 +18,7 @@ import {Annotation, AnnotationId, AnnotationPropertySerializer, AnnotationProper import {ANNOTATION_COMMIT_UPDATE_RESULT_RPC_ID, ANNOTATION_COMMIT_UPDATE_RPC_ID, ANNOTATION_GEOMETRY_CHUNK_SOURCE_RPC_ID, ANNOTATION_METADATA_CHUNK_SOURCE_RPC_ID, ANNOTATION_REFERENCE_ADD_RPC_ID, ANNOTATION_REFERENCE_DELETE_RPC_ID, ANNOTATION_SUBSET_GEOMETRY_CHUNK_SOURCE_RPC_ID, AnnotationGeometryChunkSpecification} from 'neuroglancer/annotation/base'; import {getAnnotationTypeRenderHandler} from 'neuroglancer/annotation/type_handler'; import {Chunk, ChunkManager, ChunkSource} from 'neuroglancer/chunk_manager/frontend'; -import {getObjectKey} from 'neuroglancer/segmentation_display_state/base'; +import {forEachVisibleSegment, getObjectKey} from 'neuroglancer/segmentation_display_state/base'; import {SliceViewSourceOptions} from 'neuroglancer/sliceview/base'; import {MultiscaleSliceViewChunkSource, SliceViewChunk, SliceViewChunkSource, SliceViewChunkSourceOptions, SliceViewSingleResolutionSource} from 'neuroglancer/sliceview/frontend'; import {StatusMessage} from 'neuroglancer/status'; @@ -29,6 +29,8 @@ import {NullarySignal, Signal} from 'neuroglancer/util/signal'; import {Buffer} from 'neuroglancer/webgl/buffer'; import {GL} from 'neuroglancer/webgl/context'; import {registerRPC, registerSharedObjectOwner, RPC, SharedObject} from 'neuroglancer/worker_rpc'; +import {AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state'; +import {ChunkState} from 'neuroglancer/chunk_manager/base'; export interface AnnotationGeometryChunkSourceOptions extends SliceViewChunkSourceOptions { spec: AnnotationGeometryChunkSpecification; @@ -377,6 +379,37 @@ export function makeTemporaryChunk() { {data: new Uint8Array(0), numPickIds: 0, typeToOffset, typeToIds, typeToIdMaps}); } +export function deserializeAnnotations( + serializedAnnotations: SerializedAnnotations, + rank: number, properties: Readonly[]) { + const annotations: Annotation[] = []; + const annotationBuffer = serializedAnnotations.data; + let annotation: Annotation|undefined; + for (let [annotationType, annotationsOfType] of serializedAnnotations.typeToIdMaps.entries()) { + const handler = annotationTypeHandlers[annotationType as AnnotationType]; + const numGeometryBytes = handler.serializedBytes(rank); + const baseOffset = annotationBuffer.byteOffset; + const dataView = new DataView(annotationBuffer.buffer); + const isLittleEndian = Endianness.LITTLE === ENDIANNESS; + const annotationPropertySerializer = + new AnnotationPropertySerializer(rank, numGeometryBytes, properties); + const annotationCount = annotationsOfType.size; + for (const [annotationId, annotationIndex] of annotationsOfType) { + annotation = handler.deserialize( + dataView, + baseOffset + + annotationPropertySerializer.propertyGroupBytes[0] * + annotationIndex, + isLittleEndian, rank, annotationId); + annotationPropertySerializer.deserialize( + dataView, baseOffset, annotationIndex, annotationCount, isLittleEndian, + annotation.properties = new Array(properties.length)); + annotations.push(annotation); + } + } + return annotations; +} + export class MultiscaleAnnotationSource extends SharedObject implements MultiscaleSliceViewChunkSource, AnnotationSourceSignals { OPTIONS: {}; @@ -409,6 +442,44 @@ export class MultiscaleAnnotationSource extends SharedObject implements } } + activeAnnotations(state: AnnotationLayerState): Annotation[] { + const annotations: Annotation[] = []; + const {segmentFilteredSources, spatiallyIndexedSources, rank, properties, relationships} = this; + const {relationshipStates} = state.displayState; + let hasVisibleSegments = false; + for (let i = 0; i < relationships.length; i++) { + const relationship = relationships[i]; + const state = relationshipStates.get(relationship) + if (state) { + const {showMatches: {value: showMatches}, segmentationState: {value: segmentationState}} = state; + if (!showMatches || !segmentationState) continue; + const chunks = segmentFilteredSources[i].chunks; + forEachVisibleSegment(segmentationState.segmentationGroupState.value, objectId => { + hasVisibleSegments = true; + const key = getObjectKey(objectId); + const chunk = chunks.get(key); + if (chunk !== undefined && chunk.state === ChunkState.GPU_MEMORY) { + const {data} = chunk; + if (data === undefined) return; + const {serializedAnnotations} = data; + annotations.push(...deserializeAnnotations(serializedAnnotations, rank, properties)); + } + }); + } + } + if (!hasVisibleSegments) { + for (const source of spatiallyIndexedSources) { + for (const [_key, chunk] of source.chunks) { + const {data} = chunk; + if (data === undefined) continue; + const {serializedAnnotations} = data; + annotations.push(...deserializeAnnotations(serializedAnnotations, rank, properties)); + } + } + } + return annotations; + } + hasNonSerializedProperties() { return this.relationships.length > 0; } diff --git a/src/neuroglancer/ui/annotations.ts b/src/neuroglancer/ui/annotations.ts index 16a9476817..8e4fb7ed21 100644 --- a/src/neuroglancer/ui/annotations.ts +++ b/src/neuroglancer/ui/annotations.ts @@ -221,8 +221,16 @@ export class AnnotationLayerView extends Tab { (annotationId) => this.deleteAnnotationElement(annotationId, state))); } refCounted.registerDisposer(state.transform.changed.add(this.forceUpdateView)); + refCounted.registerDisposer( + state.displayState.relationshipStates.changed.add(this.forceUpdateView)); newAttachedAnnotationStates.set( state, {refCounted, annotations: [], idToIndex: new Map(), listOffset: 0}); + if (source instanceof MultiscaleAnnotationSource) { + refCounted.registerDisposer( + source.chunkManager.chunkQueueManager.visibleChunksChanged.add(() => { + this.forceUpdateView(); + })); + } } this.attachedAnnotationStates = newAttachedAnnotationStates; attachedAnnotationStates.clear(); @@ -431,7 +439,7 @@ export class AnnotationLayerView extends Tab { if (selectionState === undefined) return; const element = this.getRenderedAnnotationListElement( selectionState.annotationLayerState, selectionState.annotationId, - /*scrollIntoView=*/selectionState.pin); + /*scrollIntoView=*/ selectionState.pin); if (element !== undefined) { element.classList.add('neuroglancer-annotation-selected'); } @@ -538,7 +546,9 @@ export class AnnotationLayerView extends Tab { if (!state.source.readonly) isMutable = true; if (state.chunkTransform.value.error !== undefined) continue; const {source} = state; - const annotations = Array.from(source); + const annotations = source instanceof MultiscaleAnnotationSource ? + source.activeAnnotations(state) : + Array.from(source); info.annotations = annotations; const {idToIndex} = info; idToIndex.clear(); @@ -551,8 +561,12 @@ export class AnnotationLayerView extends Tab { } const oldLength = this.virtualListSource.length; this.updateListLength(); - this.virtualListSource.changed!.dispatch( - [{retainCount: 0, deleteCount: oldLength, insertCount: listElements.length}]); + // TODO, what problems does this change cause? + // this prevents the scroll list position from resetting when updateView is run + const insertCount = Math.max(0, listElements.length - oldLength); + const deleteCount = Math.max(0, oldLength - listElements.length); + const retainCount = Math.min(listElements.length, oldLength); + this.virtualListSource.changed!.dispatch([{retainCount, deleteCount, insertCount}]); this.mutableControls.style.display = isMutable ? 'contents' : 'none'; this.resetOnUpdate(); } @@ -786,7 +800,8 @@ function getSelectedAssociatedSegments(annotationLayer: AnnotationLayerState, ge if (segmentationState.segmentSelectionState.hasSelectedSegment) { segments[i] = [segmentationState.segmentSelectionState.selectedSegment.clone()]; if (getBase) { - segments[i] = [...segments[i], segmentationState.segmentSelectionState.baseSelectedSegment.clone()]; + segments[i] = + [...segments[i], segmentationState.segmentSelectionState.baseSelectedSegment.clone()]; } continue; } @@ -834,7 +849,7 @@ export class PlacePointTool extends PlaceAnnotationTool { type: AnnotationType.POINT, properties: annotationLayer.source.properties.map(x => x.default), }; - const reference = annotationLayer.source.add(annotation, /*commit=*/true); + const reference = annotationLayer.source.add(annotation, /*commit=*/ true); this.layer.selectAnnotation(annotationLayer, reference.id, true); reference.dispose(); } @@ -896,7 +911,7 @@ abstract class TwoStepAnnotationTool extends PlaceAnnotationTool { if (inProgressAnnotation.value === undefined) { const reference = annotationLayer.source.add( - this.getInitialAnnotation(mouseState, annotationLayer), /*commit=*/false); + this.getInitialAnnotation(mouseState, annotationLayer), /*commit=*/ false); this.layer.selectAnnotation(annotationLayer, reference.id, true); const mouseDisposer = mouseState.changed.add(updatePointB); const disposer = () => { @@ -1328,7 +1343,7 @@ export function UserLayerWithAnnotationsMixin x.sourceIndex === state.annotationSourceIndex && - x.subsubsourceId === state.annotationSubsubsourceId && + x.subsubsourceId === state.annotationSubsubsourceId && (state.annotationSubsource === undefined || x.subsourceId === state.annotationSubsource)); if (annotationLayer === undefined) return false; @@ -1368,7 +1383,7 @@ export function UserLayerWithAnnotationsMixin Date: Thu, 4 Jan 2024 10:55:59 -0500 Subject: [PATCH 2/3] improved annotation list performance by only generating annotations that the virtual list renders started to work on sorting the list for spatial index but the chunk calculation is incorrect --- .../annotation/frontend_source.ts | 114 +++++++++-- src/neuroglancer/ui/annotations.ts | 178 +++++++++++++----- 2 files changed, 230 insertions(+), 62 deletions(-) diff --git a/src/neuroglancer/annotation/frontend_source.ts b/src/neuroglancer/annotation/frontend_source.ts index 3e8ad9f371..e0182a185a 100644 --- a/src/neuroglancer/annotation/frontend_source.ts +++ b/src/neuroglancer/annotation/frontend_source.ts @@ -15,22 +15,25 @@ */ import {Annotation, AnnotationId, AnnotationPropertySerializer, AnnotationPropertySpec, AnnotationReference, AnnotationSourceSignals, AnnotationType, annotationTypeHandlers, annotationTypes, fixAnnotationAfterStructuredCloning, makeAnnotationId, makeAnnotationPropertySerializers, SerializedAnnotations} from 'neuroglancer/annotation'; +import {AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state'; import {ANNOTATION_COMMIT_UPDATE_RESULT_RPC_ID, ANNOTATION_COMMIT_UPDATE_RPC_ID, ANNOTATION_GEOMETRY_CHUNK_SOURCE_RPC_ID, ANNOTATION_METADATA_CHUNK_SOURCE_RPC_ID, ANNOTATION_REFERENCE_ADD_RPC_ID, ANNOTATION_REFERENCE_DELETE_RPC_ID, ANNOTATION_SUBSET_GEOMETRY_CHUNK_SOURCE_RPC_ID, AnnotationGeometryChunkSpecification} from 'neuroglancer/annotation/base'; import {getAnnotationTypeRenderHandler} from 'neuroglancer/annotation/type_handler'; +import {ChunkState} from 'neuroglancer/chunk_manager/base'; import {Chunk, ChunkManager, ChunkSource} from 'neuroglancer/chunk_manager/frontend'; +import {Position} from 'neuroglancer/navigation_state'; import {forEachVisibleSegment, getObjectKey} from 'neuroglancer/segmentation_display_state/base'; import {SliceViewSourceOptions} from 'neuroglancer/sliceview/base'; import {MultiscaleSliceViewChunkSource, SliceViewChunk, SliceViewChunkSource, SliceViewChunkSourceOptions, SliceViewSingleResolutionSource} from 'neuroglancer/sliceview/frontend'; import {StatusMessage} from 'neuroglancer/status'; +import {getCenterPosition} from 'neuroglancer/ui/annotations'; import {Borrowed, Owned} from 'neuroglancer/util/disposable'; import {ENDIANNESS, Endianness} from 'neuroglancer/util/endian'; +import {vec3} from 'neuroglancer/util/geom'; import * as matrix from 'neuroglancer/util/matrix'; import {NullarySignal, Signal} from 'neuroglancer/util/signal'; import {Buffer} from 'neuroglancer/webgl/buffer'; import {GL} from 'neuroglancer/webgl/context'; import {registerRPC, registerSharedObjectOwner, RPC, SharedObject} from 'neuroglancer/worker_rpc'; -import {AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state'; -import {ChunkState} from 'neuroglancer/chunk_manager/base'; export interface AnnotationGeometryChunkSourceOptions extends SliceViewChunkSourceOptions { spec: AnnotationGeometryChunkSpecification; @@ -380,8 +383,8 @@ export function makeTemporaryChunk() { } export function deserializeAnnotations( - serializedAnnotations: SerializedAnnotations, - rank: number, properties: Readonly[]) { + serializedAnnotations: SerializedAnnotations, rank: number, + properties: Readonly[]) { const annotations: Annotation[] = []; const annotationBuffer = serializedAnnotations.data; let annotation: Annotation|undefined; @@ -397,9 +400,7 @@ export function deserializeAnnotations( for (const [annotationId, annotationIndex] of annotationsOfType) { annotation = handler.deserialize( dataView, - baseOffset + - annotationPropertySerializer.propertyGroupBytes[0] * - annotationIndex, + baseOffset + annotationPropertySerializer.propertyGroupBytes[0] * annotationIndex, isLittleEndian, rank, annotationId); annotationPropertySerializer.deserialize( dataView, baseOffset, annotationIndex, annotationCount, isLittleEndian, @@ -442,16 +443,27 @@ export class MultiscaleAnnotationSource extends SharedObject implements } } - activeAnnotations(state: AnnotationLayerState): Annotation[] { - const annotations: Annotation[] = []; + activeAnnotations(state: AnnotationLayerState, sortByPosition: Position): [ + length: number, + indexToId: (index: number) => Annotation, + idToIndex: (id: string) => number | undefined, + ] { const {segmentFilteredSources, spatiallyIndexedSources, rank, properties, relationships} = this; const {relationshipStates} = state.displayState; let hasVisibleSegments = false; + + let currentLength = 0; + const listOffsets: number[] = []; // TODO, maybe change to index start + const serializedAnnotationsList: SerializedAnnotations[] = []; + const deserializedAnnotationsList: Annotation[][] = []; + const idToIndexMap = new Map(); + for (let i = 0; i < relationships.length; i++) { const relationship = relationships[i]; const state = relationshipStates.get(relationship) if (state) { - const {showMatches: {value: showMatches}, segmentationState: {value: segmentationState}} = state; + const {showMatches: {value: showMatches}, segmentationState: {value: segmentationState}} = + state; if (!showMatches || !segmentationState) continue; const chunks = segmentFilteredSources[i].chunks; forEachVisibleSegment(segmentationState.segmentationGroupState.value, objectId => { @@ -459,25 +471,90 @@ export class MultiscaleAnnotationSource extends SharedObject implements const key = getObjectKey(objectId); const chunk = chunks.get(key); if (chunk !== undefined && chunk.state === ChunkState.GPU_MEMORY) { - const {data} = chunk; - if (data === undefined) return; - const {serializedAnnotations} = data; - annotations.push(...deserializeAnnotations(serializedAnnotations, rank, properties)); + const {data} = chunk; + if (data === undefined) return; + const {serializedAnnotations} = data; + const length = serializedAnnotations.typeToIds.reduce( + (sum, idsOfType) => sum + idsOfType.length, 0); + listOffsets.push(currentLength); + currentLength += length; } }); } } if (!hasVisibleSegments) { for (const source of spatiallyIndexedSources) { - for (const [_key, chunk] of source.chunks) { + const {rank} = sortByPosition.coordinateSpace.value; + const sortChunk = new Uint32Array(rank); + const {chunkDataSize} = source.spec; + for (let i = 0; i < rank; i++) { + sortChunk[i] = Math.floor(sortByPosition.value[i] / chunkDataSize[i]); + } + // currently not working well because the chunk calculation is off so we end up using a low + // level chunk + const chunk = source.chunks.get(sortChunk.join(',')); + if (chunk) { const {data} = chunk; if (data === undefined) continue; const {serializedAnnotations} = data; - annotations.push(...deserializeAnnotations(serializedAnnotations, rank, properties)); + const length = + serializedAnnotations.typeToIds.reduce((sum, idsOfType) => sum + idsOfType.length, 0); + listOffsets.push(currentLength); + currentLength += length; + const tempCenter = new Float32Array(rank); + const annotations = deserializeAnnotations(serializedAnnotations, rank, properties); + annotations.sort((a, b) => { + getCenterPosition(tempCenter, a); + const distanceA = vec3.distance(tempCenter as vec3, sortByPosition.value as vec3); + getCenterPosition(tempCenter, b); + const distanceB = vec3.distance(tempCenter as vec3, sortByPosition.value as vec3); + return distanceA - distanceB; + }); + deserializedAnnotationsList[0] = annotations; + for (let i = 0; i < annotations.length; i++) { + idToIndexMap.set(annotations[0].id, i); + } + break; } } } - return annotations; + const indexToId = (index: number) => { + if (index < currentLength) { + for (const [idx, listOffset] of listOffsets.entries()) { + const nextOffset = listOffsets[idx + 1] || Number.MAX_VALUE; + if (index < nextOffset) { + if (!deserializedAnnotationsList[idx]) { + const serializedAnnotations = serializedAnnotationsList[idx]; + deserializedAnnotationsList[idx] = + deserializeAnnotations(serializedAnnotations, rank, properties); + } + return deserializedAnnotationsList[idx][index - listOffset]; + } + } + } + throw new Error('OUT OF BOUNDS'); + }; + const idToIndex = (id: string) => { + const index = idToIndexMap.get(id); + if (index) { + return index; + } + for (const [idx, listOffset] of listOffsets.entries()) { + const serializedAnnotations = serializedAnnotationsList[idx]; + if (!serializedAnnotations) continue; + const {typeToIdMaps} = serializedAnnotations; + for (const idMapForType of typeToIdMaps) { + const localIndex = idMapForType.get(id); + if (localIndex) { + return listOffset + localIndex; // TODO, is this handling SerializedAnnotations with + // multiple types correctly? + } + } + } + return undefined; + }; + + return [currentLength, indexToId, idToIndex]; } hasNonSerializedProperties() { @@ -550,7 +627,8 @@ export class MultiscaleAnnotationSource extends SharedObject implements } } else { if (newAnnotation === null) { - // Annotation has a local update already, so we need to delete it from the temporary chunk. + // Annotation has a local update already, so we need to delete it from the temporary + // chunk. deleteAnnotation( this.temporary.data!, annotation.type, annotation.id, this.annotationPropertySerializers); diff --git a/src/neuroglancer/ui/annotations.ts b/src/neuroglancer/ui/annotations.ts index 8e4fb7ed21..e259e2275f 100644 --- a/src/neuroglancer/ui/annotations.ts +++ b/src/neuroglancer/ui/annotations.ts @@ -20,6 +20,7 @@ import './annotations.css'; +import {debounce} from 'lodash'; import {Annotation, AnnotationId, AnnotationPropertySerializer, AnnotationReference, AnnotationSource, annotationToJson, AnnotationType, annotationTypeHandlers, AxisAlignedBoundingBox, Ellipsoid, formatNumericProperty, Line} from 'neuroglancer/annotation'; import {AnnotationDisplayState, AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state'; import {MultiscaleAnnotationSource} from 'neuroglancer/annotation/frontend_source'; @@ -118,7 +119,7 @@ export class MergedAnnotationStates extends RefCounted implements } } -function getCenterPosition(center: Float32Array, annotation: Annotation) { +export function getCenterPosition(center: Float32Array, annotation: Annotation) { switch (annotation.type) { case AnnotationType.AXIS_ALIGNED_BOUNDING_BOX: case AnnotationType.LINE: @@ -161,13 +162,42 @@ function visitTransformedAnnotationGeometry( }); } -interface AnnotationLayerViewAttachedState { - refCounted: RefCounted; +interface AnnotationSourceInfo { annotations: Annotation[]; idToIndex: Map; +} + +interface MultiscaleAnnotationSourceInfo { + idToIndexFunc: (id: string) => number | undefined; + length: number; +} + +function instanceOfAnnotationSourceInfo(object: AnnotationSourceInfo|MultiscaleAnnotationSourceInfo| + undefined): object is AnnotationSourceInfo { + return object !== undefined && 'annotations' in object; +} + +interface AnnotationLayerViewAttachedState { + refCounted: RefCounted; + source?: AnnotationSourceInfo|MultiscaleAnnotationSourceInfo; listOffset: number; } +interface annotationListElement { + state: AnnotationLayerState; + annotation: Annotation; +} +interface multiscaleAnnotationListElement { + state: AnnotationLayerState, length: number; + indexToAnnotation: (index: number) => Annotation; +} + +function instanceOfAnnotationListElement( + object: annotationListElement| + multiscaleAnnotationListElement): object is annotationListElement { + return 'annotation' in object; +} + export class AnnotationLayerView extends Tab { private previousSelectedState: {annotationId: string, annotationLayerState: AnnotationLayerState, pin: boolean}|undefined = @@ -181,7 +211,8 @@ export class AnnotationLayerView extends Tab { changed: new Signal<(splices: ArraySpliceOp[]) => void>(), }; private virtualList = new VirtualList({source: this.virtualListSource}); - private listElements: {state: AnnotationLayerState, annotation: Annotation}[] = []; + + private listElements: (annotationListElement|multiscaleAnnotationListElement)[] = []; private updated = false; private mutableControls = document.createElement('div'); private headerRow = document.createElement('div'); @@ -223,13 +254,13 @@ export class AnnotationLayerView extends Tab { refCounted.registerDisposer(state.transform.changed.add(this.forceUpdateView)); refCounted.registerDisposer( state.displayState.relationshipStates.changed.add(this.forceUpdateView)); - newAttachedAnnotationStates.set( - state, {refCounted, annotations: [], idToIndex: new Map(), listOffset: 0}); + newAttachedAnnotationStates.set(state, {refCounted, listOffset: 0}); if (source instanceof MultiscaleAnnotationSource) { refCounted.registerDisposer( - source.chunkManager.chunkQueueManager.visibleChunksChanged.add(() => { - this.forceUpdateView(); - })); + source.chunkManager.chunkQueueManager.visibleChunksChanged.add(debounce(() => { + console.log('calling force update view!'); + this.forceUpdateView() + }, 1000))); } } this.attachedAnnotationStates = newAttachedAnnotationStates; @@ -376,7 +407,10 @@ export class AnnotationLayerView extends Tab { |undefined { const attached = this.attachedAnnotationStates.get(state); if (attached == undefined) return undefined; - const index = attached.idToIndex.get(id); + const {source} = attached; + if (source == undefined) return undefined; + const index = instanceOfAnnotationSourceInfo(source) ? source.idToIndex.get(id) : + source.idToIndexFunc(id); if (index === undefined) return undefined; const listIndex = attached.listOffset + index; if (scrollIntoView) { @@ -468,8 +502,26 @@ export class AnnotationLayerView extends Tab { } private render(index: number) { - const {annotation, state} = this.listElements[index]; - return this.makeAnnotationListElement(annotation, state); + this.makeAnnotationListElement; + let currentIndex = 0; + for (const element of this.listElements) { + if (instanceOfAnnotationListElement(element)) { + // TODO, this is innefficient, do we want lists to have a combination of multiscale and + // regular? + const {state, annotation} = element; + if (index === currentIndex) { + return this.makeAnnotationListElement(annotation, state); + } + currentIndex++; + } else { + const {state, length, indexToAnnotation} = element; + if (index < currentIndex + length) { + return this.makeAnnotationListElement(indexToAnnotation(index - currentIndex), state); + } + currentIndex += element.length; + } + } + throw new Error('trying to render an annotation element outside the available list'); } private setColumnWidth(column: number, width: number) { @@ -546,26 +598,36 @@ export class AnnotationLayerView extends Tab { if (!state.source.readonly) isMutable = true; if (state.chunkTransform.value.error !== undefined) continue; const {source} = state; - const annotations = source instanceof MultiscaleAnnotationSource ? - source.activeAnnotations(state) : - Array.from(source); - info.annotations = annotations; - const {idToIndex} = info; - idToIndex.clear(); - for (let i = 0, length = annotations.length; i < length; ++i) { - idToIndex.set(annotations[i].id, i); - } - for (const annotation of annotations) { - listElements.push({state, annotation}); + if (source instanceof MultiscaleAnnotationSource) { + const {globalPosition} = this.layer.managedLayer.manager.root; + const [sourceLength, indexToAnnotation, idToIndex] = + source.activeAnnotations(state, globalPosition); + info.source = {idToIndexFunc: idToIndex, length: sourceLength}; + listElements.push({state, length: sourceLength, indexToAnnotation}); + } else { + const annotations = Array.from(source); + if (info.source && instanceOfAnnotationSourceInfo(info.source)) { + info.source.idToIndex.clear(); + info.source.annotations = annotations; + } else { + info.source = { + annotations: annotations, + idToIndex: new Map(), + }; + } + for (let i = 0, length = annotations.length; i < length; ++i) { + info.source.idToIndex.set(annotations[i].id, i); + } + for (const annotation of annotations) { + listElements.push({state, annotation}); + } } } const oldLength = this.virtualListSource.length; - this.updateListLength(); - // TODO, what problems does this change cause? - // this prevents the scroll list position from resetting when updateView is run - const insertCount = Math.max(0, listElements.length - oldLength); - const deleteCount = Math.max(0, oldLength - listElements.length); - const retainCount = Math.min(listElements.length, oldLength); + const newLength = this.updateListLength(); + const insertCount = Math.max(0, newLength - oldLength); + const deleteCount = Math.max(0, oldLength - newLength); + const retainCount = Math.min(newLength, oldLength); this.virtualListSource.changed!.dispatch([{retainCount, deleteCount, insertCount}]); this.mutableControls.style.display = isMutable ? 'contents' : 'none'; this.resetOnUpdate(); @@ -575,12 +637,23 @@ export class AnnotationLayerView extends Tab { let length = 0; for (const info of this.attachedAnnotationStates.values()) { info.listOffset = length; - length += info.annotations.length; + if (!info.source) continue; + if (instanceOfAnnotationSourceInfo(info.source)) { + length += info.source.annotations.length; + } else { + length += info.source.length; + } } this.virtualListSource.length = length; + return length; } private addAnnotationElement(annotation: Annotation, state: AnnotationLayerState) { + const {source} = state; + if (source instanceof MultiscaleAnnotationSource) { + return; // only AnnotationSource has info.annotations + } + if (!this.visible) { this.updated = false; return; @@ -590,12 +663,12 @@ export class AnnotationLayerView extends Tab { return; } const info = this.attachedAnnotationStates.get(state); - if (info !== undefined) { - const index = info.annotations.length; - info.annotations.push(annotation); - info.idToIndex.set(annotation.id, index); + if (info !== undefined && instanceOfAnnotationSourceInfo(info.source)) { + const index = info.source.annotations.length; + info.source.annotations.push(annotation); + info.source.idToIndex.set(annotation.id, index); const spliceStart = info.listOffset + index; - this.listElements.splice(spliceStart, 0, {state, annotation}); + this.listElements.splice(spliceStart, 0, {state, length: 0, annotation}); // TODO this.updateListLength(); this.virtualListSource.changed!.dispatch( [{retainCount: spliceStart, deleteCount: 0, insertCount: 1}]); @@ -604,6 +677,13 @@ export class AnnotationLayerView extends Tab { } private updateAnnotationElement(annotation: Annotation, state: AnnotationLayerState) { + const {source} = state; + if (source instanceof MultiscaleAnnotationSource) { + return; // only AnnotationSource has info.annotations + } + annotation; + state; + console.log('as well as this'); if (!this.visible) { this.updated = false; return; @@ -613,20 +693,31 @@ export class AnnotationLayerView extends Tab { return; } const info = this.attachedAnnotationStates.get(state); - if (info !== undefined) { - const index = info.idToIndex.get(annotation.id); + if (info !== undefined && instanceOfAnnotationSourceInfo(info.source)) { + const {idToIndex, annotations} = info.source; + const index = idToIndex.get(annotation.id); if (index !== undefined) { const updateStart = info.listOffset + index; - info.annotations[index] = annotation; - this.listElements[updateStart].annotation = annotation; - this.virtualListSource.changed!.dispatch( - [{retainCount: updateStart, deleteCount: 1, insertCount: 1}]); + annotations[index] = annotation; + const listElement = this.listElements[updateStart]; + if (instanceOfAnnotationListElement(listElement)) { + listElement.annotation = annotation; + this.virtualListSource.changed!.dispatch( + [{retainCount: updateStart, deleteCount: 1, insertCount: 1}]); + } } } this.resetOnUpdate(); } private deleteAnnotationElement(annotationId: string, state: AnnotationLayerState) { + const {source} = state; + if (source instanceof MultiscaleAnnotationSource) { + return; // only AnnotationSource has info.annotations + } + annotationId; + state; + console.log('and delete?'); if (!this.visible) { this.updated = false; return; @@ -636,12 +727,11 @@ export class AnnotationLayerView extends Tab { return; } const info = this.attachedAnnotationStates.get(state); - if (info !== undefined) { - const {idToIndex} = info; + if (info !== undefined && instanceOfAnnotationSourceInfo(info.source)) { + const {idToIndex, annotations} = info.source; const index = idToIndex.get(annotationId); if (index !== undefined) { const spliceStart = info.listOffset + index; - const {annotations} = info; annotations.splice(index, 1); idToIndex.delete(annotationId); for (let i = index, length = annotations.length; i < length; ++i) { From 1f3f73430c2432e266f3ba5fca5715bdefd9f6c7 Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Fri, 5 Jan 2024 13:30:36 -0500 Subject: [PATCH 3/3] fix - serializedAnnotationsList wasn't being constructed for segmentFilteredSources --- src/neuroglancer/annotation/frontend_source.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/neuroglancer/annotation/frontend_source.ts b/src/neuroglancer/annotation/frontend_source.ts index e0182a185a..aa3871603b 100644 --- a/src/neuroglancer/annotation/frontend_source.ts +++ b/src/neuroglancer/annotation/frontend_source.ts @@ -476,6 +476,7 @@ export class MultiscaleAnnotationSource extends SharedObject implements const {serializedAnnotations} = data; const length = serializedAnnotations.typeToIds.reduce( (sum, idsOfType) => sum + idsOfType.length, 0); + serializedAnnotationsList.push(serializedAnnotations); listOffsets.push(currentLength); currentLength += length; }