From 518a6f4460c7aa5524f72e4b061346bf890b0f4c Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 9 May 2022 13:10:37 -0400 Subject: [PATCH] feat: Add controls to combine and change annotations. --- CHANGELOG.md | 1 + src/annotation/annotation.js | 6 +++ src/annotation/polygonAnnotation.js | 3 +- src/annotation/rectangleAnnotation.js | 3 +- src/annotationLayer.js | 55 +++++++++++++++++++++++++ src/css/cursor-crosshair-difference.svg | 4 ++ src/css/cursor-crosshair-intersect.svg | 4 ++ src/css/cursor-crosshair-union.svg | 4 ++ src/css/cursor-crosshair-xor.svg | 4 ++ src/main.styl | 8 ++++ src/util/polyops.js | 5 +++ tests/cases/annotationLayer.js | 41 ++++++++++++++++++ tests/cases/polyops.js | 6 +++ 13 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 src/css/cursor-crosshair-difference.svg create mode 100644 src/css/cursor-crosshair-intersect.svg create mode 100644 src/css/cursor-crosshair-union.svg create mode 100644 src/css/cursor-crosshair-xor.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index b47588f909..cc69609551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Polygon operations ([#1198](../../pull/1198)) +- Use meta keys to modify annotations ([#1209](../../pull/1209)) ## Version 1.8.8 diff --git a/src/annotation/annotation.js b/src/annotation/annotation.js index cba55d88d0..dd93396f0f 100644 --- a/src/annotation/annotation.js +++ b/src/annotation/annotation.js @@ -74,6 +74,12 @@ var editHandleFeatureLevel = 3; * @property {boolean|string[]} [showLabel=true] `true` to show the annotation * label on annotations in done or edit states. Alternately, a list of * states in which to show the label. Falsy to not show the label. + * @property {boolean} [allowBooleanOperations] This defaults to `true` for + * annotations that have area and `false` for those without area (e.g., + * false for lines and points). If it is truthy, then, when the annotation + * is being created, it checks the metakeys on the first click that defines + * a coordinate to determine what boolean polygon operation should be + * performaned on the completion of the annotation. */ /** diff --git a/src/annotation/polygonAnnotation.js b/src/annotation/polygonAnnotation.js index b2485e323c..f69ccd4877 100644 --- a/src/annotation/polygonAnnotation.js +++ b/src/annotation/polygonAnnotation.js @@ -79,7 +79,8 @@ var polygonAnnotation = function (args) { }, stroke: false, strokeColor: {r: 0, g: 0, b: 1} - } + }, + allowBooleanOperations: true }, args || {}); args.vertices = args.vertices || args.coordinates || []; delete args.coordinates; diff --git a/src/annotation/rectangleAnnotation.js b/src/annotation/rectangleAnnotation.js index 024bcfa3ff..36e796c74b 100644 --- a/src/annotation/rectangleAnnotation.js +++ b/src/annotation/rectangleAnnotation.js @@ -69,7 +69,8 @@ var rectangleAnnotation = function (args, annotationName) { fillColor: {r: 0.3, g: 0.3, b: 0.3}, fillOpacity: 0.25, strokeColor: {r: 0, g: 0, b: 1} - } + }, + allowBooleanOperations: true }, args || {}); args.corners = args.corners || args.coordinates || []; delete args.coordinates; diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 533c6f0b38..2f1a3a1d2b 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -181,6 +181,29 @@ var annotationLayer = function (arg) { m_this._updateFromEvent(update); }; + /** + * Check if the map is currently in a mode where we are adding an annotation + * with a boolean operation. If so, remove the current annotation from the + * layer, then apply it via the boolean operation. + */ + this._handleBooleanOperation = function () { + const mapNode = m_this.map().node(); + let op; + Object.values(m_this._booleanClasses).forEach((c) => { + if (mapNode.hasClass(c)) { + op = c.split('-')[1]; + } + }); + if (!op || !m_this.currentAnnotation || !m_this.currentAnnotation.toPolygonList) { + return; + } + const newAnnot = m_this.currentAnnotation; + m_this.removeAnnotation(m_this.currentAnnotation, false); + if (m_this.annotations().length || (op !== 'difference' && op !== 'intersect')) { + util.polyops[op](m_this, newAnnot.toPolygonList(), {correspond: {}, keepAnnotations: 'exact', style: m_this}); + } + }; + /** * Handle updating the current annotation based on an update state. * @@ -196,6 +219,7 @@ var annotationLayer = function (arg) { m_this.mode(null); break; case 'done': + m_this._handleBooleanOperation(); m_this.mode(null); break; } @@ -205,6 +229,23 @@ var annotationLayer = function (arg) { } }; + /** + * Check the state of the modifier keys and apply them if appropriate. + * + * @param {geo.event} evt The mouse move or click event. + */ + this._handleMouseMoveModifiers = function (evt) { + if (m_this.mode() !== m_this.modes.edit && m_this.currentAnnotation.options('allowBooleanOperations') && m_this.currentAnnotation._coordinates().length < 2) { + if (evt.modifiers) { + const mod = (evt.modifiers.shift ? 's' : '') + (evt.modifiers.ctrl ? 'c' : '') + (evt.modifiers.meta || evt.modifiers.alt ? 'a' : ''); + const mapNode = m_this.map().node(); + Object.values(m_this._booleanClasses).forEach((c) => { + mapNode.toggleClass(c, m_this._booleanClasses[mod] === c); + }); + } + } + }; + /** * Handle mouse movement. If there is a current annotation, the movement * event is sent to it. @@ -213,6 +254,7 @@ var annotationLayer = function (arg) { */ this._handleMouseMove = function (evt) { if (m_this.mode() && m_this.currentAnnotation) { + m_this._handleMouseMoveModifiers(evt); var update = m_this.currentAnnotation.mouseMove(evt); if (update) { m_this.modified(); @@ -316,6 +358,7 @@ var annotationLayer = function (arg) { }); retrigger = true; } else if (m_this.mode() && m_this.currentAnnotation) { + m_this._handleMouseMoveModifiers(evt); update = m_this.currentAnnotation.mouseClick(evt); m_this._updateFromEvent(update); retrigger = !m_this.mode(); @@ -518,6 +561,15 @@ var annotationLayer = function (arg) { edit: 'edit' }; + /* Keys are short-hand for preferred event modifiers. Values are classes to + * apply to the map node. */ + this._booleanClasses = { + s: 'annotation-union', + sc: 'annotation-intersect', + c: 'annotation-difference', + sa: 'annotation-xor' + }; + /** * Get or set the current mode. * @@ -542,6 +594,9 @@ var annotationLayer = function (arg) { mapNode = m_this.map().node(), oldMode = m_mode; m_mode = arg; mapNode.toggleClass('annotation-input', !!(m_mode && m_mode !== m_this.modes.edit)); + if (!m_mode || m_mode === m_this.modes.edit) { + Object.values(m_this._booleanClasses).forEach((c) => mapNode.toggleClass(c, false)); + } if (!m_keyHandler) { m_keyHandler = Mousetrap(mapNode[0]); } diff --git a/src/css/cursor-crosshair-difference.svg b/src/css/cursor-crosshair-difference.svg new file mode 100644 index 0000000000..7f878e207d --- /dev/null +++ b/src/css/cursor-crosshair-difference.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/css/cursor-crosshair-intersect.svg b/src/css/cursor-crosshair-intersect.svg new file mode 100644 index 0000000000..7fd92e3180 --- /dev/null +++ b/src/css/cursor-crosshair-intersect.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/css/cursor-crosshair-union.svg b/src/css/cursor-crosshair-union.svg new file mode 100644 index 0000000000..811ded9186 --- /dev/null +++ b/src/css/cursor-crosshair-union.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/css/cursor-crosshair-xor.svg b/src/css/cursor-crosshair-xor.svg new file mode 100644 index 0000000000..e13e9e28ac --- /dev/null +++ b/src/css/cursor-crosshair-xor.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main.styl b/src/main.styl index 928f12df33..13ca13c989 100644 --- a/src/main.styl +++ b/src/main.styl @@ -49,6 +49,14 @@ &.annotation-input cursor crosshair + &.annotation-intersect + cursor embedurl("./css/cursor-crosshair-intersect.svg") 12 12,crosshair + &.annotation-difference + cursor embedurl("./css/cursor-crosshair-difference.svg") 12 12,crosshair + &.annotation-union + cursor embedurl("./css/cursor-crosshair-union.svg") 12 12,crosshair + &.annotation-xor + cursor embedurl("./css/cursor-crosshair-xor.svg") 12 12,crosshair &.highlight-focus &:after diff --git a/src/util/polyops.js b/src/util/polyops.js index f0f3d5c95f..478c7cb349 100644 --- a/src/util/polyops.js +++ b/src/util/polyops.js @@ -181,6 +181,11 @@ function toPolygonList(poly, mode, opts) { if (poly.toPolygonList) { mode.style = poly; poly = poly.toPolygonList(opts); + if (!poly.length) { + mode.min = mode.max = [0, 0]; + mode.epsilon = 1e-10; + return poly; + } } else { mode.style = ''; if (poly.outer) { diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index d868ab901f..c8ff39af4a 100644 --- a/tests/cases/annotationLayer.js +++ b/tests/cases/annotationLayer.js @@ -514,6 +514,47 @@ describe('geo.annotationLayer', function () { expect(layer.mode()).toBe(null); layer.options('clickToEdit', false); }); + it('_handleMouseClick with modifier', function () { + layer.removeAllAnnotations(); + layer.mode('polygon'); + expect(layer.annotations().length).toBe(1); + expect(layer.annotations()[0].options('vertices').length).toBe(0); + var time = Date.now(); + layer._handleMouseClick({ + buttonsDown: {left: true}, + modifiers: {shift: true}, + time: time, + map: {x: 10, y: 20}, + mapgcs: map.displayToGcs({x: 10, y: 20}, null) + }); + layer._handleMouseClick({ + buttonsDown: {left: true}, + time: time, + map: {x: 30, y: 20}, + mapgcs: map.displayToGcs({x: 30, y: 20}, null) + }); + layer._handleMouseClick({ + buttonsDown: {left: true}, + time: time + 1000, + map: {x: 30, y: 20}, + mapgcs: map.displayToGcs({x: 30, y: 20}, null) + }); + layer._handleMouseClick({ + buttonsDown: {left: true}, + time: time + 1000, + map: {x: 20, y: 50}, + mapgcs: map.displayToGcs({x: 20, y: 50}, null) + }); + layer._handleMouseClick({ + buttonsDown: {left: true}, + time: time + 1000, + map: {x: 20, y: 50}, + mapgcs: map.displayToGcs({x: 20, y: 50}, null) + }); + expect(layer.annotations()[0].options('vertices').length).toBe(3); + expect(layer.annotations()[0].state()).toBe(geo.annotation.state.done); + layer.removeAllAnnotations(); + }); it('_handleMouseOn', function () { var rect = geo.annotation.rectangleAnnotation({ layer: layer, diff --git a/tests/cases/polyops.js b/tests/cases/polyops.js index 8d78541563..284f7f98e2 100644 --- a/tests/cases/polyops.js +++ b/tests/cases/polyops.js @@ -68,6 +68,12 @@ describe('geo.util.polyops', function () { }); }); + describe('toPolygonList', function () { + it('empty list', function () { + expect(geo.util.polyops.toPolygonList([])).toEqual([]); + }); + }); + var opTests = [{ a: [[0, 0], [10, 0], [10, 10], [0, 10]], b: [[5, 0], [15, 0], [15, 5], [5, 5]],