mapshaper 0.7.51 → 0.7.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +2519 -216
- package/package.json +1 -1
- package/www/mapshaper-gui.js +419 -117
- package/www/mapshaper.js +2519 -216
- package/www/page.css +5 -0
package/www/mapshaper-gui.js
CHANGED
|
@@ -8933,6 +8933,44 @@
|
|
|
8933
8933
|
}
|
|
8934
8934
|
|
|
8935
8935
|
|
|
8936
|
+
// Divide an arc at one or two cut locations, appending the pieces to the end of
|
|
8937
|
+
// the arc collection (the original arc is left in place).
|
|
8938
|
+
// cuts: cut descriptors ordered from the start of the arc, each containing an
|
|
8939
|
+
// arc-relative vertex offset plus source and display coordinates of the cut
|
|
8940
|
+
// location (both null when cutting at an existing vertex).
|
|
8941
|
+
// Returns the ids of the new arcs.
|
|
8942
|
+
function splitArc(lyr, arcId, cuts) {
|
|
8943
|
+
var ids = internal.splitArcAtCuts(lyr.gui.source.dataset.arcs, arcId,
|
|
8944
|
+
cuts.map(function(cut) {
|
|
8945
|
+
return {offset: cut.offset, point: cut.point || null};
|
|
8946
|
+
}));
|
|
8947
|
+
if (isProjectedLayer(lyr)) {
|
|
8948
|
+
internal.splitArcAtCuts(lyr.gui.displayArcs, arcId,
|
|
8949
|
+
cuts.map(function(cut) {
|
|
8950
|
+
return {offset: cut.offset, point: cut.displayPoint || null};
|
|
8951
|
+
}));
|
|
8952
|
+
}
|
|
8953
|
+
return ids;
|
|
8954
|
+
}
|
|
8955
|
+
|
|
8956
|
+
function deleteLastArcs(lyr, count) {
|
|
8957
|
+
for (var i=0; i<count; i++) {
|
|
8958
|
+
internal.deleteLastArc(lyr.gui.source.dataset.arcs);
|
|
8959
|
+
if (isProjectedLayer(lyr)) {
|
|
8960
|
+
internal.deleteLastArc(lyr.gui.displayArcs);
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8965
|
+
// Add a path feature to the end of a layer.
|
|
8966
|
+
// shp: an array of parts; rec: an attribute record, or null for an empty one
|
|
8967
|
+
function appendFeature(lyr, shp, rec) {
|
|
8968
|
+
lyr.shapes.push(shp);
|
|
8969
|
+
if (lyr.data) {
|
|
8970
|
+
lyr.data.getRecords().push(rec || getEmptyDataRecord(lyr.data));
|
|
8971
|
+
}
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8936
8974
|
function deleteLastPath(lyr) {
|
|
8937
8975
|
var arcId = lyr.gui.displayArcs.size() - 1;
|
|
8938
8976
|
if (lyr.data) {
|
|
@@ -12523,6 +12561,60 @@
|
|
|
12523
12561
|
}
|
|
12524
12562
|
}
|
|
12525
12563
|
|
|
12564
|
+
// Divide one path of one feature at one location (open path) or two locations
|
|
12565
|
+
// (ring). See internal.planPathDivision() for the cut format.
|
|
12566
|
+
//
|
|
12567
|
+
// The shorter of the two resulting paths is split off into a new feature at the
|
|
12568
|
+
// end of the layer, with a copy of the original feature's attributes; the
|
|
12569
|
+
// longer one stays in place. For a multipart feature this leaves the longer
|
|
12570
|
+
// path connected to the rest of the feature.
|
|
12571
|
+
//
|
|
12572
|
+
// Returns a descriptor for undoing the edit, or null if the cuts are degenerate.
|
|
12573
|
+
//
|
|
12574
|
+
function snipPath(lyr, fid, partId, cuts) {
|
|
12575
|
+
var arcs = lyr.gui.source.dataset.arcs;
|
|
12576
|
+
var shp = lyr.shapes[fid];
|
|
12577
|
+
var path = shp[partId];
|
|
12578
|
+
var closed = geom.pathIsClosed(path, arcs);
|
|
12579
|
+
var prevShape = shp.map(function(part) { return part.concat(); });
|
|
12580
|
+
var arcCount = arcs.size();
|
|
12581
|
+
var plan = internal.planPathDivision(path, cuts, function(arcId) {
|
|
12582
|
+
return arcs.getVertexData().nn[arcId];
|
|
12583
|
+
}, arcCount, closed);
|
|
12584
|
+
var newShape, keep, remove, rec;
|
|
12585
|
+
if (!plan) return null;
|
|
12586
|
+
plan.splits.forEach(function(split) {
|
|
12587
|
+
splitArc(lyr, split.arcId, split.cuts);
|
|
12588
|
+
});
|
|
12589
|
+
if (internal.getPlanarPathLength(plan.a, arcs) >=
|
|
12590
|
+
internal.getPlanarPathLength(plan.b, arcs)) {
|
|
12591
|
+
keep = plan.a;
|
|
12592
|
+
remove = plan.b;
|
|
12593
|
+
} else {
|
|
12594
|
+
keep = plan.b;
|
|
12595
|
+
remove = plan.a;
|
|
12596
|
+
}
|
|
12597
|
+
newShape = shp.concat();
|
|
12598
|
+
newShape[partId] = keep;
|
|
12599
|
+
lyr.shapes[fid] = newShape;
|
|
12600
|
+
rec = lyr.data ? internal.copyRecord(lyr.data.getRecordAt(fid)) : null;
|
|
12601
|
+
appendFeature(lyr, [remove], rec);
|
|
12602
|
+
return {
|
|
12603
|
+
fid: fid,
|
|
12604
|
+
partId: partId,
|
|
12605
|
+
cuts: cuts,
|
|
12606
|
+
prevShape: prevShape,
|
|
12607
|
+
newFeatureId: lyr.shapes.length - 1,
|
|
12608
|
+
appendedArcCount: arcs.size() - arcCount
|
|
12609
|
+
};
|
|
12610
|
+
}
|
|
12611
|
+
|
|
12612
|
+
function undoSnip(lyr, result) {
|
|
12613
|
+
deleteFeature(lyr, result.newFeatureId);
|
|
12614
|
+
lyr.shapes[result.fid] = result.prevShape;
|
|
12615
|
+
deleteLastArcs(lyr, result.appendedArcCount);
|
|
12616
|
+
}
|
|
12617
|
+
|
|
12526
12618
|
var copyRecord = internal.copyRecord;
|
|
12527
12619
|
|
|
12528
12620
|
function isUndoEvt(e) {
|
|
@@ -12693,6 +12785,24 @@
|
|
|
12693
12785
|
addHistoryState(undo, redo);
|
|
12694
12786
|
});
|
|
12695
12787
|
|
|
12788
|
+
gui.on('snip', function(e) {
|
|
12789
|
+
// read from e.data: the event dispatcher sets e.target to itself, so a
|
|
12790
|
+
// 'target' property in the payload never reaches the handler
|
|
12791
|
+
var target = e.data.target;
|
|
12792
|
+
var result = e.data.result;
|
|
12793
|
+
var undo = function() {
|
|
12794
|
+
undoSnip(target, result);
|
|
12795
|
+
gui.model.updated({arc_count: true});
|
|
12796
|
+
};
|
|
12797
|
+
var redo = function() {
|
|
12798
|
+
// re-snipping restores the same arc ids, because the arcs added by the
|
|
12799
|
+
// previous snip were removed from the end of the collection
|
|
12800
|
+
result = snipPath(target, result.fid, result.partId, result.cuts);
|
|
12801
|
+
gui.model.updated({arc_count: true});
|
|
12802
|
+
};
|
|
12803
|
+
addHistoryState(undo, redo);
|
|
12804
|
+
});
|
|
12805
|
+
|
|
12696
12806
|
gui.on('point_add', function(e) {
|
|
12697
12807
|
var redo = function() {
|
|
12698
12808
|
appendNewPoint(e.data.target, e.p);
|
|
@@ -13174,7 +13284,7 @@
|
|
|
13174
13284
|
empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box', 'ruler'],
|
|
13175
13285
|
polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons', 'ruler'],
|
|
13176
13286
|
rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons', 'ruler'],
|
|
13177
|
-
lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', '
|
|
13287
|
+
lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', 'snip_lines', 'ruler'],
|
|
13178
13288
|
table: ['info', 'selection'],
|
|
13179
13289
|
raster: ['ruler', 'box'],
|
|
13180
13290
|
labels: ['info', 'selection', 'box', 'point_style', 'labels', 'edit_points', 'ruler'],
|
|
@@ -13198,9 +13308,9 @@
|
|
|
13198
13308
|
polygon_style: 'style polygons',
|
|
13199
13309
|
labels: 'position labels',
|
|
13200
13310
|
edit_points: 'add/drag points',
|
|
13201
|
-
edit_lines: 'draw/edit
|
|
13311
|
+
edit_lines: 'draw/edit lines',
|
|
13202
13312
|
edit_polygons: 'draw/edit polygons',
|
|
13203
|
-
snip_lines: 'snip
|
|
13313
|
+
snip_lines: 'snip lines',
|
|
13204
13314
|
vertices: 'edit vertices',
|
|
13205
13315
|
selection: 'selection tool',
|
|
13206
13316
|
ruler: 'measure distance',
|
|
@@ -13278,7 +13388,7 @@
|
|
|
13278
13388
|
};
|
|
13279
13389
|
|
|
13280
13390
|
this.modeSupportsUndo = function(mode) {
|
|
13281
|
-
return ['data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
|
|
13391
|
+
return ['data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'snip_lines', 'vertices', 'rectangles'].includes(mode);
|
|
13282
13392
|
};
|
|
13283
13393
|
|
|
13284
13394
|
this.getMode = getInteractionMode;
|
|
@@ -16807,7 +16917,8 @@
|
|
|
16807
16917
|
test = polygonVertexTest;
|
|
16808
16918
|
} else if (
|
|
16809
16919
|
interactionMode == 'vertices' ||
|
|
16810
|
-
interactionMode == 'edit_lines'
|
|
16920
|
+
interactionMode == 'edit_lines' ||
|
|
16921
|
+
interactionMode == 'snip_lines') {
|
|
16811
16922
|
test = vertexTest;
|
|
16812
16923
|
} else if (geoType == 'polyline') {
|
|
16813
16924
|
test = polylineTest;
|
|
@@ -17332,6 +17443,22 @@
|
|
|
17332
17443
|
triggerChangeEvent();
|
|
17333
17444
|
};
|
|
17334
17445
|
|
|
17446
|
+
// Mark the location of a snip that has been chosen but not yet applied
|
|
17447
|
+
// (the first of the two cuts needed to divide a ring).
|
|
17448
|
+
self.setPendingSnip = function(p) {
|
|
17449
|
+
var p2 = storedData.snip_coordinates;
|
|
17450
|
+
if (!active || !p) return;
|
|
17451
|
+
if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
|
|
17452
|
+
storedData.snip_coordinates = p;
|
|
17453
|
+
triggerChangeEvent();
|
|
17454
|
+
};
|
|
17455
|
+
|
|
17456
|
+
self.clearPendingSnip = function() {
|
|
17457
|
+
if (!storedData.snip_coordinates) return;
|
|
17458
|
+
delete storedData.snip_coordinates;
|
|
17459
|
+
triggerChangeEvent();
|
|
17460
|
+
};
|
|
17461
|
+
|
|
17335
17462
|
self.clearSelection = function() {
|
|
17336
17463
|
updateSelectionState(null);
|
|
17337
17464
|
};
|
|
@@ -17584,6 +17711,11 @@
|
|
|
17584
17711
|
(type == 'hover' || type == 'dblclick')) {
|
|
17585
17712
|
return true; // special case -- using hover for line drawing animation
|
|
17586
17713
|
}
|
|
17714
|
+
if (mode == 'snip_lines' && (type == 'hover' || type == 'click')) {
|
|
17715
|
+
// the snip tool tracks vertices under the pointer, so it needs hover
|
|
17716
|
+
// events as soon as a feature is hit, not one event later
|
|
17717
|
+
return true;
|
|
17718
|
+
}
|
|
17587
17719
|
|
|
17588
17720
|
// ignore pointer events when no features are being hit
|
|
17589
17721
|
// (don't block pan and other navigation when events aren't being used for editing)
|
|
@@ -17601,7 +17733,8 @@
|
|
|
17601
17733
|
}
|
|
17602
17734
|
|
|
17603
17735
|
function possiblyStopPropagation(e) {
|
|
17604
|
-
|
|
17736
|
+
var mode = interactionMode();
|
|
17737
|
+
if (mode == 'edit_lines' || mode == 'edit_polygons' || mode == 'snip_lines') {
|
|
17605
17738
|
// handled conditionally in the control
|
|
17606
17739
|
return;
|
|
17607
17740
|
}
|
|
@@ -19189,6 +19322,7 @@
|
|
|
19189
19322
|
// hoverFill = "rgba(255, 120, 255, 0.12)",
|
|
19190
19323
|
hoverFill = "rgba(0, 0, 0, 0.08)",
|
|
19191
19324
|
grey = "#888",
|
|
19325
|
+
orange = "#f28100",
|
|
19192
19326
|
violet = "#cc6acc",
|
|
19193
19327
|
black = 'black',
|
|
19194
19328
|
violetFill = "rgba(249, 120, 249, 0.25)",
|
|
@@ -19420,20 +19554,30 @@
|
|
|
19420
19554
|
|
|
19421
19555
|
// style for vertex edit mode
|
|
19422
19556
|
function getLineEditingStyle(o) {
|
|
19557
|
+
var isVertex = o.hit_type == 'vertex' || o.hit_type == 'disabled';
|
|
19423
19558
|
return {
|
|
19424
19559
|
ids: o.ids,
|
|
19425
19560
|
overlay: true,
|
|
19426
19561
|
strokeColor: black,
|
|
19427
19562
|
strokeWidth: 1.2,
|
|
19428
19563
|
vertices: true,
|
|
19429
|
-
vertex_overlay_color: o.hit_type
|
|
19430
|
-
vertex_overlay_scale:
|
|
19564
|
+
vertex_overlay_color: getVertexOverlayColor(o.hit_type),
|
|
19565
|
+
vertex_overlay_scale: isVertex ? 2.5 : 2,
|
|
19431
19566
|
vertex_overlay: o.hit_coordinates || null,
|
|
19567
|
+
pending_snip: o.snip_coordinates || null,
|
|
19568
|
+
pending_snip_color: orange,
|
|
19432
19569
|
selected_points: o.selected_points || null,
|
|
19433
19570
|
fillColor: null
|
|
19434
19571
|
};
|
|
19435
19572
|
}
|
|
19436
19573
|
|
|
19574
|
+
function getVertexOverlayColor(hitType) {
|
|
19575
|
+
if (hitType == 'vertex') return violet;
|
|
19576
|
+
// a muted dot marks a vertex that the current tool can not act on
|
|
19577
|
+
if (hitType == 'disabled') return grey;
|
|
19578
|
+
return black;
|
|
19579
|
+
}
|
|
19580
|
+
|
|
19437
19581
|
function getSelectedFeatureStyle(lyr, o, opts) {
|
|
19438
19582
|
var isPinned = o.pinned;
|
|
19439
19583
|
var inSelection = o.ids.indexOf(o.id) > -1;
|
|
@@ -20480,147 +20624,275 @@
|
|
|
20480
20624
|
}
|
|
20481
20625
|
}
|
|
20482
20626
|
|
|
20483
|
-
|
|
20484
|
-
// find the feature part and arc containing the vertex
|
|
20485
|
-
// divide the one feature into two features
|
|
20486
|
-
// divide the arc into two new arcs
|
|
20487
|
-
var arc1, arc2;
|
|
20488
|
-
// divide the feature into two parts
|
|
20489
|
-
}
|
|
20490
|
-
|
|
20491
|
-
function mergeLinesAtVertex(lyr) {
|
|
20492
|
-
|
|
20493
|
-
}
|
|
20494
|
-
|
|
20495
|
-
// TODO: support snipping rings (by snipping in two places)
|
|
20496
|
-
|
|
20497
|
-
|
|
20498
|
-
// pixel distance threshold for hovering near a vertex or segment midpoint
|
|
20627
|
+
// pixel distance threshold for hovering near a vertex or a segment
|
|
20499
20628
|
var HOVER_THRESHOLD = 10;
|
|
20500
20629
|
|
|
20501
20630
|
function initSnipTool(gui, ext, hit) {
|
|
20502
|
-
var
|
|
20503
|
-
var
|
|
20504
|
-
var
|
|
20631
|
+
var hoverInfo = null;
|
|
20632
|
+
var pendingCut = null; // first of the two cuts needed to divide a ring
|
|
20633
|
+
var sessionCount = 0;
|
|
20634
|
+
var alert;
|
|
20635
|
+
|
|
20636
|
+
function active() {
|
|
20637
|
+
return gui.interaction.getMode() == 'snip_lines' && !!hit.getHitTarget();
|
|
20638
|
+
}
|
|
20505
20639
|
|
|
20506
20640
|
gui.on('interaction_mode_change', function(e) {
|
|
20507
|
-
if (
|
|
20641
|
+
if (e.mode == 'snip_lines') {
|
|
20642
|
+
turnOn();
|
|
20643
|
+
} else {
|
|
20508
20644
|
turnOff();
|
|
20509
20645
|
}
|
|
20510
|
-
|
|
20646
|
+
updateCursor();
|
|
20511
20647
|
}, null, 10); // higher priority than hit control, so turnOff() has correct hit target
|
|
20512
20648
|
|
|
20649
|
+
gui.on('undo_redo_pre', function() {
|
|
20650
|
+
clearPendingCut();
|
|
20651
|
+
hoverInfo = null;
|
|
20652
|
+
});
|
|
20653
|
+
|
|
20654
|
+
// A pending cut describes a location in the target layer's current geometry,
|
|
20655
|
+
// and choosing one does not change the model -- so any model update means
|
|
20656
|
+
// something else has happened and the cut is stale.
|
|
20657
|
+
gui.model.on('update', function() {
|
|
20658
|
+
clearPendingCut();
|
|
20659
|
+
hoverInfo = null;
|
|
20660
|
+
});
|
|
20661
|
+
|
|
20662
|
+
function turnOn() {
|
|
20663
|
+
hoverInfo = null;
|
|
20664
|
+
if (sessionCount === 0) {
|
|
20665
|
+
showInstructions();
|
|
20666
|
+
}
|
|
20667
|
+
sessionCount++;
|
|
20668
|
+
}
|
|
20669
|
+
|
|
20670
|
+
function turnOff() {
|
|
20671
|
+
clearPendingCut();
|
|
20672
|
+
clearHoverVertex();
|
|
20673
|
+
hideInstructions();
|
|
20674
|
+
}
|
|
20675
|
+
|
|
20676
|
+
function showInstructions() {
|
|
20677
|
+
var msg = 'Instructions: click a line to snip it apart. Snipping a ring ' +
|
|
20678
|
+
'takes two clicks -- the ring divides at the second one.';
|
|
20679
|
+
alert = showPopupAlert(msg, null, {non_blocking: true, max_width: '350px'});
|
|
20680
|
+
}
|
|
20681
|
+
|
|
20682
|
+
function hideInstructions() {
|
|
20683
|
+
if (!alert) return;
|
|
20684
|
+
alert.close('fade');
|
|
20685
|
+
alert = null;
|
|
20686
|
+
}
|
|
20687
|
+
|
|
20688
|
+
function clearHoverVertex() {
|
|
20689
|
+
hit.clearHoverVertex();
|
|
20690
|
+
hoverInfo = null;
|
|
20691
|
+
}
|
|
20692
|
+
|
|
20693
|
+
function clearPendingCut() {
|
|
20694
|
+
if (!pendingCut) return;
|
|
20695
|
+
pendingCut = null;
|
|
20696
|
+
hit.clearPendingSnip();
|
|
20697
|
+
}
|
|
20698
|
+
|
|
20699
|
+
function setPendingCut(info) {
|
|
20700
|
+
pendingCut = info;
|
|
20701
|
+
hit.setPendingSnip(info.markerPoint);
|
|
20702
|
+
}
|
|
20703
|
+
|
|
20704
|
+
// The pending cut is described in terms of the target layer's current arcs
|
|
20705
|
+
// and feature ids, so it has to be discarded if either could have changed.
|
|
20706
|
+
function pendingCutIsValid() {
|
|
20707
|
+
return !!pendingCut && pendingCut.target === hit.getHitTarget();
|
|
20708
|
+
}
|
|
20513
20709
|
|
|
20514
|
-
// hover event highlights the nearest point in close proximity to the pointer
|
|
20515
|
-
// ... or the closest point along the segment (for adding a new vertex)
|
|
20516
20710
|
hit.on('hover', function(e) {
|
|
20517
20711
|
if (!active()) return;
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
hit.setHoverVertex(hoverVertexInfo.displayPoint, hoverVertexInfo.type);
|
|
20712
|
+
if (pendingCut && !pendingCutIsValid()) {
|
|
20713
|
+
clearPendingCut();
|
|
20714
|
+
}
|
|
20715
|
+
hoverInfo = e.id >= 0 ? findSnipTarget(e) : null;
|
|
20716
|
+
if (hoverInfo) {
|
|
20717
|
+
hit.setHoverVertex(hoverInfo.markerPoint,
|
|
20718
|
+
hoverInfo.disabled ? 'disabled' : hoverInfo.type);
|
|
20526
20719
|
} else {
|
|
20527
|
-
clearHoverVertex();
|
|
20720
|
+
hit.clearHoverVertex();
|
|
20528
20721
|
}
|
|
20529
|
-
|
|
20530
|
-
|
|
20722
|
+
if (pendingCut) {
|
|
20723
|
+
hit.setPendingSnip(pendingCut.markerPoint);
|
|
20724
|
+
}
|
|
20725
|
+
updateCursor();
|
|
20531
20726
|
}, null, 100);
|
|
20532
20727
|
|
|
20533
20728
|
hit.on('click', function(e) {
|
|
20534
|
-
|
|
20535
|
-
var
|
|
20536
|
-
if (
|
|
20537
|
-
|
|
20729
|
+
var info = hoverInfo;
|
|
20730
|
+
var cuts;
|
|
20731
|
+
if (!active() || !info || info.disabled) return;
|
|
20732
|
+
// a snip invalidates the hover data (feature ids and arcs both change), so
|
|
20733
|
+
// clear it -- this also makes the second click of a double-click a no-op
|
|
20734
|
+
hoverInfo = null;
|
|
20735
|
+
if (!info.ring) {
|
|
20736
|
+
clearPendingCut();
|
|
20737
|
+
snip(info, [info.cut]);
|
|
20538
20738
|
return;
|
|
20539
20739
|
}
|
|
20540
|
-
|
|
20541
|
-
|
|
20542
|
-
|
|
20543
|
-
|
|
20740
|
+
if (!pendingCutIsValid() || pendingCut.fid !== info.fid ||
|
|
20741
|
+
pendingCut.partId !== info.partId) {
|
|
20742
|
+
// first cut on this ring: remember it, don't change any geometry
|
|
20743
|
+
setPendingCut(info);
|
|
20744
|
+
return;
|
|
20544
20745
|
}
|
|
20746
|
+
if (cutsAreAdjacent(pendingCut, info)) {
|
|
20747
|
+
clearPendingCut(); // clicking the pending cut again cancels it
|
|
20748
|
+
return;
|
|
20749
|
+
}
|
|
20750
|
+
cuts = [pendingCut.cut, info.cut];
|
|
20751
|
+
clearPendingCut();
|
|
20752
|
+
snip(info, cuts);
|
|
20753
|
+
});
|
|
20545
20754
|
|
|
20546
|
-
|
|
20755
|
+
gui.keyboard.on('keydown', function(e) {
|
|
20756
|
+
if (!active() || !pendingCut || e.keyName != 'esc') return;
|
|
20757
|
+
e.stopPropagation();
|
|
20758
|
+
e.originalEvent.preventDefault();
|
|
20759
|
+
clearPendingCut();
|
|
20760
|
+
}, null, 10);
|
|
20547
20761
|
|
|
20548
|
-
|
|
20762
|
+
function snip(info, cuts) {
|
|
20763
|
+
var target = hit.getHitTarget();
|
|
20764
|
+
var result = snipPath(target, info.fid, info.partId, cuts);
|
|
20765
|
+
if (!result) return;
|
|
20766
|
+
gui.dispatchEvent('snip', {target: target, result: result});
|
|
20767
|
+
hit.clearHoverVertex();
|
|
20768
|
+
hideInstructions();
|
|
20769
|
+
gui.model.updated({arc_count: true}); // update display arcs and redraw
|
|
20770
|
+
}
|
|
20549
20771
|
|
|
20550
|
-
|
|
20772
|
+
// Test if two cuts are close enough on screen to be treated as the same place
|
|
20773
|
+
function cutsAreAdjacent(a, b) {
|
|
20774
|
+
var dist = geom.distance2D(a.markerPoint[0], a.markerPoint[1],
|
|
20775
|
+
b.markerPoint[0], b.markerPoint[1]);
|
|
20776
|
+
return dist / ext.getPixelSize() < HOVER_THRESHOLD;
|
|
20777
|
+
}
|
|
20551
20778
|
|
|
20552
|
-
|
|
20553
|
-
|
|
20554
|
-
|
|
20555
|
-
|
|
20556
|
-
var target = hit.getHitTarget();
|
|
20557
|
-
var shp = target.shapes[e.id];
|
|
20558
|
-
var p = ext.pixCoordsToMapCoords(e.x, e.y);
|
|
20559
|
-
var ids = internal.findNearestVertices(p, shp, target.gui.displayArcs);
|
|
20560
|
-
var p2 = target.gui.displayArcs.getVertex2(ids[0]);
|
|
20561
|
-
var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
|
|
20562
|
-
var pixelDist = dist / ext.getPixelSize();
|
|
20563
|
-
if (pixelDist > HOVER_THRESHOLD) {
|
|
20564
|
-
return null;
|
|
20565
|
-
}
|
|
20566
|
-
var point = getVertexCoords(target, ids[0]); // data coordinates
|
|
20567
|
-
var displayPoint = target.gui.displayArcs.getVertex2(ids[0]);
|
|
20568
|
-
return {target, ids, point, displayPoint, type: 'vertex'};
|
|
20779
|
+
function updateCursor() {
|
|
20780
|
+
var el = gui.container.findChild('.map-layers');
|
|
20781
|
+
el.classed('snip-tool', active());
|
|
20782
|
+
el.classed('no-snip', !!(hoverInfo && hoverInfo.disabled));
|
|
20569
20783
|
}
|
|
20570
20784
|
|
|
20571
|
-
|
|
20785
|
+
// Find the snip location under the pointer: the nearest vertex within the
|
|
20786
|
+
// hover threshold, or failing that the nearest point on a segment.
|
|
20787
|
+
// Returns null if the pointer is not close enough to the hit feature.
|
|
20788
|
+
function findSnipTarget(e) {
|
|
20572
20789
|
var target = hit.getHitTarget();
|
|
20573
|
-
|
|
20574
|
-
|
|
20575
|
-
var
|
|
20576
|
-
var
|
|
20577
|
-
var
|
|
20578
|
-
|
|
20579
|
-
|
|
20580
|
-
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20587
|
-
|
|
20588
|
-
|
|
20589
|
-
|
|
20590
|
-
|
|
20591
|
-
|
|
20592
|
-
|
|
20593
|
-
|
|
20790
|
+
var shp = target && target.shapes[e.id];
|
|
20791
|
+
var arcs = target && target.gui.displayArcs;
|
|
20792
|
+
var bestVertex = null;
|
|
20793
|
+
var bestSegment = null;
|
|
20794
|
+
var p, threshold, data;
|
|
20795
|
+
if (!shp || !arcs) return null;
|
|
20796
|
+
p = ext.pixCoordsToMapCoords(e.x, e.y);
|
|
20797
|
+
threshold = HOVER_THRESHOLD * ext.getPixelSize();
|
|
20798
|
+
data = arcs.getVertexData();
|
|
20799
|
+
|
|
20800
|
+
shp.forEach(function(path, partId) {
|
|
20801
|
+
path.forEach(function(arcId, seq) {
|
|
20802
|
+
var absId = internal.absArcId(arcId);
|
|
20803
|
+
var fwd = arcId >= 0;
|
|
20804
|
+
var start = data.ii[absId];
|
|
20805
|
+
var n = data.nn[absId];
|
|
20806
|
+
var i, v1, v2, dist, p2;
|
|
20807
|
+
if (arcIsOutOfRange(data.bb, absId, p, threshold)) return;
|
|
20808
|
+
for (i=0; i<n; i++) {
|
|
20809
|
+
v1 = fwd ? start + i : start + n - 1 - i;
|
|
20810
|
+
dist = geom.distance2D(p[0], p[1], data.xx[v1], data.yy[v1]);
|
|
20811
|
+
if (dist < threshold && (!bestVertex || dist < bestVertex.dist)) {
|
|
20812
|
+
bestVertex = {partId: partId, seq: seq, offset: i, dist: dist, vid: v1};
|
|
20813
|
+
}
|
|
20814
|
+
if (i === n - 1) break;
|
|
20815
|
+
v2 = fwd ? v1 + 1 : v1 - 1;
|
|
20816
|
+
p2 = internal.findClosestPointOnSeg(p[0], p[1], data.xx[v1],
|
|
20817
|
+
data.yy[v1], data.xx[v2], data.yy[v2], 0);
|
|
20818
|
+
dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
|
|
20819
|
+
if (dist < threshold && (!bestSegment || dist < bestSegment.dist)) {
|
|
20820
|
+
bestSegment = {
|
|
20821
|
+
partId: partId, seq: seq, offset: i, dist: dist, displayPoint: p2,
|
|
20822
|
+
t: getSegmentPosition(data.xx[v1], data.yy[v1], data.xx[v2],
|
|
20823
|
+
data.yy[v2], p2)
|
|
20824
|
+
};
|
|
20825
|
+
}
|
|
20826
|
+
}
|
|
20827
|
+
});
|
|
20594
20828
|
});
|
|
20595
20829
|
|
|
20596
|
-
if (
|
|
20597
|
-
|
|
20598
|
-
|
|
20599
|
-
closest.point = translateDisplayPoint(target, closest.displayPoint);
|
|
20600
|
-
closest.type = 'interpolated';
|
|
20601
|
-
closest.target = target;
|
|
20602
|
-
return closest;
|
|
20830
|
+
if (bestVertex) return getVertexTarget(target, e.id, bestVertex);
|
|
20831
|
+
if (bestSegment) return getSegmentTarget(target, e.id, bestSegment);
|
|
20832
|
+
return null;
|
|
20603
20833
|
}
|
|
20604
20834
|
|
|
20605
|
-
function
|
|
20606
|
-
var
|
|
20607
|
-
return
|
|
20608
|
-
|
|
20835
|
+
function getVertexTarget(target, fid, info) {
|
|
20836
|
+
var displayPoint = target.gui.displayArcs.getVertex2(info.vid);
|
|
20837
|
+
return {
|
|
20838
|
+
target: target,
|
|
20839
|
+
fid: fid,
|
|
20840
|
+
partId: info.partId,
|
|
20841
|
+
type: 'vertex',
|
|
20842
|
+
ring: pathIsRing(target, fid, info.partId),
|
|
20843
|
+
disabled: !pathIsRing(target, fid, info.partId) &&
|
|
20844
|
+
vertexIsPathEndpoint(target, fid, info),
|
|
20845
|
+
markerPoint: displayPoint,
|
|
20846
|
+
cut: {seq: info.seq, offset: info.offset, point: null, displayPoint: null, t: 0}
|
|
20847
|
+
};
|
|
20609
20848
|
}
|
|
20610
20849
|
|
|
20611
|
-
function
|
|
20612
|
-
|
|
20613
|
-
|
|
20850
|
+
function getSegmentTarget(target, fid, info) {
|
|
20851
|
+
return {
|
|
20852
|
+
target: target,
|
|
20853
|
+
fid: fid,
|
|
20854
|
+
partId: info.partId,
|
|
20855
|
+
type: 'interpolated',
|
|
20856
|
+
ring: pathIsRing(target, fid, info.partId),
|
|
20857
|
+
disabled: false,
|
|
20858
|
+
markerPoint: info.displayPoint,
|
|
20859
|
+
cut: {
|
|
20860
|
+
seq: info.seq,
|
|
20861
|
+
offset: info.offset,
|
|
20862
|
+
point: translateDisplayPoint(target, info.displayPoint),
|
|
20863
|
+
displayPoint: info.displayPoint,
|
|
20864
|
+
t: info.t
|
|
20865
|
+
}
|
|
20866
|
+
};
|
|
20614
20867
|
}
|
|
20615
20868
|
|
|
20616
|
-
function
|
|
20617
|
-
return
|
|
20869
|
+
function pathIsRing(target, fid, partId) {
|
|
20870
|
+
return geom.pathIsClosed(target.shapes[fid][partId], target.gui.displayArcs);
|
|
20618
20871
|
}
|
|
20619
20872
|
|
|
20620
|
-
|
|
20621
|
-
|
|
20873
|
+
// Test if a vertex is one of the two endpoints of an open path (as opposed to
|
|
20874
|
+
// an interior node between two of the path's arcs, which is snippable).
|
|
20875
|
+
function vertexIsPathEndpoint(target, fid, info) {
|
|
20876
|
+
var path = target.shapes[fid][info.partId];
|
|
20877
|
+
var data = target.gui.displayArcs.getVertexData();
|
|
20878
|
+
var lastId = internal.absArcId(path[path.length - 1]);
|
|
20879
|
+
if (info.seq === 0 && info.offset === 0) return true;
|
|
20880
|
+
return info.seq === path.length - 1 && info.offset === data.nn[lastId] - 1;
|
|
20622
20881
|
}
|
|
20882
|
+
}
|
|
20883
|
+
|
|
20884
|
+
// Test if an arc's bounding box is too far from a point to contain a hit
|
|
20885
|
+
function arcIsOutOfRange(bb, arcId, p, dist) {
|
|
20886
|
+
var i = arcId * 4;
|
|
20887
|
+
return p[0] + dist < bb[i] || p[0] - dist > bb[i + 2] ||
|
|
20888
|
+
p[1] + dist < bb[i + 1] || p[1] - dist > bb[i + 3];
|
|
20889
|
+
}
|
|
20623
20890
|
|
|
20891
|
+
// Position of point p along the segment from (x1, y1) to (x2, y2), as a
|
|
20892
|
+
// fraction of the segment's length
|
|
20893
|
+
function getSegmentPosition(x1, y1, x2, y2, p) {
|
|
20894
|
+
var len = geom.distance2D(x1, y1, x2, y2);
|
|
20895
|
+
return len > 0 ? geom.distance2D(x1, y1, p[0], p[1]) / len : 0;
|
|
20624
20896
|
}
|
|
20625
20897
|
|
|
20626
20898
|
function initInteractiveEditing(gui, ext, hit) {
|
|
@@ -21822,6 +22094,15 @@
|
|
|
21822
22094
|
_ctx.fill();
|
|
21823
22095
|
_ctx.closePath();
|
|
21824
22096
|
}
|
|
22097
|
+
|
|
22098
|
+
if (style.pending_snip) {
|
|
22099
|
+
_ctx.beginPath();
|
|
22100
|
+
_ctx.fillStyle = style.pending_snip_color || 'black';
|
|
22101
|
+
p = style.pending_snip;
|
|
22102
|
+
drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 2.5, _ctx);
|
|
22103
|
+
_ctx.fill();
|
|
22104
|
+
_ctx.closePath();
|
|
22105
|
+
}
|
|
21825
22106
|
};
|
|
21826
22107
|
|
|
21827
22108
|
// Optimized to draw paths in same-style batches (faster Canvas drawing)
|
|
@@ -22942,9 +23223,11 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
|
|
|
22942
23223
|
}
|
|
22943
23224
|
}
|
|
22944
23225
|
|
|
22945
|
-
function getDistanceDisplay(distanceMeters
|
|
22946
|
-
var unit =
|
|
22947
|
-
var value = unit == 'km'
|
|
23226
|
+
function getDistanceDisplay(distanceMeters) {
|
|
23227
|
+
var unit = getDistanceUnit(distanceMeters);
|
|
23228
|
+
var value = unit == 'km' && distanceMeters / 1000 ||
|
|
23229
|
+
unit == 'm' && distanceMeters || unit == 'cm' && distanceMeters * 100 ||
|
|
23230
|
+
unit == 'mm' && distanceMeters * 1000 || 0;
|
|
22948
23231
|
return {
|
|
22949
23232
|
value,
|
|
22950
23233
|
unit,
|
|
@@ -22958,7 +23241,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
|
|
|
22958
23241
|
}
|
|
22959
23242
|
|
|
22960
23243
|
function getDistanceUnit(distanceMeters) {
|
|
22961
|
-
return distanceMeters >= 1000
|
|
23244
|
+
return distanceMeters >= 1000 && 'km' ||
|
|
23245
|
+
distanceMeters >= 0.1 && 'm' || distanceMeters >= 0.01 && 'cm' ||
|
|
23246
|
+
distanceMeters > 0 && 'mm' || 'm';
|
|
22962
23247
|
}
|
|
22963
23248
|
|
|
22964
23249
|
function pointIsInLngLatRange(p) {
|
|
@@ -23002,7 +23287,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
|
|
|
23002
23287
|
if (value >= 100) return 0;
|
|
23003
23288
|
if (value >= 10) return 1;
|
|
23004
23289
|
if (value >= 1) return 2;
|
|
23005
|
-
return 3;
|
|
23290
|
+
if (value > 0) return 3;
|
|
23291
|
+
return 1;
|
|
23006
23292
|
}
|
|
23007
23293
|
|
|
23008
23294
|
function lngLatToUnitVector(lng, lat) {
|
|
@@ -23480,11 +23766,10 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
|
|
|
23480
23766
|
var projectedMeters = isLatLng ? null : getProjectedPathDistanceMeters(path);
|
|
23481
23767
|
var geographicMeters = getGeographicPathDistanceMeters(path);
|
|
23482
23768
|
var displayedMeters = geographicMeters !== null ? geographicMeters : projectedMeters;
|
|
23483
|
-
var unit = getDistanceUnit(displayedMeters || 0);
|
|
23484
23769
|
return {
|
|
23485
23770
|
projectedMeters,
|
|
23486
23771
|
geographicMeters,
|
|
23487
|
-
distanceLabel: displayedMeters === null ? null : getDistanceDisplay(displayedMeters
|
|
23772
|
+
distanceLabel: displayedMeters === null ? null : getDistanceDisplay(displayedMeters).label
|
|
23488
23773
|
};
|
|
23489
23774
|
}
|
|
23490
23775
|
|
|
@@ -25414,6 +25699,23 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
|
|
|
25414
25699
|
appendNewPointForTest(target.layer, p);
|
|
25415
25700
|
gui.dispatchEvent('point_add', {target: target.layer, p: p});
|
|
25416
25701
|
gui.dispatchEvent('map-needs-refresh');
|
|
25702
|
+
},
|
|
25703
|
+
// Perform the same edit as the snip tool, without simulating the pointer
|
|
25704
|
+
// events that choose the cut locations.
|
|
25705
|
+
snipActiveLayerPath: function(fid, partId, cuts) {
|
|
25706
|
+
var target = gui.model.getActiveLayer();
|
|
25707
|
+
var lyr = target && target.layer;
|
|
25708
|
+
var result;
|
|
25709
|
+
if (!lyr || lyr.geometry_type != 'polyline') {
|
|
25710
|
+
throw new Error('Active layer is not a polyline layer');
|
|
25711
|
+
}
|
|
25712
|
+
result = snipPath(lyr, fid, partId, cuts);
|
|
25713
|
+
if (!result) {
|
|
25714
|
+
throw new Error('Snip was rejected');
|
|
25715
|
+
}
|
|
25716
|
+
gui.dispatchEvent('snip', {target: lyr, result: result});
|
|
25717
|
+
gui.model.updated({arc_count: true});
|
|
25718
|
+
return result;
|
|
25417
25719
|
}
|
|
25418
25720
|
};
|
|
25419
25721
|
}
|