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.
@@ -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', 'ruler'], // 'snip_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 polylines',
13311
+ edit_lines: 'draw/edit lines',
13202
13312
  edit_polygons: 'draw/edit polygons',
13203
- snip_lines: 'snip polylines',
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
- if (interactionMode() == 'edit_lines' || interactionMode() == 'edit_polygons') {
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 == 'vertex' ? violet : black,
19430
- vertex_overlay_scale: o.hit_type == 'vertex' ? 2.5 : 2,
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
- function snipLineAtVertex(lyr, fid, vid) {
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 _active = true;
20503
- var hoverVertexInfo;
20504
- var prevHoverEvent;
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 (active()) {
20641
+ if (e.mode == 'snip_lines') {
20642
+ turnOn();
20643
+ } else {
20508
20644
  turnOff();
20509
20645
  }
20510
- // updateCursor();
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
- // highlight nearby snappable vertex (the closest vertex on a nearby line,
20520
- // or the first vertex of the current drawing path if not near a line)
20521
- hoverVertexInfo = e.id >= 0 && findDraggableVertices(e) ||
20522
- e.id >= 0 && findInterpolatedPoint(e);
20523
- if (hoverVertexInfo) {
20524
- // hovering near a vertex: highlight the vertex
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
- // updateCursor();
20530
- prevHoverEvent = e;
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
- if (!active() || !hoverVertexInfo) return;
20535
- var target = hit.getHitTarget();
20536
- if (vertexIsEndpoint(hoverVertexInfo, target)) {
20537
- // TODO: don't allow hovering on endpoints
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
- if (hoverVertexInfo.type == 'interpolated') {
20542
- insertVertex$1(target, hoverVertexInfo.i, hoverVertexInfo.point);
20543
- hoverVertexInfo.ids = [hoverVertexInfo.i];
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
- snipLineAtVertex(target, e.id, hoverVertexInfo.ids[0]);
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
- hit.setHoverVertex(hoverVertexInfo.displayPoint, hoverVertexInfo.type);
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
- // return data on the nearest vertex (or identical vertices) to the pointer
20553
- // (if within a distance threshold)
20554
- //
20555
- function findDraggableVertices(e) {
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
- function findInterpolatedPoint(e) {
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
- //// vertex insertion not supported with simplification
20574
- // if (!target.arcs.isFlat()) return null;
20575
- var p = ext.pixCoordsToMapCoords(e.x, e.y);
20576
- var minDist = Infinity;
20577
- var shp = target.shapes[e.id];
20578
- var closest;
20579
- internal.forEachSegmentInShape(shp, target.gui.displayArcs, function(i, j, xx, yy) {
20580
- var x1 = xx[i],
20581
- y1 = yy[i],
20582
- x2 = xx[j],
20583
- y2 = yy[j],
20584
- p2 = internal.findClosestPointOnSeg(p[0], p[1], x1, y1, x2, y2, 0),
20585
- dist = geom.distance2D(p2[0], p2[1], p[0], p[1]);
20586
- if (dist < minDist) {
20587
- minDist = dist;
20588
- closest = {
20589
- i: (i < j ? i : j) + 1, // insertion vertex id
20590
- displayPoint: p2,
20591
- distance: dist
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 (closest.distance / ext.getPixelSize() > HOVER_THRESHOLD) {
20597
- return null;
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 vertexIsEndpoint(info, target) {
20606
- var vId = info.ids[0];
20607
- return internal.vertexIsArcStart(vId, target.gui.displayArcs) ||
20608
- internal.vertexIsArcEnd(vId, target.gui.displayArcs);
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 clearHoverVertex() {
20612
- hit.clearHoverVertex();
20613
- hoverVertexInfo = null;
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 active() {
20617
- return _active && gui.interaction.getMode() == 'snip_lines';
20869
+ function pathIsRing(target, fid, partId) {
20870
+ return geom.pathIsClosed(target.shapes[fid][partId], target.gui.displayArcs);
20618
20871
  }
20619
20872
 
20620
- function turnOff() {
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, unitHint) {
22946
- var unit = unitHint || (distanceMeters >= 1000 ? 'km' : 'm');
22947
- var value = unit == 'km' ? distanceMeters / 1000 : distanceMeters;
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 ? 'km' : 'm';
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, unit).label
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
  }