diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index 04aa8e7a03..ade05d6de9 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -23,6 +23,8 @@ import {CrossOriginTiffError} from './stores/images.js'; import {debounce} from '@/core/utils.js'; import {handleErrorResponse} from '@/core/messages/store.js'; import {urlParams as UrlParams} from '@/core/utils.js'; +import {LawnmowerSaveState} from './components/annotationCanvas/lawnmower.vue'; +import {useVolareMode} from './components/annotationCanvas/volare.js'; const asyncAnnotationCanvas = defineAsyncComponent({ loader: function () { @@ -73,7 +75,6 @@ export default { mapResolution: undefined, selectedLabel: null, annotationMode: 'default', - focussedAnnotationIndex: null, // For lawnmower and sampling modes: When switching images, this determines // if the first (0) or the last (Infinity) image section/sampling location // should be shown. @@ -89,13 +90,15 @@ export default { preferredUnit: 'auto', imagesArea: null, openTab: null, - userUpdatedVolareResolution: false, userId: null, crossOriginError: false, maybeCorsTiffError: false, imageFilenames: {}, labelTrees: [], projectIds: [], + lawnmowerSaveState: "", + volare: null, + pauseLawnmowerAutomatically: false, }; }, provide() { @@ -131,14 +134,8 @@ export default { selectedAnnotations() { return this.filteredAnnotations.filter((a) => a.selected); }, - focussedAnnotation() { - return this.filteredAnnotations[this.focussedAnnotationIndex]; - }, isDefaultAnnotationMode() { - return this.annotationMode === 'default'; - }, - isVolareAnnotationMode() { - return this.annotationMode === 'volare'; + return this.annotationMode === 'default' || this.annotationMode === 'volarePaused' || this.annotationMode === 'lawnmowerPaused'; }, isLawnmowerAnnotationMode() { return this.annotationMode === 'lawnmower'; @@ -206,20 +203,8 @@ export default { return; } - if (this.isVolareAnnotationMode) { - if (this.focussedAnnotationIndex < (this.filteredAnnotations.length - 1)) { - this.focussedAnnotationIndex++; - return; - } else if (this.focussedAnnotationIndex === Infinity) { - // This may happen if the volume has only one image and we can't - // switch to the next image. Here, we want to go to the second - // annotation of the image. - this.focussedAnnotationIndex = Math.min(1, this.filteredAnnotations.length - 1); - } else { - // Show the first annotation of the next image in this case, so - // don't return. - this.focussedAnnotationIndex = -Infinity; - } + if (this.volare.handleNextAnnotation()) { + return; } else if (this.isLawnmowerAnnotationMode) { // This returns false if the image section can't be advanced (i.e. // the last section is shown). @@ -246,20 +231,8 @@ export default { return; } - if (this.isVolareAnnotationMode) { - if (this.focussedAnnotationIndex > 0) { - this.focussedAnnotationIndex--; - return; - } else if (this.focussedAnnotationIndex === -Infinity) { - // This may happen if the volume has only one image and we can't - // switch to the next image. Here, we want to go to the second to - // last annotation of the image. - this.focussedAnnotationIndex = Math.max(this.filteredAnnotations.length - 2, 0); - } else { - // Show the last annotation of the previous image in this case, - // so don't return. - this.focussedAnnotationIndex = Infinity; - } + if (this.volare.handlePreviousAnnotation()) { + return; } else if (this.isLawnmowerAnnotationMode) { // This returns false if the image section can't be reversed (i.e. // the first section is shown). @@ -282,26 +255,6 @@ export default { // Show previous image. this.imageIndex = this.getPreviousIndex(this.imageIndex); }, - maybeUpdateFocussedAnnotation() { - if (this.isVolareAnnotationMode) { - if (this.filteredAnnotations.length > 0) { - if (this.focussedAnnotationIndex === Infinity) { - // Show the last annotation if the previous image is shown. - this.focussedAnnotationIndex = this.filteredAnnotations.length - 1; - } else { - // Show the first annotation if the next image is shown or - // the annotation filter changed. - this.focussedAnnotationIndex = 0; - } - } else { - // Show the whole image if there are no annotations. - this.focussedAnnotationIndex = null; - this.$refs.canvas.fitImage(); - } - } else { - this.focussedAnnotationIndex = null; - } - }, maybeUpdateShownImageSection() { if (this.isLawnmowerAnnotationMode) { if (this.annotationModeCarry === Infinity) { @@ -325,7 +278,6 @@ export default { } }, maybeUpdateAnnotationMode(data) { - this.maybeUpdateFocussedAnnotation(); this.maybeUpdateShownImageSection(); this.maybeUpdateShownSampling(data); }, @@ -338,6 +290,9 @@ export default { y: Math.round(viewport.center[1]), }); }, + handleMapMovestart() { + this.pauseLawnmowerAfterViewportChanged(); + }, // Handler for the select event fired by the global event bus. handleSelectAnnotation(annotation, shift) { // Handle the case where the second argument is an event object for @@ -370,9 +325,6 @@ export default { }); } }, - focusAnnotation(annotation, fast, keepResolution) { - this.$refs.canvas.focusAnnotation(annotation, fast, keepResolution); - }, handleDetachAnnotationLabel(annotation, annotationLabel) { if (this.isEditor) { if (annotation.labels.length > 1) { @@ -411,13 +363,6 @@ export default { .catch(handleErrorResponse); } }, - selectAndFocusAnnotation(annotation, keepResolution) { - this.selectedAnnotations.forEach(function (a) { - a.selected = false; - }); - annotation.selected = true; - this.focusAnnotation(annotation, true, keepResolution); - }, handleFilter(filter) { this.annotationFilter = filter; }, @@ -471,7 +416,7 @@ export default { confidence: 1, }; - if (this.isVolareAnnotationMode) { + if (this.annotationMode === 'volare') { this.$refs.canvas.blinkAnnotation(annotation); } @@ -596,6 +541,31 @@ export default { break; } }, + enableAutomaticLawnmowerPausing() { + this.pauseLawnmowerAutomatically = true; + }, + disableAutomaticLawnmowerPausing() { + this.pauseLawnmowerAutomatically = false; + }, + pauseLawnmowerAfterViewportChanged() { + if (!this.isLawnmowerAnnotationMode || !this.pauseLawnmowerAutomatically) { + return; + } + + this.$refs.annotationModesTab.pauseLawnmower(); + }, + pauseLawnmower(timestamp) { + this.$refs.annotationModesTab.pauseLawnmower(timestamp); + }, + pauseVolare(timestamp) { + this.$refs.annotationModesTab.pauseVolare(timestamp); + }, + showImageWithId(imageId) { + const index = this.imagesIds.indexOf(imageId); + if (index !== -1) { + this.imageIndex = index; + } + }, handleAnnotationModeChange(mode, data) { this.annotationMode = mode; this.annotationModeCarry = null; @@ -663,6 +633,9 @@ export default { openSidebarLabels() { this.openTab = 'labels'; }, + focusAnnotation(annotation, fast, keepResolution) { + this.$refs.canvas.focusAnnotation(annotation, fast, keepResolution); + } }, watch: { async imageId(id) { @@ -677,6 +650,11 @@ export default { let [image, annotations] = await Promise.all(this.getImageAndAnnotationsPromises(id)); this.image = image; this.annotations = annotations; + + // Wait for the canvas to be updated, otherwise lawnmower mode + // may compute the current section using the old viewport + await this.$nextTick(); + this.maybeUpdateAnnotationMode(); this.maybeShowTilingInProgressMessage(); } catch (e) { @@ -702,14 +680,6 @@ export default { // Twice the count because the next and previous images are cached. ImagesStore.setMaxCacheSize(count * 2); }, - focussedAnnotation(annotation) { - if (annotation) { - this.selectAndFocusAnnotation(annotation, this.userUpdatedVolareResolution); - } - }, - annotationFilter() { - this.maybeUpdateFocussedAnnotation(); - }, showScaleLine(show) { if (show) { this.fetchImagesArea(); @@ -720,16 +690,6 @@ export default { this.fetchImagesArea(); } }, - isVolareAnnotationMode(enabled) { - if (!enabled) { - this.userUpdatedVolareResolution = false; - } - }, - mapResolution() { - if (this.isVolareAnnotationMode) { - this.userUpdatedVolareResolution = true; - } - }, image(image) { this.crossOriginError = image?.crossOrigin; }, @@ -738,6 +698,15 @@ export default { this.openTab = ''; } }, + annotationMode(newMode, oldMode) { + if (newMode === 'lawnmowerPaused') { + this.lawnmowerSaveState = LawnmowerSaveState.SAVE; + } else if (oldMode === 'lawnmowerPaused' && newMode === 'lawnmower') { + this.lawnmowerSaveState = LawnmowerSaveState.LOAD; + } else { + this.lawnmowerSaveState = LawnmowerSaveState.DISCARD; + } + } }, created() { this.allImagesIds = biigle.$require('annotations.imagesIds'); @@ -789,7 +758,7 @@ export default { // Use $nextTick so the annotationCanvas component has time to // render the image. this.$nextTick( - () => this.selectAndFocusAnnotation(annotations[i]) + () => this.volare.selectAndFocusAnnotation(annotations[i]) ); return; } @@ -813,6 +782,19 @@ export default { Keyboard.on('C', this.selectLastAnnotation, 0, this.listenerSet); this.initLabelBot(); + + this.volare = useVolareMode({ + filteredAnnotations: computed(() => this.filteredAnnotations), + selectedAnnotations: computed(() => this.selectedAnnotations), + focusAnnotationInCanvas: this.focusAnnotation, + fitImageInCanvas: (...args) => this.$refs.canvas.fitImage(...args), + annotationFilter: this.annotationFilter, + image: computed(() => this.image), + mapResolution: computed(() => this.mapResolution), + showImageWithId: this.showImageWithId, + annotationMode: computed(() => this.annotationMode), + restoreVolarePauseState: (timestamp) => this.$refs.annotationModesTab.pauseVolare(timestamp) + }); }, }; diff --git a/resources/assets/js/annotations/components/annotationCanvas.vue b/resources/assets/js/annotations/components/annotationCanvas.vue index a5d6801960..2e55e5f020 100644 --- a/resources/assets/js/annotations/components/annotationCanvas.vue +++ b/resources/assets/js/annotations/components/annotationCanvas.vue @@ -60,6 +60,7 @@ export default { template: '#annotation-canvas-template', emits: [ 'moveend', + 'movestart', 'update', 'select', 'previous', @@ -910,6 +911,7 @@ export default { this.map.on('change:size', this.updateMapSize); this.map.on('moveend', this.updateMapView); + this.map.on('movestart', () => this.$emit('movestart')); this.map.on('pointermove', this.updateMousePosition); this.selectInteraction.on('select', this.handleFeatureSelect); diff --git a/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue b/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue index 3127c28636..da04e9a2de 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue +++ b/resources/assets/js/annotations/components/annotationCanvas/lawnmower.vue @@ -1,10 +1,28 @@ \ No newline at end of file diff --git a/resources/assets/js/core/components/powerToggle.vue b/resources/assets/js/core/components/powerToggle.vue index 49ff2cc5aa..3345eb6924 100644 --- a/resources/assets/js/core/components/powerToggle.vue +++ b/resources/assets/js/core/components/powerToggle.vue @@ -48,10 +48,14 @@ export default { type: String, default: 'info', }, - icon: { + iconOn: { type: String, default: 'power-off', }, + iconOff: { + type: String, + default: 'power-off' + }, disabled: { type: Boolean, default: false, @@ -72,7 +76,11 @@ export default { return obj; }, iconClass() { - return `fa-${this.icon}`; + if (this.active) { + return `fa-${this.iconOn}`; + } else { + return `fa-${this.iconOff}`; + } }, }, methods: { diff --git a/resources/assets/js/utils.js b/resources/assets/js/utils.js index 8f4b45e354..9c39edae08 100644 --- a/resources/assets/js/utils.js +++ b/resources/assets/js/utils.js @@ -62,6 +62,10 @@ let declare = function (namespace, object) { return object; }; +function required(name) { + throw new Error(`Missing required parameter '${name}'`); +} + function setOrUnsetProperty(feature, property, value) { if (!feature) { return; @@ -74,4 +78,4 @@ function setOrUnsetProperty(feature, property, value) { } } -export {mount, require, declare, setOrUnsetProperty}; +export { mount, require, required, declare, setOrUnsetProperty }; diff --git a/resources/assets/sass/annotations/sidebar/_main.scss b/resources/assets/sass/annotations/sidebar/_main.scss index 7032e658dc..ab97ea6395 100644 --- a/resources/assets/sass/annotations/sidebar/_main.scss +++ b/resources/assets/sass/annotations/sidebar/_main.scss @@ -14,9 +14,15 @@ margin-top: 0; } } + + .sidebar-tab__timestamp { + padding-left: 2px; + color: $text-muted; + font-size: $font-size-small; + } } @import "labels"; @import "annotation-modes"; @import "image-labels"; -@import "settingsTab"; +@import "settingsTab"; \ No newline at end of file diff --git a/resources/views/annotations/show.blade.php b/resources/views/annotations/show.blade.php index 18ec249ce4..6aa4d36463 100644 --- a/resources/views/annotations/show.blade.php +++ b/resources/views/annotations/show.blade.php @@ -100,9 +100,11 @@ :focused-popup-key="focusedPopupKey" :labelbot-timeout="labelbotTimeout" :draft-annotation-uses-label-color="draftAnnotationUsesLabelColor" + :lawnmower-save-state="lawnmowerSaveState" v-on:change-labelbot-focused-popup="changeLabelbotFocusedPopup" v-on:close-labelbot-popup="closeLabelbotPopup" v-on:moveend="handleMapMoveend" + v-on:movestart="handleMapMovestart" v-on:previous="handlePrevious" v-on:next="handleNext" v-on:new="handleNewAnnotation" @@ -115,6 +117,10 @@ v-on:measuring="fetchImagesArea" v-on:requires-selected-label="handleRequiresSelectedLabel" v-on:is-invalid-shape="handleInvalidShape" + v-on:restore-lawnmower-image="showImageWithId" + v-on:lawnmower-pre-viewport-change="disableAutomaticLawnmowerPausing" + v-on:lawnmower-post-viewport-change="enableAutomaticLawnmowerPausing" + v-on:restore-lawnmower-pause-state="pauseLawnmower" ref="canvas" > diff --git a/resources/views/annotations/show/tabs/annotationModes.blade.php b/resources/views/annotations/show/tabs/annotationModes.blade.php index 3b61240737..d42eacc01c 100644 --- a/resources/views/annotations/show/tabs/annotationModes.blade.php +++ b/resources/views/annotations/show/tabs/annotationModes.blade.php @@ -1,5 +1,9 @@ - + @push('scripts') @@ -7,27 +11,55 @@
@can('add-annotation', $image)