diff --git a/resources/assets/js/annotations/components/annotationCanvas/drawInteractions.vue b/resources/assets/js/annotations/components/annotationCanvas/drawInteractions.vue index 2e0b3fd1d..2abd93c32 100644 --- a/resources/assets/js/annotations/components/annotationCanvas/drawInteractions.vue +++ b/resources/assets/js/annotations/components/annotationCanvas/drawInteractions.vue @@ -5,14 +5,9 @@ import Styles from '../../stores/styles'; import { shiftKeyOnly } from '@biigle/ol/events/condition'; import snapInteraction from '../../snapInteraction.vue'; import { Point } from '@biigle/ol/geom'; +import * as preventDoubleclick from '../../../prevent-doubleclick'; -function computeDistance(point1, point2) { - let p1=point1.getCoordinates(); - let p2=point2.getCoordinates(); - return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2)); -} - /** * Mixin for the annotationCanvas component that contains logic for the draw interactions. * @@ -21,9 +16,6 @@ function computeDistance(point1, point2) { let drawInteraction; -const POINT_CLICK_COOLDOWN = 400; -const POINT_CLICK_DISTANCE = 5; - // Custom OpenLayers freehandCondition that is true if a pen is used for input or // if Shift is pressed otherwise. let penOrShift = function (mapBrowserEvent) { @@ -142,8 +134,8 @@ export default { } }, isPointDoubleClick(e) { - return new Date().getTime() - this.lastDrawnPointTime < POINT_CLICK_COOLDOWN - && computeDistance(this.lastDrawnPoint,e.feature.getGeometry()) < POINT_CLICK_DISTANCE; + return new Date().getTime() - this.lastDrawnPointTime < preventDoubleclick.POINT_CLICK_COOLDOWN + && preventDoubleclick.computeDistance(this.lastDrawnPoint,e.feature.getGeometry()) < preventDoubleclick.POINT_CLICK_DISTANCE; }, }, watch: { diff --git a/resources/assets/js/prevent-doubleclick.js b/resources/assets/js/prevent-doubleclick.js new file mode 100644 index 000000000..52d1d1521 --- /dev/null +++ b/resources/assets/js/prevent-doubleclick.js @@ -0,0 +1,18 @@ +/** +* Compute the Euclidean distance between two points. +* +* @param Object point1 - The first point with getCoordinates() method. +* @param Object point2 - The second point with getCoordinates() method. +* @returns number - The computed distance between the two points. +*/ + +const POINT_CLICK_COOLDOWN = 400; +const POINT_CLICK_DISTANCE = 5; + +let computeDistance = function (point1, point2) { + let p1 = point1.getCoordinates(); + let p2 = point2.getCoordinates(); + return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2)); +}; + +export { computeDistance, POINT_CLICK_COOLDOWN, POINT_CLICK_DISTANCE }; diff --git a/resources/assets/js/videos/components/settingsTab.vue b/resources/assets/js/videos/components/settingsTab.vue index 743d8b900..b8d67b62c 100644 --- a/resources/assets/js/videos/components/settingsTab.vue +++ b/resources/assets/js/videos/components/settingsTab.vue @@ -26,6 +26,7 @@ export default { 'enableJumpByFrame', 'jumpStep', 'muteVideo', + 'singleAnnotation', ], annotationOpacity: 1, showMinimap: true, @@ -38,6 +39,7 @@ export default { showThumbnailPreview: true, enableJumpByFrame: false, muteVideo: true, + singleAnnotation: false, }; }, computed: { @@ -88,6 +90,12 @@ export default { handleUnmuteVideo() { this.muteVideo = false; }, + handleSingleAnnotation() { + this.singleAnnotation = true; + }, + handleDisableSingleAnnotation() { + this.singleAnnotation = false; + }, toggleAnnotationOpacity() { if (this.annotationOpacity > 0) { this.annotationOpacity = 0; @@ -148,6 +156,10 @@ export default { this.$emit('update', 'muteVideo', show); Settings.set('muteVideo', show); }, + singleAnnotation(show) { + this.$emit('update', 'singleAnnotation', show); + Settings.set('singleAnnotation', show); + }, }, created() { this.restoreKeys.forEach((key) => { diff --git a/resources/assets/js/videos/components/videoScreen.vue b/resources/assets/js/videos/components/videoScreen.vue index fb287ecbe..517e72a92 100644 --- a/resources/assets/js/videos/components/videoScreen.vue +++ b/resources/assets/js/videos/components/videoScreen.vue @@ -77,16 +77,30 @@ @click="drawPoint" > + + @@ -100,9 +114,16 @@ @click="drawRectangle" > + @@ -116,16 +137,30 @@ @click="drawCircle" > + + @@ -139,9 +174,16 @@ @click="drawLineString" > + @@ -154,10 +196,16 @@ @click="drawPolygon" > + + @@ -360,6 +415,10 @@ export default { type: Boolean, default: true, }, + singleAnnotation: { + type: Boolean, + default: false, + }, showMousePosition: { type: Boolean, default: true, @@ -429,6 +488,12 @@ export default { jumpForwardMessage() { return `Advance video by ${this.jumpStep} s 𝗖𝘁𝗿𝗹+𝗥𝗶𝗴𝗵𝘁 𝗮𝗿𝗿𝗼𝘄`; }, + disableFinishTrackAnnotation() { + return this.cantFinishTrackAnnotation || this.disableJobTracking || this.singleAnnotation + }, + disableFinishDrawAnnotation() { + return this.cantFinishDrawAnnotation || this.singleAnnotation + }, }, methods: { createMap() { diff --git a/resources/assets/js/videos/components/videoScreen/drawInteractions.vue b/resources/assets/js/videos/components/videoScreen/drawInteractions.vue index ebc9d1ede..e07b87266 100644 --- a/resources/assets/js/videos/components/videoScreen/drawInteractions.vue +++ b/resources/assets/js/videos/components/videoScreen/drawInteractions.vue @@ -7,12 +7,15 @@ import VectorLayer from '@biigle/ol/layer/Vector'; import VectorSource from '@biigle/ol/source/Vector'; import snapInteraction from "./snapInteraction.vue"; import { isInvalidShape } from '../../../annotations/utils'; +import { Point } from '@biigle/ol/geom'; +import * as preventDoubleclick from '../../../prevent-doubleclick'; /** * Mixin for the videoScreen component that contains logic for the draw interactions. * * @type {Object} */ + export default { mixins: [snapInteraction], data() { @@ -20,8 +23,16 @@ export default { pendingAnnotation: {}, autoplayDrawTimeout: null, drawEnded: true, + lastDrawnPoint: new Point(0, 0), + lastDrawnPointTime: 0, }; }, + props: { + singleAnnotation: { + type: Boolean, + default: false + } + }, computed: { hasSelectedLabel() { return !!this.selectedLabel; @@ -207,6 +218,24 @@ export default { window.clearTimeout(this.autoplayDrawTimeout); this.autoplayDrawTimeout = window.setTimeout(this.pause, this.autoplayDraw * 1000); } + + if (this.singleAnnotation) { + if (this.isDrawingPoint) { + if (this.isPointDoubleClick(e)) { + // The feature is added to the source only after this event + // is handled, so remove has to happen after the addfeature + // event. + this.pendingAnnotationSource.once('addfeature', function (e) { + this.removeFeature(e.feature); + }); + this.resetPendingAnnotation(this.pendingAnnotation.shape); + return + } + this.lastDrawnPointTime = new Date().getTime(); + this.lastDrawnPoint = e.feature.getGeometry(); + } + this.pendingAnnotationSource.once('addfeature', this.finishDrawAnnotation); + } } else { // If the pending annotation (time) is invalid, remove it again. // We have to wait for this feature to be added to the source to be able @@ -217,6 +246,11 @@ export default { } this.$emit('pending-annotation', this.pendingAnnotation); + + }, + isPointDoubleClick(e) { + return new Date().getTime() - this.lastDrawnPointTime < preventDoubleclick.POINT_CLICK_COOLDOWN + && preventDoubleclick.computeDistance(this.lastDrawnPoint, e.feature.getGeometry()) < preventDoubleclick.POINT_CLICK_DISTANCE; }, }, created() { diff --git a/resources/assets/js/videos/stores/settings.js b/resources/assets/js/videos/stores/settings.js index e89afd50c..2fc6e8893 100644 --- a/resources/assets/js/videos/stores/settings.js +++ b/resources/assets/js/videos/stores/settings.js @@ -11,6 +11,7 @@ let defaults = { enableJumpByFrame: false, jumpStep: 5.0, muteVideo: true, + singleAnnotation: false, }; export default new Settings({ diff --git a/resources/assets/js/videos/utils.js b/resources/assets/js/videos/utils.js index 1e63abe08..aacee9972 100644 --- a/resources/assets/js/videos/utils.js +++ b/resources/assets/js/videos/utils.js @@ -24,4 +24,10 @@ let getRoundToPrecision = function (reference) { }; }; -export {getRoundToPrecision}; +let computeDistance = function (point1, point2) { + let p1 = point1.getCoordinates(); + let p2 = point2.getCoordinates(); + return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2)); +}; + +export { getRoundToPrecision, computeDistance }; diff --git a/resources/assets/js/videos/videoContainer.vue b/resources/assets/js/videos/videoContainer.vue index 3fd523a54..f35828e8d 100644 --- a/resources/assets/js/videos/videoContainer.vue +++ b/resources/assets/js/videos/videoContainer.vue @@ -73,6 +73,7 @@ export default { showThumbnailPreview: true, enableJumpByFrame: false, muteVideo: true, + singleAnnotation: false, }, openTab: '', urlParams: { diff --git a/resources/views/manual/tutorials/videos/sidebar.blade.php b/resources/views/manual/tutorials/videos/sidebar.blade.php index 2211a5f0d..bdaea80fe 100644 --- a/resources/views/manual/tutorials/videos/sidebar.blade.php +++ b/resources/views/manual/tutorials/videos/sidebar.blade.php @@ -87,5 +87,9 @@

The mute video switch enables or disables the audio track of the video.

+ +

+ The Single Frame Annotation switch allows you to add annotations with a single click by automatically completing them after the first frame. When enabled, additional controls for finishing and tracking are disabled. +

@endsection diff --git a/resources/views/videos/show/content.blade.php b/resources/views/videos/show/content.blade.php index faf6bf059..6e9132368 100644 --- a/resources/views/videos/show/content.blade.php +++ b/resources/views/videos/show/content.blade.php @@ -38,6 +38,7 @@ :selected-label="selectedLabel" :show-label-tooltip="settings.showLabelTooltip" :show-minimap="settings.showMinimap" + :single-annotation="settings.singleAnnotation" :show-mouse-position="settings.showMousePosition" :enable-jump-by-frame="settings.enableJumpByFrame" :video="video" diff --git a/resources/views/videos/show/sidebar-settings.blade.php b/resources/views/videos/show/sidebar-settings.blade.php index 8bfef31b0..76d81a3d7 100644 --- a/resources/views/videos/show/sidebar-settings.blade.php +++ b/resources/views/videos/show/sidebar-settings.blade.php @@ -52,6 +52,10 @@ + +