Skip to content

Commit

Permalink
feat: Add controls to combine and change annotations.
Browse files Browse the repository at this point in the history
  • Loading branch information
manthey committed May 26, 2022
1 parent d27d201 commit 73fa35b
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Polygon operations ([#1198](../../pull/1198))
- Use meta keys to modify annotations ([#1209](../../pull/1209))

## Version 1.8.10

Expand Down
6 changes: 6 additions & 0 deletions src/annotation/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

/**
Expand Down
3 changes: 2 additions & 1 deletion src/annotation/polygonAnnotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/annotation/rectangleAnnotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
88 changes: 86 additions & 2 deletions src/annotationLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,44 @@ var annotationLayer = function (arg) {
m_this._updateFromEvent(update);
};

/**
* Check if there is a current boolean operation.
*
* @returns {string?} Either undefined for no current boolean operation or
* the name of the operation.
*/
this.currentBooleanOperation = function () {
let op;
if (m_this._currentBooleanClass) {
op = m_this._currentBooleanClass.split('-')[1];
}
return op;
};

/**
* 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 op = m_this.currentBooleanOperation();
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')) {
const evt = {
annotation: newAnnot,
operation: op
};
m_this.geoTrigger(geo_event.annotation.boolean, evt);
if (evt.cancel !== false) {
util.polyops[op](m_this, newAnnot.toPolygonList(), {correspond: {}, keepAnnotations: 'exact', style: m_this});
}
}
};

/**
* Handle updating the current annotation based on an update state.
*
Expand All @@ -196,6 +234,7 @@ var annotationLayer = function (arg) {
m_this.mode(null);
break;
case 'done':
m_this._handleBooleanOperation();
m_this.mode(null);
break;
}
Expand All @@ -205,6 +244,27 @@ 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' : '');
if (m_this._currentBooleanClass === m_this._booleanClasses[mod]) {
return;
}
m_this._currentBooleanClass = m_this._booleanClasses[mod];
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.
Expand All @@ -213,6 +273,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();
Expand Down Expand Up @@ -316,6 +377,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();
Expand Down Expand Up @@ -518,6 +580,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.
*
Expand All @@ -537,11 +608,15 @@ var annotationLayer = function (arg) {
if (arg === undefined) {
return m_mode;
}
if (arg !== m_mode || (arg === m_this.modes.edit && editAnnotation !== m_this.editAnnotation)) {
if (arg !== m_mode || (arg === m_this.modes.edit && editAnnotation !== m_this.editAnnotation) || (arg !== m_this.modes.edit && arg && !m_this.currentAnnotation)) {
var createAnnotation, actions,
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));
m_this._currentBooleanClass = undefined;
}
if (!m_keyHandler) {
m_keyHandler = Mousetrap(mapNode[0]);
}
Expand Down Expand Up @@ -1199,6 +1274,13 @@ var annotationLayer = function (arg) {
exact = opts.correspond.exact1;
annot = m_this.annotations();
}
if (keep !== 'all' && keep !== 'none') {
annot.forEach((oldAnnot, idx) => {
if (opts.annotationIndices.indexOf(idx) < 0) {
keepIds[oldAnnot.id()] = true;
}
});
}
const polyAnnot = [];
poly.forEach((p, idx) => {
p = p.map((h) => h.map((pt) => ({x: pt[0], y: pt[1]})));
Expand All @@ -1218,7 +1300,9 @@ var annotationLayer = function (arg) {
reusedIds[orig.id()] = true;
}
['name', 'description', 'label'].forEach((k) => {
result[k] = orig[k]();
if (orig[k](undefined, true)) {
result[k] = orig[k](undefined, true);
}
});
Object.entries(orig.options()).forEach(([key, value]) => {
if (['showLabel', 'style'].indexOf(key) >= 0 || key.endsWith('Style')) {
Expand Down
4 changes: 4 additions & 0 deletions src/css/cursor-crosshair-difference.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/css/cursor-crosshair-intersect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/css/cursor-crosshair-union.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/css/cursor-crosshair-xor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -692,4 +692,18 @@ geo_event.annotation.state = 'geo_annotation_state';
*/
geo_event.annotation.mode = 'geo_annotation_mode';

/**
* Triggered when an annotation can be combined via a boolean operation (union,
* intersect, difference, xor).
*
* @event geo.event.annotation.boolean
* @type {geo.event.base}
* @property {geo.annotation} annotation The annotation that is being operated
* on.
* @property {string} operation The operation being performed.
* @property {boolean} [cancel] If the handle sets this to false, don't apply
* the operation to the annotation layer.
*/
geo_event.annotation.boolean = 'geo_annotation_boolean';

module.exports = geo_event;
8 changes: 8 additions & 0 deletions src/main.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/util/polyops.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var geo_map = require('../map');
* applied. If not specified , this is taken from the map second if
* available.
* @property {geo.map} [map] Used for ``ingcs`` and ``gcs`` if needed.
* @property {string} [innerOperation="xor"] one of union, intersect, xor.
* @property {string} [innerOperation="union"] one of union, intersect, xor.
* Used to combine individual polygons in each of ``poly1`` and ``poly2``
* before the main operation is carried out.
* @property {object} [correspond] If present, information about the
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -401,8 +406,8 @@ function generalOperationProcess(op, poly1, poly2, opts) {
}
let seglist1 = poly1.map(p => PolyBool.segments({regions: p}));
let seglist2 = poly2.map(p => PolyBool.segments({regions: p}));
seglist1 = polygonOperationSeglist(opts.innerOperation || 'xor', mode1.epsilon, seglist1);
seglist2 = polygonOperationSeglist(opts.innerOperation || 'xor', mode2.epsilon, seglist2);
seglist1 = polygonOperationSeglist(opts.innerOperation || 'union', mode1.epsilon, seglist1);
seglist2 = polygonOperationSeglist(opts.innerOperation || 'union', mode2.epsilon, seglist2);
let seglist = seglist1;
if (seglist1[0] && seglist2[0]) {
/* We need to do the main operation with the same inversion flags */
Expand Down
41 changes: 41 additions & 0 deletions tests/cases/annotationLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions tests/cases/polyops.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand Down

0 comments on commit 73fa35b

Please sign in to comment.