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 @@