diff --git a/client/src/components/LayerManager.vue b/client/src/components/LayerManager.vue index 8b88d363b..ec2e1076a 100644 --- a/client/src/components/LayerManager.vue +++ b/client/src/components/LayerManager.vue @@ -93,66 +93,25 @@ export default defineComponent({ const frameNumberRef = annotator.frame; const flickNumberRef = annotator.flick; - const rectAnnotationLayer = new RectangleLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); - const overlapLayer = new OverlapLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); - - const polyAnnotationLayer = new PolygonLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); - - const lineLayer = new LineLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); - const pointLayer = new PointLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); - const tailLayer = new TailLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }, trackStore); - - const textLayer = new TextLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - formatter: props.formatTextRow, - }); - - const attributeBoxLayer = new AttributeBoxLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); - - const attributeLayer = new AttributeLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - }); + // Track initialization state to prevent race conditions with GeoJS + const layersInitialized = ref(false); + const hoverOvered: Ref = ref([]); - const editAnnotationLayer = new EditAnnotationLayer({ - annotator, - stateStyling: trackStyleManager.stateStyles, - typeStyling: typeStylingRef, - type: 'rectangle', - }); + // Layer references - initialized after mount to ensure GeoJS is ready + let rectAnnotationLayer: RectangleLayer; + let overlapLayer: OverlapLayer; + let polyAnnotationLayer: PolygonLayer; + let lineLayer: LineLayer; + let pointLayer: PointLayer; + let tailLayer: TailLayer; + let textLayer: TextLayer; + let attributeBoxLayer: AttributeBoxLayer; + let attributeLayer: AttributeLayer; + let editAnnotationLayer: EditAnnotationLayer; + let uiLayer: UILayer; const updateAttributes = () => { + if (!layersInitialized.value) return; const newList = attributes.value.filter((item) => item.render).sort((a, b) => { if (a.render && b.render) { return (a.render.order - b.render.order); @@ -163,16 +122,89 @@ export default defineComponent({ attributeLayer.updateRenderAttributes(newList, user); attributeBoxLayer.updateRenderAttributes(newList); }; - updateAttributes(); - const uiLayer = new UILayer(annotator); - const hoverOvered: Ref = ref([]); - const toolTipWidgetProps = { - color: typeStylingRef.value.color, - dataList: hoverOvered, - selected: selectedTrackIdRef, - stateStyling: trackStyleManager.stateStyles, - }; - uiLayer.addDOMWidget('customToolTip', ToolTipWidget, toolTipWidgetProps, { x: 10, y: 10 }); + + /** + * Initialize all annotation layers. This is deferred to onMounted to ensure + * the GeoJS viewer is fully initialized before creating layers. + */ + function initializeLayers() { + rectAnnotationLayer = new RectangleLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + overlapLayer = new OverlapLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + + polyAnnotationLayer = new PolygonLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + + lineLayer = new LineLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + pointLayer = new PointLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + tailLayer = new TailLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }, trackStore!); + + textLayer = new TextLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + formatter: props.formatTextRow, + }); + + attributeBoxLayer = new AttributeBoxLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + + attributeLayer = new AttributeLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + }); + + editAnnotationLayer = new EditAnnotationLayer({ + annotator, + stateStyling: trackStyleManager.stateStyles, + typeStyling: typeStylingRef, + type: 'rectangle', + }); + + uiLayer = new UILayer(annotator); + const toolTipWidgetProps = { + color: typeStylingRef.value.color, + dataList: hoverOvered, + selected: selectedTrackIdRef, + stateStyling: trackStyleManager.stateStyles, + }; + uiLayer.addDOMWidget('customToolTip', ToolTipWidget, toolTipWidgetProps, { x: 10, y: 10 }); + + // Set up event listeners + setupEventListeners(); + + // Mark as initialized + layersInitialized.value = true; + + // Update attributes now that layers are ready + updateAttributes(); + } function updateLayers( frame: number, @@ -184,6 +216,10 @@ export default defineComponent({ selectedKey: string, colorBy: string, ) { + // Guard against calling before layers are initialized + if (!layersInitialized.value) { + return; + } const currentFrameIds: AnnotationId[] | undefined = trackStore?.intervalTree .search([frame, frame]) .map((str) => parseInt(str, 10)); @@ -357,22 +393,32 @@ export default defineComponent({ } /** - * TODO: for some reason, GeoJS requires us to initialize - * by calling the render function twice. This is a bug. - * https://github.com/Kitware/dive/issues/365 + * Watch for the GeoJS viewer to be ready before initializing layers. + * The annotator sets ready=true only after initializeViewer() completes, + * which guarantees geoViewerRef.value is properly initialized. + * This fixes a race condition where layers were created before GeoJS + * was fully initialized, causing annotations to not display on first load. + * See: https://github.com/Kitware/dive/issues/365 */ - [1, 2].forEach(() => { - updateLayers( - frameNumberRef.value, - editingModeRef.value, - selectedTrackIdRef.value, - multiSeletListRef.value, - enabledTracksRef.value, - visibleModesRef.value, - selectedKeyRef.value, - props.colorBy, - ); - }); + watch( + () => annotator.ready.value, + (ready) => { + if (ready && !layersInitialized.value) { + initializeLayers(); + updateLayers( + frameNumberRef.value, + editingModeRef.value, + selectedTrackIdRef.value, + multiSeletListRef.value, + enabledTracksRef.value, + visibleModesRef.value, + selectedKeyRef.value, + props.colorBy, + ); + } + }, + { immediate: true }, + ); /** Shallow watch */ watch( @@ -445,49 +491,6 @@ export default defineComponent({ } }; - //Sync of internal geoJS state with the application - editAnnotationLayer.bus.$on('editing-annotation-sync', (editing: boolean) => { - handler.trackSelect(selectedTrackIdRef.value, editing); - }); - rectAnnotationLayer.bus.$on('annotation-clicked', Clicked); - rectAnnotationLayer.bus.$on('annotation-right-clicked', Clicked); - rectAnnotationLayer.bus.$on('annotation-ctrl-clicked', Clicked); - polyAnnotationLayer.bus.$on('annotation-clicked', Clicked); - polyAnnotationLayer.bus.$on('annotation-right-clicked', Clicked); - polyAnnotationLayer.bus.$on('annotation-ctrl-clicked', Clicked); - editAnnotationLayer.bus.$on('update:geojson', ( - mode: 'in-progress' | 'editing', - geometryCompleteEvent: boolean, - data: GeoJSON.Feature, - type: string, - key = '', - cb: () => void = () => (undefined), - ) => { - if (type === 'rectangle') { - const bounds = geojsonToBound(data as GeoJSON.Feature); - cb(); - handler.updateRectBounds(frameNumberRef.value, flickNumberRef.value, bounds); - } else { - handler.updateGeoJSON(mode, frameNumberRef.value, flickNumberRef.value, data, key, cb); - } - // Jump into edit mode if we completed a new shape - if (geometryCompleteEvent) { - updateLayers( - frameNumberRef.value, - editingModeRef.value, - selectedTrackIdRef.value, - multiSeletListRef.value, - enabledTracksRef.value, - visibleModesRef.value, - selectedKeyRef.value, - props.colorBy, - ); - } - }); - editAnnotationLayer.bus.$on( - 'update:selectedIndex', - (index: number, _type: EditAnnotationTypes, key = '') => handler.selectFeatureHandle(index, key), - ); const annotationHoverTooltip = ( found: { styleType: [string, number]; @@ -518,8 +521,58 @@ export default defineComponent({ hoverOvered.value = hoveredVals.sort((a, b) => a.maxX - b.maxX); uiLayer.setToolTipWidget('customToolTip', (hoverOvered.value.length > 0)); }; - rectAnnotationLayer.bus.$on('annotation-hover', annotationHoverTooltip); - polyAnnotationLayer.bus.$on('annotation-hover', annotationHoverTooltip); + + /** + * Set up event listeners for annotation layers. + * Called after layers are initialized. + */ + function setupEventListeners() { + //Sync of internal geoJS state with the application + editAnnotationLayer.bus.$on('editing-annotation-sync', (editing: boolean) => { + handler.trackSelect(selectedTrackIdRef.value, editing); + }); + rectAnnotationLayer.bus.$on('annotation-clicked', Clicked); + rectAnnotationLayer.bus.$on('annotation-right-clicked', Clicked); + rectAnnotationLayer.bus.$on('annotation-ctrl-clicked', Clicked); + polyAnnotationLayer.bus.$on('annotation-clicked', Clicked); + polyAnnotationLayer.bus.$on('annotation-right-clicked', Clicked); + polyAnnotationLayer.bus.$on('annotation-ctrl-clicked', Clicked); + editAnnotationLayer.bus.$on('update:geojson', ( + mode: 'in-progress' | 'editing', + geometryCompleteEvent: boolean, + data: GeoJSON.Feature, + type: string, + key = '', + cb: () => void = () => (undefined), + ) => { + if (type === 'rectangle') { + const bounds = geojsonToBound(data as GeoJSON.Feature); + cb(); + handler.updateRectBounds(frameNumberRef.value, flickNumberRef.value, bounds); + } else { + handler.updateGeoJSON(mode, frameNumberRef.value, flickNumberRef.value, data, key, cb); + } + // Jump into edit mode if we completed a new shape + if (geometryCompleteEvent) { + updateLayers( + frameNumberRef.value, + editingModeRef.value, + selectedTrackIdRef.value, + multiSeletListRef.value, + enabledTracksRef.value, + visibleModesRef.value, + selectedKeyRef.value, + props.colorBy, + ); + } + }); + editAnnotationLayer.bus.$on( + 'update:selectedIndex', + (index: number, _type: EditAnnotationTypes, key = '') => handler.selectFeatureHandle(index, key), + ); + rectAnnotationLayer.bus.$on('annotation-hover', annotationHoverTooltip); + polyAnnotationLayer.bus.$on('annotation-hover', annotationHoverTooltip); + } }, }); diff --git a/client/src/components/annotators/mediaControllerType.ts b/client/src/components/annotators/mediaControllerType.ts index 15d11fce2..78aae3fb8 100644 --- a/client/src/components/annotators/mediaControllerType.ts +++ b/client/src/components/annotators/mediaControllerType.ts @@ -39,6 +39,8 @@ export interface MediaController extends AggregateMediaController { flick: Readonly>; // eslint-disable-next-line @typescript-eslint/no-explicit-any geoViewerRef: Readonly>; + /** True when the GeoJS viewer is fully initialized and ready to create layers */ + ready: Readonly>; /** @deprecated may be removed in a future release */ syncedFrame: Readonly>; diff --git a/client/src/components/annotators/useMediaController.ts b/client/src/components/annotators/useMediaController.ts index 210ae3291..1ce27b888 100644 --- a/client/src/components/annotators/useMediaController.ts +++ b/client/src/components/annotators/useMediaController.ts @@ -406,6 +406,7 @@ export function useMediaController() { volume: toRef(state[camera], 'volume'), maxFrame: toRef(state[camera], 'maxFrame'), speed: toRef(state[camera], 'speed'), + ready: toRef(state[camera], 'ready'), syncedFrame: toRef(state[camera], 'syncedFrame'), prevFrame, nextFrame,