Skip to content

Commit 3517034

Browse files
committed
feat: Add the ability to cut polygonal annotations with a line
Maybe we shouldn't be merging AS we are dividing. - We still need to cut line annotations. - We should only split polygons that the line actually touches or is entirely within; extending it is surprising.
1 parent 51eca9e commit 3517034

File tree

6 files changed

+187
-16
lines changed

6 files changed

+187
-16
lines changed

src/annotation/lineAnnotation.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ lineAnnotation.defaults = Object.assign({}, annotation.defaults, {
343343
closed: false,
344344
lineCap: 'butt',
345345
lineJoin: 'miter'
346-
}
346+
},
347+
allowBooleanOperations: ['annotation-cut']
347348
});
348349

349350
var lineRequiredFeatures = {};

src/annotationLayer.js

Lines changed: 164 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var transform = require('./transform');
77
var $ = require('jquery');
88
var Mousetrap = require('mousetrap');
99
var textFeature = require('./textFeature');
10+
const lineAnnotation = require('./annotation/lineAnnotation');
1011

1112
/**
1213
* Object specification for an annotation layer.
@@ -223,7 +224,7 @@ var annotationLayer = function (arg) {
223224
*/
224225
this._handleBooleanOperation = function () {
225226
const op = m_this.currentBooleanOperation();
226-
if (!op || !m_this.currentAnnotation || !m_this.currentAnnotation.toPolygonList) {
227+
if (!op || !m_this.currentAnnotation || (op !== 'cut' && !m_this.currentAnnotation.toPolygonList) || (op === 'cut' && !(m_this.currentAnnotation instanceof lineAnnotation))) {
227228
return;
228229
}
229230
const newAnnot = m_this.currentAnnotation;
@@ -235,11 +236,153 @@ var annotationLayer = function (arg) {
235236
};
236237
m_this.geoTrigger(geo_event.annotation.boolean, evt);
237238
if (evt.cancel !== false) {
238-
util.polyops[op](m_this, newAnnot.toPolygonList(), {correspond: {}, keepAnnotations: 'exact', style: m_this});
239+
if (op !== 'cut') {
240+
util.polyops[op](m_this, newAnnot.toPolygonList(), {correspond: {}, keepAnnotations: 'exact', style: m_this});
241+
} else {
242+
m_this._cutOperation(newAnnot);
243+
}
239244
}
240245
}
241246
};
242247

248+
this._extendLine = function (p1, p2, bbox) {
249+
const dx = p2[0] - p1[0];
250+
const dy = p2[1] - p1[1];
251+
if (!dx && !dy) {
252+
return p1;
253+
}
254+
let best;
255+
let t = ((dx > 0 ? bbox.min.x : bbox.max.x) - p1[0]) / (dx || 1);
256+
if (t < 0) {
257+
const y = p1[1] + t * dy;
258+
if (y >= bbox.min.y && y <= bbox.max.y) {
259+
best = [dx > 0 ? bbox.min.x : bbox.max.x, y];
260+
}
261+
}
262+
t = ((dy > 0 ? bbox.min.y : bbox.max.y) - p1[1]) / (dy || 1);
263+
if (t < 0) {
264+
const x = p1[0] + t * dx;
265+
if (x >= bbox.min.x && x <= bbox.max.x && (!best || t < (bbox.min.x - p1[0]) / dx)) {
266+
best = [x, dy > 0 ? bbox.min.y : bbox.max.y];
267+
}
268+
}
269+
return best;
270+
};
271+
272+
/**
273+
* Given a cut line, cut existing polygons and lines.
274+
*
275+
* @param {geo.annotation} cutLine The line to use to cut the existing
276+
* annotations.
277+
*/
278+
this._cutOperation = function (cutLine) {
279+
const cutPts = cutLine.coordinates(null).map((p) => [p.x, p.y]);
280+
let range;
281+
for (let p = 0; p < cutPts.length; p += 1) {
282+
const x = cutPts[p][0];
283+
const y = cutPts[p][1];
284+
if (!p) {
285+
range = {min: {x: x, y: y}, max: {x: x, y: y}};
286+
}
287+
if (x < range.min.x) { range.min.x = x; }
288+
if (y < range.min.y) { range.min.y = y; }
289+
if (x > range.max.x) { range.max.x = x; }
290+
if (y > range.max.y) { range.max.y = y; }
291+
}
292+
const polylist = m_this.toPolygonList();
293+
for (let poly = 0; poly < polylist.length; poly += 1) {
294+
for (let h = 0; h < polylist[poly].length; h += 1) {
295+
for (let p = 0; p < polylist[poly][h].length; p += 1) {
296+
const x = polylist[poly][h][p][0];
297+
const y = polylist[poly][h][p][1];
298+
if (x < range.min.x) { range.min.x = x; }
299+
if (y < range.min.y) { range.min.y = y; }
300+
if (x > range.max.x) { range.max.x = x; }
301+
if (y > range.max.y) { range.max.y = y; }
302+
}
303+
}
304+
}
305+
m_this.annotations().forEach((annot) => {
306+
if (annot instanceof lineAnnotation) {
307+
const pts = annot.coordinates(null);
308+
for (let p = 0; p < pts.length; p += 1) {
309+
const x = pts[p].x;
310+
const y = pts[p].y;
311+
if (x < range.min.x) { range.min.x = x; }
312+
if (y < range.min.y) { range.min.y = y; }
313+
if (x > range.max.x) { range.max.x = x; }
314+
if (y > range.max.y) { range.max.y = y; }
315+
}
316+
}
317+
});
318+
if (range === undefined || range.min.x === range.max.x || range.min.y === range.max.y) {
319+
return;
320+
}
321+
// expand the range so that all polygons and lines, including our cut line
322+
// are guaranteed to be inside the bounding box.
323+
range = {min: {
324+
x: range.min.x - (range.max.x - range.min.x) * 0.01,
325+
y: range.min.y - (range.max.y - range.min.y) * 0.01
326+
},
327+
max: {
328+
x: range.max.x + (range.max.x - range.min.x) * 0.01,
329+
y: range.max.y + (range.max.y - range.min.y) * 0.01
330+
}};
331+
// we convert our line annotation so it expands past our bounding box, then
332+
// close it on the left / top. Our polygons will be the set that is cut
333+
// and the set that is union with this.
334+
cutPts[0] = m_this._extendLine(cutPts[0], cutPts[1], range);
335+
cutPts[cutPts.length - 1] = m_this._extendLine(cutPts[cutPts.length - 1], cutPts[cutPts.length - 2], range);
336+
const cutPoly = cutPts.slice();
337+
const corners = [[range.min.x, range.min.y], [range.max.x, range.min.y], [range.max.x, range.max.y], [range.min.x, range.max.y]];
338+
const n = cutPoly.length - 1;
339+
const idx0 = cutPoly[n][0] === range.min.x ? 0 : (cutPoly[n][1] === range.min.y ? 1 : (cutPoly[n][0] === range.max.x ? 2 : 3));
340+
const idx1 = cutPoly[0][0] === range.min.x ? 0 : (cutPoly[0][1] === range.min.y ? 1 : (cutPoly[0][0] === range.max.x ? 2 : 3));
341+
for (let idx = idx0; idx % 4 !== idx1; idx += 1) {
342+
cutPoly.push(corners[idx % 4]);
343+
}
344+
// mimic some of what is done in fromPolygonList because we need both sides
345+
// of the cut.
346+
let diffPoly;
347+
const annot = m_this.annotations();
348+
const diff = {poly2: [[cutPoly]], correspond: {}, keepAnnotations: 'exact', style: {fromPolygonList: (poly, opts) => { diffPoly = poly; }}};
349+
diff.poly1 = m_this.toPolygonList(diff);
350+
util.polyops.difference(diff);
351+
util.polyops.intersect(m_this, [[cutPoly]], {correspond: {}, keepAnnotations: 'exact', style: m_this});
352+
const indices = (diff.annotationIndices || {})[m_this.id()];
353+
const correspond = diff.correspond.poly1;
354+
const exact = diff.correspond.exact1;
355+
diffPoly.forEach((p, idx) => {
356+
p = p.map((h) => h.map((pt) => ({x: pt[0], y: pt[1]})));
357+
const result = {
358+
vertices: p.length === 1 ? p[0] : {outer: p[0], inner: p.slice(1)}
359+
};
360+
for (let i = 0; i < correspond.length; i += 1) {
361+
if (correspond[i] && correspond[i].indexOf(idx) >= 0) {
362+
const orig = annot[indices[i]];
363+
if (exact[i] && exact[i].indexOf(idx) >= 0) {
364+
m_this.addAnnotation(orig, m_this.map().gcs(), false);
365+
return;
366+
}
367+
['name', 'description', 'label'].forEach((k) => {
368+
if (orig[k](undefined, true)) {
369+
result[k] = orig[k](undefined, true);
370+
}
371+
});
372+
Object.entries(orig.options()).forEach(([key, value]) => {
373+
if (['showLabel', 'style'].indexOf(key) >= 0 || key.endsWith('Style')) {
374+
result[key] = value;
375+
}
376+
});
377+
m_this.addAnnotation(registry.createAnnotation('polygon', result), m_this.map().gcs(), false);
378+
return;
379+
}
380+
}
381+
m_this.addAnnotation(registry.createAnnotation('polygon', result), m_this.map().gcs(), false);
382+
});
383+
// TODO: cut lines
384+
};
385+
243386
/**
244387
* Handle updating the current annotation based on an update state.
245388
*
@@ -271,16 +414,22 @@ var annotationLayer = function (arg) {
271414
* @param {geo.event} evt The mouse move or click event.
272415
*/
273416
this._handleMouseMoveModifiers = function (evt) {
274-
if (m_this.mode() !== m_this.modes.edit && m_this.currentAnnotation.options('allowBooleanOperations') && (m_this.currentAnnotation._coordinates().length < 2 || m_this.mode() === m_this.modes.cursor)) {
417+
const ops = m_this.currentAnnotation.options('allowBooleanOperations');
418+
if (m_this.mode() !== m_this.modes.edit && ops && (m_this.currentAnnotation._coordinates().length < 2 || m_this.mode() === m_this.modes.cursor)) {
275419
if (evt.modifiers) {
276420
const mod = (evt.modifiers.shift ? 's' : '') + (evt.modifiers.ctrl ? 'c' : '') + (evt.modifiers.meta || evt.modifiers.alt ? 'a' : '');
277-
if (m_this._currentBooleanClass === m_this._booleanClasses[mod]) {
421+
if (mod === '' && !m_this._currentBooleanClass) {
422+
return;
423+
}
424+
const op = Object.keys(m_this._booleanClasses).find((op) =>
425+
m_this._booleanClasses[op] === mod && (ops === true || ops.includes(op)));
426+
if (m_this._currentBooleanClass === op) {
278427
return;
279428
}
280-
m_this._currentBooleanClass = m_this._booleanClasses[mod];
429+
m_this._currentBooleanClass = op;
281430
const mapNode = m_this.map().node();
282-
Object.values(m_this._booleanClasses).forEach((c) => {
283-
mapNode.toggleClass(c, m_this._booleanClasses[mod] === c);
431+
Object.keys(m_this._booleanClasses).forEach((c) => {
432+
mapNode.toggleClass(c, op === c);
284433
});
285434
}
286435
}
@@ -655,13 +804,14 @@ var annotationLayer = function (arg) {
655804
cursor: 'cursor'
656805
};
657806

658-
/* Keys are short-hand for preferred event modifiers. Values are classes to
659-
* apply to the map node. */
807+
/* Keys are classes to apply to the map node. Values are short-hand for
808+
* preferred event modifiers. */
660809
this._booleanClasses = {
661-
s: 'annotation-union',
662-
sc: 'annotation-intersect',
663-
c: 'annotation-difference',
664-
sa: 'annotation-xor'
810+
'annotation-union': 's',
811+
'annotation-intersect': 'sc',
812+
'annotation-difference': 'c',
813+
'annotation-xor': 'sa',
814+
'annotation-cut': 'c'
665815
};
666816

667817
/**
@@ -696,7 +846,7 @@ var annotationLayer = function (arg) {
696846
m_mode = arg;
697847
mapNode.toggleClass('annotation-input', !!(m_mode && m_mode !== m_this.modes.edit && m_mode !== m_this.modes.cursor));
698848
if (!m_mode || m_mode === m_this.modes.edit) {
699-
Object.values(m_this._booleanClasses).forEach((c) => mapNode.toggleClass(c, false));
849+
Object.keys(m_this._booleanClasses).forEach((c) => mapNode.toggleClass(c, false));
700850
m_this._currentBooleanClass = undefined;
701851
}
702852
if (!m_keyHandler) {

src/css/cursor-crosshair-cut.svg

Lines changed: 17 additions & 0 deletions
Loading

src/event.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ geo_event.annotation.mode = 'geo_annotation_mode';
764764

765765
/**
766766
* Triggered when an annotation can be combined via a boolean operation (union,
767-
* intersect, difference, xor).
767+
* intersect, difference, xor, cut).
768768
*
769769
* @event geo.event.annotation.boolean
770770
* @type {geo.event.base}

src/main.styl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
cursor embedurl("./css/cursor-crosshair-union.svg") 12 12,crosshair
5858
&.annotation-xor
5959
cursor embedurl("./css/cursor-crosshair-xor.svg") 12 12,crosshair
60+
&.annotation-cut
61+
cursor embedurl("./css/cursor-crosshair-cut.svg") 12 12,crosshair
6062

6163
&.annotation-cursor
6264
cursor crosshair

src/util/polyops.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const AlternateOpNames = {
7474
'&': 'intersect',
7575
mul: 'intersect',
7676
multiply: 'intersect',
77+
intersection: 'intersect',
7778
x: 'xor',
7879
'^': 'xor'
7980
};

0 commit comments

Comments
 (0)