diff --git a/client/dive-common/components/ConfidenceFilter.vue b/client/dive-common/components/ConfidenceFilter.vue index 915d1d899..34596543b 100644 --- a/client/dive-common/components/ConfidenceFilter.vue +++ b/client/dive-common/components/ConfidenceFilter.vue @@ -25,9 +25,7 @@ export default defineComponent({ setup(props, { emit }) { function _updateConfidence(event: InputEvent) { if (event.target) { - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore - emit('update:confidence', Number.parseFloat(event.target.value)); + emit('update:confidence', Number.parseFloat((event.target as HTMLInputElement).value)); } } function _emitEnd() { diff --git a/client/dive-common/components/ControlsContainer.vue b/client/dive-common/components/ControlsContainer.vue index 0edab8f44..f3877d64d 100644 --- a/client/dive-common/components/ControlsContainer.vue +++ b/client/dive-common/components/ControlsContainer.vue @@ -4,15 +4,14 @@ import { } from '@vue/composition-api'; import type { DatasetType } from 'dive-common/apispec'; import FileNameTimeDisplay from 'vue-media-annotator/components/controls/FileNameTimeDisplay.vue'; -import { injectMediaController } from 'vue-media-annotator/components/annotators/useMediaController'; import { Controls, EventChart, + injectAggregateController, LineChart, Timeline, } from 'vue-media-annotator/components'; - export default defineComponent({ components: { Controls, @@ -35,11 +34,14 @@ export default defineComponent({ type: String as PropType, required: true, }, + collapsed: { + type: Boolean, + default: false, + }, }, - setup() { + setup(_, { emit }) { const currentView = ref('Detections'); - const collapsed = ref(false); const ticks = ref([0.25, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0]); @@ -49,16 +51,15 @@ export default defineComponent({ */ function toggleView(type: 'Detections' | 'Events') { currentView.value = type; - collapsed.value = false; + emit('update:collapsed', false); } const { maxFrame, frame, seek, volume, setVolume, setSpeed, speed, - } = injectMediaController(); + } = injectAggregateController().value; return { currentView, toggleView, - collapsed, maxFrame, frame, seek, @@ -73,7 +74,10 @@ export default defineComponent({ diff --git a/client/dive-common/use/useModeManager.ts b/client/dive-common/use/useModeManager.ts index 62277f84d..34a299b05 100644 --- a/client/dive-common/use/useModeManager.ts +++ b/client/dive-common/use/useModeManager.ts @@ -6,7 +6,7 @@ import Track, { TrackId } from 'vue-media-annotator/track'; import { getTrack } from 'vue-media-annotator/use/useTrackStore'; import { RectBounds, updateBounds } from 'vue-media-annotator/utils'; import { EditAnnotationTypes, VisibleAnnotationTypes } from 'vue-media-annotator/layers'; -import { MediaController } from 'vue-media-annotator/components/annotators/mediaControllerType'; +import { AggregateMediaController } from 'vue-media-annotator/components/annotators/mediaControllerType'; import Recipe from 'vue-media-annotator/recipe'; import { usePrompt } from 'dive-common/vue-utilities/prompt-service'; @@ -29,9 +29,11 @@ interface SetAnnotationStateArgs { */ export default function useModeManager({ selectedTrackId, + selectedCamera, editingTrack, trackMap, - mediaController, + camTrackMap, + aggregateController, recipes, selectTrack, selectNextTrack, @@ -39,13 +41,15 @@ export default function useModeManager({ removeTrack, }: { selectedTrackId: Ref; + selectedCamera: Ref; editingTrack: Ref; trackMap: Map; - mediaController: Ref; + camTrackMap: Record>; + aggregateController: Ref; recipes: Recipe[]; selectTrack: (trackId: TrackId | null, edit: boolean) => void; selectNextTrack: (delta?: number) => TrackId | null; - addTrack: (frame: number, defaultType: string, afterId?: TrackId) => Track; + addTrack: (frame: number, defaultType: string, afterId?: TrackId, cameraName?: string) => Track; removeTrack: (trackId: TrackId) => void; }) { let creating = false; @@ -76,7 +80,7 @@ export default function useModeManager({ const editingDetails = computed(() => { _depend(); if (editingMode.value && selectedTrackId.value !== null) { - const { frame } = mediaController.value; + const { frame } = aggregateController.value; const track = trackMap.get(selectedTrackId.value); if (track) { const [feature] = track.getFeature(frame.value); @@ -121,11 +125,11 @@ export default function useModeManager({ function seekNearest(track: Track) { // Seek to the nearest point in the track. - const { frame } = mediaController.value; + const { frame } = aggregateController.value; if (frame.value < track.begin) { - mediaController.value.seek(track.begin); + aggregateController.value.seek(track.begin); } else if (frame.value > track.end) { - mediaController.value.seek(track.end); + aggregateController.value.seek(track.end); } } @@ -183,10 +187,10 @@ export default function useModeManager({ function handleAddTrackOrDetection(): TrackId { // Handles adding a new track with the NewTrack Settings - const { frame } = mediaController.value; + const { frame } = aggregateController.value; const newTrackId = addTrack( frame.value, trackSettings.value.newTrackSettings.type, - selectedTrackId.value || undefined, + selectedTrackId.value || undefined, selectedCamera.value, ).trackId; selectTrack(newTrackId, true); creating = true; @@ -207,7 +211,7 @@ export default function useModeManager({ if (trackSettings.value.newTrackSettings.mode === 'Track' && trackSettings.value.newTrackSettings.modeSettings.Track.autoAdvanceFrame ) { - mediaController.value.nextFrame(); + aggregateController.value.nextFrame(); newCreatingValue = true; } else if (trackSettings.value.newTrackSettings.mode === 'Detection') { if ( @@ -224,7 +228,10 @@ export default function useModeManager({ function handleUpdateRectBounds(frameNum: number, flickNum: number, bounds: RectBounds) { if (selectedTrackId.value !== null) { - const track = trackMap.get(selectedTrackId.value); + let track = trackMap.get(selectedTrackId.value); + if (selectedCamera.value !== 'singleCam') { + track = camTrackMap[selectedCamera.value].get(selectedTrackId.value); + } if (track) { // Determines if we are creating a new Detection const { interpolate } = track.canInterpolate(frameNum); @@ -270,7 +277,10 @@ export default function useModeManager({ }; if (selectedTrackId.value !== null) { - const track = trackMap.get(selectedTrackId.value); + let track = trackMap.get(selectedTrackId.value); + if (selectedCamera.value !== 'singleCam') { + track = camTrackMap[selectedCamera.value].get(selectedTrackId.value); + } if (track) { // newDetectionMode is true if there's no keyframe on frameNum const { features, interpolate } = track.canInterpolate(frameNum); @@ -278,7 +288,8 @@ export default function useModeManager({ // Give each recipe the opportunity to make changes recipes.forEach((recipe) => { - const changes = recipe.update(eventType, frameNum, track, [data], key); + // "as Track" used because TS doesn't know that forEach has the proper track setting + const changes = recipe.update(eventType, frameNum, track as Track, [data], key); // Prevent key conflicts among recipes Object.keys(changes.data).forEach((key_) => { if (key_ in update.geoJsonFeatureRecord) { @@ -365,11 +376,14 @@ export default function useModeManager({ /* If any recipes are active, allow them to remove a point */ function handleRemovePoint() { if (selectedTrackId.value !== null && selectedFeatureHandle.value !== -1) { - const track = trackMap.get(selectedTrackId.value); - if (track) { + let track = trackMap.get(selectedTrackId.value); + if (selectedCamera.value !== 'singleCam') { + track = camTrackMap[selectedCamera.value].get(selectedTrackId.value); + } + if (track !== undefined) { recipes.forEach((r) => { - if (r.active.value) { - const { frame } = mediaController.value; + if (r.active.value && track) { + const { frame } = aggregateController.value; r.deletePoint( frame.value, track, @@ -387,11 +401,14 @@ export default function useModeManager({ /* If any recipes are active, remove the geometry they added */ function handleRemoveAnnotation() { if (selectedTrackId.value !== null) { - const track = trackMap.get(selectedTrackId.value); - if (track) { - const { frame } = mediaController.value; + let track = trackMap.get(selectedTrackId.value); + if (selectedCamera.value !== 'singleCam') { + track = camTrackMap[selectedCamera.value].get(selectedTrackId.value); + } + if (track !== undefined) { + const { frame } = aggregateController.value; recipes.forEach((r) => { - if (r.active.value) { + if (r.active.value && track) { r.delete(frame.value, track, selectedKey.value, annotationModes.editing); } }); @@ -439,7 +456,10 @@ export default function useModeManager({ /** Toggle editing mode for track */ function handleTrackEdit(trackId: TrackId) { - const track = getTrack(trackMap, trackId); + let track = getTrack(trackMap, trackId); + if (selectedCamera.value !== 'singleCam') { + track = getTrack(camTrackMap[selectedCamera.value], trackId); + } seekNearest(track); const editing = trackId === selectedTrackId.value ? (!editingTrack.value) : true; handleSelectTrack(trackId, editing); diff --git a/client/dive-common/use/useRequest.ts b/client/dive-common/use/useRequest.ts index ec91c3c81..487151a62 100644 --- a/client/dive-common/use/useRequest.ts +++ b/client/dive-common/use/useRequest.ts @@ -1,4 +1,5 @@ import { reactive, toRefs } from '@vue/composition-api'; +import { AxiosError } from 'axios'; import { getResponseError } from 'vue-media-annotator/utils'; export default function useRequest() { @@ -18,7 +19,7 @@ export default function useRequest() { return val; } catch (err) { state.loading = false; - state.error = getResponseError(err); + state.error = getResponseError(err as AxiosError); throw err; } } diff --git a/client/src/components/LayerManager.vue b/client/src/components/LayerManager.vue index f9855efee..0601467bf 100644 --- a/client/src/components/LayerManager.vue +++ b/client/src/components/LayerManager.vue @@ -4,7 +4,7 @@ import { } from '@vue/composition-api'; import { TrackWithContext } from '../use/useTrackFilters'; -import { injectMediaController } from './annotators/useMediaController'; +import { injectAggregateController } from './annotators/useMediaController'; import RectangleLayer from '../layers/AnnotationLayers/RectangleLayer'; import PolygonLayer from '../layers/AnnotationLayers/PolygonLayer'; import PointLayer from '../layers/AnnotationLayers/PointLayer'; @@ -33,6 +33,8 @@ import { useStateStyles, useMergeList, useAnnotatorPreferences, + useCamTrackMap, + useSelectedCamera, } from '../provides'; /** LayerManager is a component intended to be used as a child of an Annotator. @@ -46,11 +48,20 @@ export default defineComponent({ type: Function as PropType, default: undefined, }, + camera: { + type: String, + default: 'singleCam', + }, }, setup(props) { const handler = useHandler(); const intervalTree = useIntervalTree(); - const trackMap = useTrackMap(); + const camTrackMap = useCamTrackMap(); + const selectedCamera = useSelectedCamera(); + let trackMap = useTrackMap(); + if (props.camera !== 'singleCam' && camTrackMap[props.camera] !== undefined) { + trackMap = camTrackMap[props.camera]; + } const enabledTracksRef = useEnabledTracks(); const selectedTrackIdRef = useSelectedTrackId(); const mergeListRef = useMergeList(); @@ -61,7 +72,7 @@ export default defineComponent({ const stateStyling = useStateStyles(); const annotatorPrefs = useAnnotatorPreferences(); - const annotator = injectMediaController(); + const annotator = injectAggregateController().value.getController(props.camera); const frameNumberRef = annotator.frame; const flickNumberRef = annotator.flick; @@ -141,7 +152,9 @@ export default defineComponent({ (trackId: TrackId) => { const track = trackMap.get(trackId); if (track === undefined) { - throw new Error(`TrackID ${trackId} not found in map`); + // Track may be located in another Camera + // TODO: Find a better way to represent tracks outside of cameras + return; } const enabledIndex = enabledTracks.findIndex( (trackWithContext) => trackWithContext.track.trackId === trackId, @@ -161,7 +174,8 @@ export default defineComponent({ }; frameData.push(trackFrame); if (trackFrame.selected) { - if (editingTrack) { + //Only edit current camera tracks + if (editingTrack && props.camera === selectedCamera.value) { editingTracks.push(trackFrame); } if (annotator.lockedCamera.value) { diff --git a/client/src/components/annotators/ImageAnnotator.vue b/client/src/components/annotators/ImageAnnotator.vue index cdde8cd05..6e8dc6017 100644 --- a/client/src/components/annotators/ImageAnnotator.vue +++ b/client/src/components/annotators/ImageAnnotator.vue @@ -3,7 +3,7 @@ import { defineComponent, ref, onUnmounted, PropType, toRef, watch, } from '@vue/composition-api'; import { SetTimeFunc } from '../../use/useTimeObserver'; -import useMediaController from './useMediaController'; +import { injectCameraInitializer } from './useMediaController'; export interface ImageDataItem { url: string; @@ -47,13 +47,29 @@ export default defineComponent({ type: Number as PropType, default: undefined, }, + camera: { + type: String as PropType, + default: 'singleCam', + }, }, setup(props) { const loadingVideo = ref(false); const loadingImage = ref(true); - const commonMedia = useMediaController(); - const { data } = commonMedia; + const cameraInitializer = injectCameraInitializer(); + const { + state: data, + geoViewer, + cursorHandler, + imageCursor, + container, + initializeViewer, + mediaController, + } = cameraInitializer(props.camera, { + // allow hoisting for these functions to pass a reference before defining them. + // eslint-disable-next-line @typescript-eslint/no-use-before-define + seek, pause, play, setVolume: unimplemented, setSpeed: unimplemented, + }); data.maxFrame = props.imageData.length - 1; // Below are configuration settings we can set until we decide on good numbers to utilize. let local = { @@ -114,7 +130,7 @@ export default defineComponent({ */ local.width = img.naturalWidth; local.height = img.naturalHeight; - commonMedia.resetMapDimensions(local.width, local.height); + mediaController.resetMapDimensions(local.width, local.height); } local.quadFeature .data([ @@ -305,18 +321,6 @@ export default defineComponent({ throw new Error('Method unimplemented!'); } - const { - cursorHandler, - initializeViewer, - mediaController, - } = commonMedia.initialize({ - seek, - play, - pause, - setVolume: unimplemented, - setSpeed: unimplemented, - }); - const setBrightnessFilter = (on: boolean) => { if (local.quadFeature !== undefined) { local.quadFeature.layer().node().css('filter', on ? 'url(#brightness)' : ''); @@ -327,7 +331,7 @@ export default defineComponent({ const imgInternal = cacheFrame(0); imgInternal.onloadPromise.then(() => { initializeViewer(imgInternal.image.naturalWidth, imgInternal.image.naturalHeight); - const quadFeatureLayer = commonMedia.geoViewerRef.value.createLayer('feature', { + const quadFeatureLayer = geoViewer.value.createLayer('feature', { features: ['quad'], autoshareRenderer: false, }); @@ -360,7 +364,7 @@ export default defineComponent({ const imgInternal = cacheFrame(0); imgInternal.onloadPromise.then(() => { initializeViewer(imgInternal.image.naturalWidth, imgInternal.image.naturalHeight); - const quadFeatureLayer = commonMedia.geoViewerRef.value.createLayer('feature', { + const quadFeatureLayer = geoViewer.value.createLayer('feature', { features: ['quad'], autoshareRenderer: false, }); @@ -392,11 +396,9 @@ export default defineComponent({ data, loadingVideo, loadingImage, - imageCursorRef: commonMedia.imageCursorRef, - containerRef: commonMedia.containerRef, - onResize: commonMedia.onResize, + imageCursorRef: imageCursor, + containerRef: container, cursorHandler, - mediaController, }; }, }); @@ -455,11 +457,6 @@ export default defineComponent({ - diff --git a/client/src/components/annotators/VideoAnnotator.vue b/client/src/components/annotators/VideoAnnotator.vue index e15118cc0..e8862751a 100644 --- a/client/src/components/annotators/VideoAnnotator.vue +++ b/client/src/components/annotators/VideoAnnotator.vue @@ -4,7 +4,7 @@ import { } from '@vue/composition-api'; import { Flick, SetTimeFunc } from '../../use/useTimeObserver'; -import useMediaController from './useMediaController'; +import { injectCameraInitializer } from './useMediaController'; /** * For MPEG codecs, the PTS (Presentation Timestamp) @@ -98,11 +98,27 @@ export default defineComponent({ type: Number as PropType, default: undefined, }, + camera: { + type: String as PropType, + default: 'singleCam', + }, }, setup(props) { - const commonMedia = useMediaController(); - const { data } = commonMedia; + const cameraInitializer = injectCameraInitializer(); + const { + state: data, + geoViewer, + cursorHandler, + imageCursor, + container, + initializeViewer, + mediaController, + } = cameraInitializer(props.camera, { + // allow hoisting for these functions. + // eslint-disable-next-line @typescript-eslint/no-use-before-define + seek, pause, play, setVolume, setSpeed, + }); function makeVideo() { const video = document.createElement('video'); @@ -154,7 +170,7 @@ export default defineComponent({ data.frame = Math.floor(newFrame); data.flick = Math.round(video.currentTime * Flick); data.syncedFrame = data.frame; - commonMedia.geoViewerRef.value.scheduleAnimationFrame(syncWithVideo); + geoViewer.value.scheduleAnimationFrame(syncWithVideo); } data.currentTime = video.currentTime; } @@ -183,14 +199,6 @@ export default defineComponent({ data.speed = video.playbackRate; } - const { - cursorHandler, - initializeViewer, - mediaController, - } = commonMedia.initialize({ - seek, play, pause, setVolume, setSpeed, - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any let quadFeatureLayer = undefined as any; const setBrightnessFilter = (on: boolean) => { @@ -223,7 +231,7 @@ export default defineComponent({ data.maxFrame = maybeMaxFrame; } initializeViewer(width, height); - quadFeatureLayer = commonMedia.geoViewerRef.value.createLayer('feature', { + quadFeatureLayer = geoViewer.value.createLayer('feature', { features: ['quad.video'], autoshareRenderer: false, }); @@ -268,9 +276,8 @@ export default defineComponent({ return { data, - imageCursorRef: commonMedia.imageCursorRef, - containerRef: commonMedia.containerRef, - onResize: commonMedia.onResize, + imageCursorRef: imageCursor, + containerRef: container, cursorHandler, mediaController, }; @@ -280,7 +287,6 @@ export default defineComponent({