mapshaper 0.7.51 → 0.7.52

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 CHANGED
@@ -75897,7 +75897,7 @@ ${svg}
75897
75897
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
75898
75898
  }
75899
75899
 
75900
- var version = "0.7.51";
75900
+ var version = "0.7.52";
75901
75901
 
75902
75902
  // Parse command line args into commands and run them
75903
75903
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -76559,6 +76559,256 @@ ${svg}
76559
76559
  return false;
76560
76560
  }
76561
76561
 
76562
+ // Utilities for dividing a path at one or two points ("snipping").
76563
+ //
76564
+ // The new arcs are appended to the end of the collection; the original arc is
76565
+ // left in place (it may still be referenced by other shapes). Callers are
76566
+ // responsible for updating the shapes that should use the new arcs.
76567
+
76568
+ // Divide an arc's vertices into slices at one or two cut locations.
76569
+ // n: number of vertices in the arc
76570
+ // cuts: one or two cut objects, ordered from the start of the arc:
76571
+ // {offset: i, point: [x, y]} cuts the segment between vertices i and i + 1
76572
+ // {offset: i, point: null} cuts at vertex i (which is duplicated, so that
76573
+ // both adjacent slices contain it)
76574
+ // Returns slice descriptors {start, end, prefix, suffix}, where start and end
76575
+ // are an inclusive vertex range (empty if start > end) and prefix/suffix are
76576
+ // points inserted at a cut location.
76577
+ function getArcSlices(n, cuts) {
76578
+ var slices = [];
76579
+ var start = 0;
76580
+ var prefix = null;
76581
+ cuts.forEach(function(cut) {
76582
+ var i = cut.offset;
76583
+ if (cut.point) {
76584
+ if (i < 0 || i > n - 2) error('Out-of-range arc cut offset:', i);
76585
+ slices.push({start: start, end: i, prefix: prefix, suffix: cut.point});
76586
+ prefix = cut.point;
76587
+ start = i + 1;
76588
+ } else {
76589
+ if (i < 1 || i > n - 2) error('Out-of-range arc cut offset:', i);
76590
+ slices.push({start: start, end: i, prefix: prefix, suffix: null});
76591
+ prefix = null;
76592
+ start = i;
76593
+ }
76594
+ });
76595
+ slices.push({start: start, end: n - 1, prefix: prefix, suffix: null});
76596
+ return slices;
76597
+ }
76598
+
76599
+ function getArcSliceLength(slice) {
76600
+ var n = slice.end >= slice.start ? slice.end - slice.start + 1 : 0;
76601
+ if (slice.prefix) n++;
76602
+ if (slice.suffix) n++;
76603
+ return n;
76604
+ }
76605
+
76606
+ // Append the slices of a divided arc to an ArcCollection.
76607
+ // Returns the ids of the new arcs, ordered from the start of the original arc.
76608
+ function splitArcAtCuts(arcs, arcId, cuts) {
76609
+ var data = arcs.getVertexData();
76610
+ var zlimit = arcs.getRetainedInterval();
76611
+ var arcStart = data.ii[arcId];
76612
+ var arcCount = data.nn.length;
76613
+ var slices = getArcSlices(data.nn[arcId], cuts);
76614
+ var pointCount = data.xx.length;
76615
+ var added = slices.reduce(function(memo, slice) {
76616
+ return memo + getArcSliceLength(slice);
76617
+ }, 0);
76618
+ var xx2 = new Float64Array(pointCount + added);
76619
+ var yy2 = new Float64Array(pointCount + added);
76620
+ var zz2 = arcs.isFlat() ? null : new Float64Array(pointCount + added);
76621
+ var nn2 = new Uint32Array(arcCount + slices.length);
76622
+ var i = pointCount;
76623
+ var ids = [];
76624
+
76625
+ xx2.set(data.xx);
76626
+ yy2.set(data.yy);
76627
+ nn2.set(data.nn);
76628
+ if (zz2) zz2.set(data.zz);
76629
+
76630
+ slices.forEach(function(slice, sliceId) {
76631
+ var first = i;
76632
+ var j;
76633
+ if (slice.prefix) {
76634
+ xx2[i] = slice.prefix[0];
76635
+ yy2[i] = slice.prefix[1];
76636
+ i++;
76637
+ }
76638
+ for (j = slice.start; j <= slice.end; j++) {
76639
+ xx2[i] = data.xx[arcStart + j];
76640
+ yy2[i] = data.yy[arcStart + j];
76641
+ if (zz2) zz2[i] = data.zz[arcStart + j];
76642
+ i++;
76643
+ }
76644
+ if (slice.suffix) {
76645
+ xx2[i] = slice.suffix[0];
76646
+ yy2[i] = slice.suffix[1];
76647
+ i++;
76648
+ }
76649
+ if (zz2) {
76650
+ // arc endpoints must never be removed by simplification
76651
+ zz2[first] = Infinity;
76652
+ zz2[i - 1] = Infinity;
76653
+ }
76654
+ nn2[arcCount + sliceId] = i - first;
76655
+ ids.push(arcCount + sliceId);
76656
+ });
76657
+
76658
+ arcs.updateVertexData(nn2, xx2, yy2, zz2);
76659
+ arcs.setRetainedInterval(zlimit);
76660
+ return ids;
76661
+ }
76662
+
76663
+ // A cut identifies one location along a path (an array of arc ids):
76664
+ // seq: index into the path of the arc containing the cut
76665
+ // offset: vertex index within that arc, in traversal orientation
76666
+ // point: coords of the cut, or null to cut at vertex <offset>
76667
+ // displayPoint: coords of the cut in the GUI's display CRS, or null
76668
+ // t: position along the segment (0-1), used to order two cuts
76669
+ // that land on the same segment
76670
+
76671
+ // Work out how to divide a path at one cut (open path) or two cuts (ring),
76672
+ // without mutating anything.
76673
+ //
76674
+ // path: array of arc ids
76675
+ // cutsArg: one cut (open path) or two cuts (ring), in any order
76676
+ // arcLen: function(arcId) returning the number of vertices in an arc
76677
+ // baseId: arcs.size(), used to predict the ids of arcs that will be appended
76678
+ // closed: whether the path is a ring
76679
+ //
76680
+ // Returns {splits, a, b}, where splits are arc divisions to perform in order
76681
+ // and a and b are the two resulting paths, or null if the cuts are degenerate
76682
+ // (at the endpoint of an open path, or two cuts in the same place on a ring).
76683
+ //
76684
+ function planPathDivision(path, cutsArg, arcLen, baseId, closed) {
76685
+ var cuts = sortCuts(cutsArg);
76686
+ var boundaryCuts = []; // cuts that fall between two arcs
76687
+ var interiorCuts = {}; // cuts that fall inside an arc, by path index
76688
+ var expanded = []; // path with divided arcs replaced by their pieces
76689
+ var positions = []; // position of each cut within the expanded path
76690
+ var splits = [];
76691
+ var nextArcId = baseId;
76692
+ var a, b, q, q1, q2;
76693
+
76694
+ if (cuts.length != (closed ? 2 : 1)) return null;
76695
+
76696
+ cuts.forEach(function(cut, cutId) {
76697
+ var n = arcLen(absArcId(path[cut.seq]));
76698
+ var seq;
76699
+ if (!cut.point && (cut.offset === 0 || cut.offset === n - 1)) {
76700
+ // a cut at an arc endpoint doesn't divide the arc, it divides the
76701
+ // sequence of arcs
76702
+ seq = cut.offset === 0 ? cut.seq : cut.seq + 1;
76703
+ if (closed && seq === path.length) seq = 0; // a ring's seam is one place
76704
+ boundaryCuts.push({cutId: cutId, seq: seq});
76705
+ } else {
76706
+ if (!interiorCuts[cut.seq]) interiorCuts[cut.seq] = [];
76707
+ interiorCuts[cut.seq].push({cutId: cutId, cut: cut, arcLen: n});
76708
+ }
76709
+ });
76710
+
76711
+ for (var k=0; k<=path.length; k++) {
76712
+ boundaryCuts.forEach(function(item) {
76713
+ if (item.seq === k) positions[item.cutId] = expanded.length;
76714
+ });
76715
+ if (k === path.length) break;
76716
+ if (!interiorCuts[k]) {
76717
+ expanded.push(path[k]);
76718
+ continue;
76719
+ }
76720
+ addDividedArc(interiorCuts[k], path[k]);
76721
+ }
76722
+
76723
+ if (!closed) {
76724
+ q = positions[0];
76725
+ if (q === 0 || q === expanded.length) return null; // cut at a path endpoint
76726
+ a = expanded.slice(0, q);
76727
+ b = expanded.slice(q);
76728
+ } else {
76729
+ q1 = Math.min(positions[0], positions[1]);
76730
+ q2 = Math.max(positions[0], positions[1]);
76731
+ if (q1 === q2) return null; // both cuts in the same place
76732
+ a = expanded.slice(q1, q2);
76733
+ b = expanded.slice(q2).concat(expanded.slice(0, q1));
76734
+ }
76735
+ return {splits: splits, a: a, b: b};
76736
+
76737
+ function addDividedArc(group, arcId) {
76738
+ var fwd = arcId >= 0;
76739
+ var n = group[0].arcLen;
76740
+ // convert traversal offsets to forward-orientation offsets
76741
+ var fwdCuts = group.map(function(item) {
76742
+ return {
76743
+ offset: fwd ? item.cut.offset :
76744
+ n - (item.cut.point ? 2 : 1) - item.cut.offset,
76745
+ point: item.cut.point || null,
76746
+ displayPoint: item.cut.displayPoint || null
76747
+ };
76748
+ });
76749
+ // a reversed arc is traversed from its last vertex, so traversal order is
76750
+ // the reverse of forward order
76751
+ var pieces = getSplitArcIds(nextArcId, group.length, fwd);
76752
+ splits.push({
76753
+ arcId: absArcId(arcId),
76754
+ cuts: fwd ? fwdCuts : fwdCuts.concat().reverse()
76755
+ });
76756
+ nextArcId += group.length + 1;
76757
+ pieces.forEach(function(id, i) {
76758
+ if (i > 0) positions[group[i - 1].cutId] = expanded.length;
76759
+ expanded.push(id);
76760
+ });
76761
+ }
76762
+ }
76763
+
76764
+ // Ids of the pieces of a divided arc, in traversal order
76765
+ function getSplitArcIds(baseId, cutCount, fwd) {
76766
+ var ids = [];
76767
+ for (var i=0; i<=cutCount; i++) {
76768
+ ids.push(fwd ? baseId + i : ~(baseId + cutCount - i));
76769
+ }
76770
+ return ids;
76771
+ }
76772
+
76773
+ function sortCuts(cuts) {
76774
+ return cuts.concat().sort(function(a, b) {
76775
+ return a.seq - b.seq || getCutOffset(a) - getCutOffset(b);
76776
+ });
76777
+ }
76778
+
76779
+ // A sortable offset within one arc. A cut on the segment following a vertex
76780
+ // comes after the vertex itself; two cuts on the same segment are ordered by
76781
+ // their position along it.
76782
+ function getCutOffset(cut) {
76783
+ if (!cut.point) return cut.offset;
76784
+ return cut.offset + 0.5 + (cut.t > 0 ? Math.min(cut.t, 1) : 0) * 0.4;
76785
+ }
76786
+
76787
+ function getPlanarPathLength(ids, arcs) {
76788
+ var iter = arcs.getShapeIter(ids);
76789
+ var len = 0;
76790
+ var x, y;
76791
+ if (iter.hasNext()) {
76792
+ x = iter.x;
76793
+ y = iter.y;
76794
+ }
76795
+ while (iter.hasNext()) {
76796
+ len += distance2D$1(x, y, iter.x, iter.y);
76797
+ x = iter.x;
76798
+ y = iter.y;
76799
+ }
76800
+ return len;
76801
+ }
76802
+
76803
+ var ArcSplit = /*#__PURE__*/Object.freeze({
76804
+ __proto__: null,
76805
+ getArcSliceLength: getArcSliceLength,
76806
+ getArcSlices: getArcSlices,
76807
+ getPlanarPathLength: getPlanarPathLength,
76808
+ planPathDivision: planPathDivision,
76809
+ splitArcAtCuts: splitArcAtCuts
76810
+ });
76811
+
76562
76812
  var BOX_BLUR_PASSES = 3;
76563
76813
 
76564
76814
  function blurRasterGrid(raster, optsArg, crs) {
@@ -77735,6 +77985,7 @@ ${svg}
77735
77985
  AnchorPoints,
77736
77986
  ArcClassifier,
77737
77987
  ArcDissolve,
77988
+ ArcSplit,
77738
77989
  ArcUtils,
77739
77990
  Bbox2Clipping,
77740
77991
  BinArray$1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.51",
3
+ "version": "0.7.52",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -32,6 +32,7 @@
32
32
  "postpublish": "./release_web_ui; ./release_github_version",
33
33
  "dev": "rollup --config --watch",
34
34
  "test:browser": "playwright test --fully-parallel --workers=6",
35
+ "install:browsers": "./install-playwright-browsers",
35
36
  "benchmark:raster": "MAPSHAPER_RUN_BENCHMARKS=1 playwright test browser-tests/raster-resampling-benchmark.spec.mjs --project=chromium --workers=1"
36
37
  },
37
38
  "main": "./mapshaper.js",
@@ -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,22 @@
12693
12785
  addHistoryState(undo, redo);
12694
12786
  });
12695
12787
 
12788
+ gui.on('snip', function(e) {
12789
+ var target = e.target;
12790
+ var result = e.result;
12791
+ var undo = function() {
12792
+ undoSnip(target, result);
12793
+ gui.model.updated({arc_count: true});
12794
+ };
12795
+ var redo = function() {
12796
+ // re-snipping restores the same arc ids, because the arcs added by the
12797
+ // previous snip were removed from the end of the collection
12798
+ result = snipPath(target, result.fid, result.partId, result.cuts);
12799
+ gui.model.updated({arc_count: true});
12800
+ };
12801
+ addHistoryState(undo, redo);
12802
+ });
12803
+
12696
12804
  gui.on('point_add', function(e) {
12697
12805
  var redo = function() {
12698
12806
  appendNewPoint(e.data.target, e.p);
@@ -13174,7 +13282,7 @@
13174
13282
  empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box', 'ruler'],
13175
13283
  polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons', 'ruler'],
13176
13284
  rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons', 'ruler'],
13177
- lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', 'ruler'], // 'snip_lines'
13285
+ lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', 'snip_lines', 'ruler'],
13178
13286
  table: ['info', 'selection'],
13179
13287
  raster: ['ruler', 'box'],
13180
13288
  labels: ['info', 'selection', 'box', 'point_style', 'labels', 'edit_points', 'ruler'],
@@ -13278,7 +13386,7 @@
13278
13386
  };
13279
13387
 
13280
13388
  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);
13389
+ return ['data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'snip_lines', 'vertices', 'rectangles'].includes(mode);
13282
13390
  };
13283
13391
 
13284
13392
  this.getMode = getInteractionMode;
@@ -16807,7 +16915,8 @@
16807
16915
  test = polygonVertexTest;
16808
16916
  } else if (
16809
16917
  interactionMode == 'vertices' ||
16810
- interactionMode == 'edit_lines') {
16918
+ interactionMode == 'edit_lines' ||
16919
+ interactionMode == 'snip_lines') {
16811
16920
  test = vertexTest;
16812
16921
  } else if (geoType == 'polyline') {
16813
16922
  test = polylineTest;
@@ -17332,6 +17441,22 @@
17332
17441
  triggerChangeEvent();
17333
17442
  };
17334
17443
 
17444
+ // Mark the location of a snip that has been chosen but not yet applied
17445
+ // (the first of the two cuts needed to divide a ring).
17446
+ self.setPendingSnip = function(p) {
17447
+ var p2 = storedData.snip_coordinates;
17448
+ if (!active || !p) return;
17449
+ if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
17450
+ storedData.snip_coordinates = p;
17451
+ triggerChangeEvent();
17452
+ };
17453
+
17454
+ self.clearPendingSnip = function() {
17455
+ if (!storedData.snip_coordinates) return;
17456
+ delete storedData.snip_coordinates;
17457
+ triggerChangeEvent();
17458
+ };
17459
+
17335
17460
  self.clearSelection = function() {
17336
17461
  updateSelectionState(null);
17337
17462
  };
@@ -17584,6 +17709,11 @@
17584
17709
  (type == 'hover' || type == 'dblclick')) {
17585
17710
  return true; // special case -- using hover for line drawing animation
17586
17711
  }
17712
+ if (mode == 'snip_lines' && (type == 'hover' || type == 'click')) {
17713
+ // the snip tool tracks vertices under the pointer, so it needs hover
17714
+ // events as soon as a feature is hit, not one event later
17715
+ return true;
17716
+ }
17587
17717
 
17588
17718
  // ignore pointer events when no features are being hit
17589
17719
  // (don't block pan and other navigation when events aren't being used for editing)
@@ -17601,7 +17731,8 @@
17601
17731
  }
17602
17732
 
17603
17733
  function possiblyStopPropagation(e) {
17604
- if (interactionMode() == 'edit_lines' || interactionMode() == 'edit_polygons') {
17734
+ var mode = interactionMode();
17735
+ if (mode == 'edit_lines' || mode == 'edit_polygons' || mode == 'snip_lines') {
17605
17736
  // handled conditionally in the control
17606
17737
  return;
17607
17738
  }
@@ -19189,6 +19320,7 @@
19189
19320
  // hoverFill = "rgba(255, 120, 255, 0.12)",
19190
19321
  hoverFill = "rgba(0, 0, 0, 0.08)",
19191
19322
  grey = "#888",
19323
+ orange = "#f28100",
19192
19324
  violet = "#cc6acc",
19193
19325
  black = 'black',
19194
19326
  violetFill = "rgba(249, 120, 249, 0.25)",
@@ -19420,20 +19552,30 @@
19420
19552
 
19421
19553
  // style for vertex edit mode
19422
19554
  function getLineEditingStyle(o) {
19555
+ var isVertex = o.hit_type == 'vertex' || o.hit_type == 'disabled';
19423
19556
  return {
19424
19557
  ids: o.ids,
19425
19558
  overlay: true,
19426
19559
  strokeColor: black,
19427
19560
  strokeWidth: 1.2,
19428
19561
  vertices: true,
19429
- vertex_overlay_color: o.hit_type == 'vertex' ? violet : black,
19430
- vertex_overlay_scale: o.hit_type == 'vertex' ? 2.5 : 2,
19562
+ vertex_overlay_color: getVertexOverlayColor(o.hit_type),
19563
+ vertex_overlay_scale: isVertex ? 2.5 : 2,
19431
19564
  vertex_overlay: o.hit_coordinates || null,
19565
+ pending_snip: o.snip_coordinates || null,
19566
+ pending_snip_color: orange,
19432
19567
  selected_points: o.selected_points || null,
19433
19568
  fillColor: null
19434
19569
  };
19435
19570
  }
19436
19571
 
19572
+ function getVertexOverlayColor(hitType) {
19573
+ if (hitType == 'vertex') return violet;
19574
+ // a muted dot marks a vertex that the current tool can not act on
19575
+ if (hitType == 'disabled') return grey;
19576
+ return black;
19577
+ }
19578
+
19437
19579
  function getSelectedFeatureStyle(lyr, o, opts) {
19438
19580
  var isPinned = o.pinned;
19439
19581
  var inSelection = o.ids.indexOf(o.id) > -1;
@@ -20480,147 +20622,275 @@
20480
20622
  }
20481
20623
  }
20482
20624
 
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
20625
+ // pixel distance threshold for hovering near a vertex or a segment
20499
20626
  var HOVER_THRESHOLD = 10;
20500
20627
 
20501
20628
  function initSnipTool(gui, ext, hit) {
20502
- var _active = true;
20503
- var hoverVertexInfo;
20504
- var prevHoverEvent;
20629
+ var hoverInfo = null;
20630
+ var pendingCut = null; // first of the two cuts needed to divide a ring
20631
+ var sessionCount = 0;
20632
+ var alert;
20633
+
20634
+ function active() {
20635
+ return gui.interaction.getMode() == 'snip_lines' && !!hit.getHitTarget();
20636
+ }
20505
20637
 
20506
20638
  gui.on('interaction_mode_change', function(e) {
20507
- if (active()) {
20639
+ if (e.mode == 'snip_lines') {
20640
+ turnOn();
20641
+ } else {
20508
20642
  turnOff();
20509
20643
  }
20510
- // updateCursor();
20644
+ updateCursor();
20511
20645
  }, null, 10); // higher priority than hit control, so turnOff() has correct hit target
20512
20646
 
20647
+ gui.on('undo_redo_pre', function() {
20648
+ clearPendingCut();
20649
+ hoverInfo = null;
20650
+ });
20651
+
20652
+ // A pending cut describes a location in the target layer's current geometry,
20653
+ // and choosing one does not change the model -- so any model update means
20654
+ // something else has happened and the cut is stale.
20655
+ gui.model.on('update', function() {
20656
+ clearPendingCut();
20657
+ hoverInfo = null;
20658
+ });
20659
+
20660
+ function turnOn() {
20661
+ hoverInfo = null;
20662
+ if (sessionCount === 0) {
20663
+ showInstructions();
20664
+ }
20665
+ sessionCount++;
20666
+ }
20667
+
20668
+ function turnOff() {
20669
+ clearPendingCut();
20670
+ clearHoverVertex();
20671
+ hideInstructions();
20672
+ }
20673
+
20674
+ function showInstructions() {
20675
+ var msg = 'Instructions: click a line to snip it apart. Snipping a ring ' +
20676
+ 'takes two clicks -- the ring divides at the second one.';
20677
+ alert = showPopupAlert(msg, null, {non_blocking: true, max_width: '350px'});
20678
+ }
20679
+
20680
+ function hideInstructions() {
20681
+ if (!alert) return;
20682
+ alert.close('fade');
20683
+ alert = null;
20684
+ }
20685
+
20686
+ function clearHoverVertex() {
20687
+ hit.clearHoverVertex();
20688
+ hoverInfo = null;
20689
+ }
20690
+
20691
+ function clearPendingCut() {
20692
+ if (!pendingCut) return;
20693
+ pendingCut = null;
20694
+ hit.clearPendingSnip();
20695
+ }
20696
+
20697
+ function setPendingCut(info) {
20698
+ pendingCut = info;
20699
+ hit.setPendingSnip(info.markerPoint);
20700
+ }
20701
+
20702
+ // The pending cut is described in terms of the target layer's current arcs
20703
+ // and feature ids, so it has to be discarded if either could have changed.
20704
+ function pendingCutIsValid() {
20705
+ return !!pendingCut && pendingCut.target === hit.getHitTarget();
20706
+ }
20513
20707
 
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
20708
  hit.on('hover', function(e) {
20517
20709
  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);
20710
+ if (pendingCut && !pendingCutIsValid()) {
20711
+ clearPendingCut();
20712
+ }
20713
+ hoverInfo = e.id >= 0 ? findSnipTarget(e) : null;
20714
+ if (hoverInfo) {
20715
+ hit.setHoverVertex(hoverInfo.markerPoint,
20716
+ hoverInfo.disabled ? 'disabled' : hoverInfo.type);
20526
20717
  } else {
20527
- clearHoverVertex();
20718
+ hit.clearHoverVertex();
20528
20719
  }
20529
- // updateCursor();
20530
- prevHoverEvent = e;
20720
+ if (pendingCut) {
20721
+ hit.setPendingSnip(pendingCut.markerPoint);
20722
+ }
20723
+ updateCursor();
20531
20724
  }, null, 100);
20532
20725
 
20533
20726
  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
20727
+ var info = hoverInfo;
20728
+ var cuts;
20729
+ if (!active() || !info || info.disabled) return;
20730
+ // a snip invalidates the hover data (feature ids and arcs both change), so
20731
+ // clear it -- this also makes the second click of a double-click a no-op
20732
+ hoverInfo = null;
20733
+ if (!info.ring) {
20734
+ clearPendingCut();
20735
+ snip(info, [info.cut]);
20538
20736
  return;
20539
20737
  }
20540
-
20541
- if (hoverVertexInfo.type == 'interpolated') {
20542
- insertVertex$1(target, hoverVertexInfo.i, hoverVertexInfo.point);
20543
- hoverVertexInfo.ids = [hoverVertexInfo.i];
20738
+ if (!pendingCutIsValid() || pendingCut.fid !== info.fid ||
20739
+ pendingCut.partId !== info.partId) {
20740
+ // first cut on this ring: remember it, don't change any geometry
20741
+ setPendingCut(info);
20742
+ return;
20743
+ }
20744
+ if (cutsAreAdjacent(pendingCut, info)) {
20745
+ clearPendingCut(); // clicking the pending cut again cancels it
20746
+ return;
20544
20747
  }
20748
+ cuts = [pendingCut.cut, info.cut];
20749
+ clearPendingCut();
20750
+ snip(info, cuts);
20751
+ });
20545
20752
 
20546
- snipLineAtVertex(target, e.id, hoverVertexInfo.ids[0]);
20753
+ gui.keyboard.on('keydown', function(e) {
20754
+ if (!active() || !pendingCut || e.keyName != 'esc') return;
20755
+ e.stopPropagation();
20756
+ e.originalEvent.preventDefault();
20757
+ clearPendingCut();
20758
+ }, null, 10);
20547
20759
 
20548
- hit.setHoverVertex(hoverVertexInfo.displayPoint, hoverVertexInfo.type);
20760
+ function snip(info, cuts) {
20761
+ var target = hit.getHitTarget();
20762
+ var result = snipPath(target, info.fid, info.partId, cuts);
20763
+ if (!result) return;
20764
+ gui.dispatchEvent('snip', {target: target, result: result});
20765
+ hit.clearHoverVertex();
20766
+ hideInstructions();
20767
+ gui.model.updated({arc_count: true}); // update display arcs and redraw
20768
+ }
20549
20769
 
20550
- });
20770
+ // Test if two cuts are close enough on screen to be treated as the same place
20771
+ function cutsAreAdjacent(a, b) {
20772
+ var dist = geom.distance2D(a.markerPoint[0], a.markerPoint[1],
20773
+ b.markerPoint[0], b.markerPoint[1]);
20774
+ return dist / ext.getPixelSize() < HOVER_THRESHOLD;
20775
+ }
20551
20776
 
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'};
20777
+ function updateCursor() {
20778
+ var el = gui.container.findChild('.map-layers');
20779
+ el.classed('snip-tool', active());
20780
+ el.classed('no-snip', !!(hoverInfo && hoverInfo.disabled));
20569
20781
  }
20570
20782
 
20571
- function findInterpolatedPoint(e) {
20783
+ // Find the snip location under the pointer: the nearest vertex within the
20784
+ // hover threshold, or failing that the nearest point on a segment.
20785
+ // Returns null if the pointer is not close enough to the hit feature.
20786
+ function findSnipTarget(e) {
20572
20787
  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
- }
20788
+ var shp = target && target.shapes[e.id];
20789
+ var arcs = target && target.gui.displayArcs;
20790
+ var bestVertex = null;
20791
+ var bestSegment = null;
20792
+ var p, threshold, data;
20793
+ if (!shp || !arcs) return null;
20794
+ p = ext.pixCoordsToMapCoords(e.x, e.y);
20795
+ threshold = HOVER_THRESHOLD * ext.getPixelSize();
20796
+ data = arcs.getVertexData();
20797
+
20798
+ shp.forEach(function(path, partId) {
20799
+ path.forEach(function(arcId, seq) {
20800
+ var absId = internal.absArcId(arcId);
20801
+ var fwd = arcId >= 0;
20802
+ var start = data.ii[absId];
20803
+ var n = data.nn[absId];
20804
+ var i, v1, v2, dist, p2;
20805
+ if (arcIsOutOfRange(data.bb, absId, p, threshold)) return;
20806
+ for (i=0; i<n; i++) {
20807
+ v1 = fwd ? start + i : start + n - 1 - i;
20808
+ dist = geom.distance2D(p[0], p[1], data.xx[v1], data.yy[v1]);
20809
+ if (dist < threshold && (!bestVertex || dist < bestVertex.dist)) {
20810
+ bestVertex = {partId: partId, seq: seq, offset: i, dist: dist, vid: v1};
20811
+ }
20812
+ if (i === n - 1) break;
20813
+ v2 = fwd ? v1 + 1 : v1 - 1;
20814
+ p2 = internal.findClosestPointOnSeg(p[0], p[1], data.xx[v1],
20815
+ data.yy[v1], data.xx[v2], data.yy[v2], 0);
20816
+ dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
20817
+ if (dist < threshold && (!bestSegment || dist < bestSegment.dist)) {
20818
+ bestSegment = {
20819
+ partId: partId, seq: seq, offset: i, dist: dist, displayPoint: p2,
20820
+ t: getSegmentPosition(data.xx[v1], data.yy[v1], data.xx[v2],
20821
+ data.yy[v2], p2)
20822
+ };
20823
+ }
20824
+ }
20825
+ });
20594
20826
  });
20595
20827
 
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;
20828
+ if (bestVertex) return getVertexTarget(target, e.id, bestVertex);
20829
+ if (bestSegment) return getSegmentTarget(target, e.id, bestSegment);
20830
+ return null;
20603
20831
  }
20604
20832
 
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);
20833
+ function getVertexTarget(target, fid, info) {
20834
+ var displayPoint = target.gui.displayArcs.getVertex2(info.vid);
20835
+ return {
20836
+ target: target,
20837
+ fid: fid,
20838
+ partId: info.partId,
20839
+ type: 'vertex',
20840
+ ring: pathIsRing(target, fid, info.partId),
20841
+ disabled: !pathIsRing(target, fid, info.partId) &&
20842
+ vertexIsPathEndpoint(target, fid, info),
20843
+ markerPoint: displayPoint,
20844
+ cut: {seq: info.seq, offset: info.offset, point: null, displayPoint: null, t: 0}
20845
+ };
20609
20846
  }
20610
20847
 
20611
- function clearHoverVertex() {
20612
- hit.clearHoverVertex();
20613
- hoverVertexInfo = null;
20848
+ function getSegmentTarget(target, fid, info) {
20849
+ return {
20850
+ target: target,
20851
+ fid: fid,
20852
+ partId: info.partId,
20853
+ type: 'interpolated',
20854
+ ring: pathIsRing(target, fid, info.partId),
20855
+ disabled: false,
20856
+ markerPoint: info.displayPoint,
20857
+ cut: {
20858
+ seq: info.seq,
20859
+ offset: info.offset,
20860
+ point: translateDisplayPoint(target, info.displayPoint),
20861
+ displayPoint: info.displayPoint,
20862
+ t: info.t
20863
+ }
20864
+ };
20614
20865
  }
20615
20866
 
20616
- function active() {
20617
- return _active && gui.interaction.getMode() == 'snip_lines';
20867
+ function pathIsRing(target, fid, partId) {
20868
+ return geom.pathIsClosed(target.shapes[fid][partId], target.gui.displayArcs);
20618
20869
  }
20619
20870
 
20620
- function turnOff() {
20621
-
20871
+ // Test if a vertex is one of the two endpoints of an open path (as opposed to
20872
+ // an interior node between two of the path's arcs, which is snippable).
20873
+ function vertexIsPathEndpoint(target, fid, info) {
20874
+ var path = target.shapes[fid][info.partId];
20875
+ var data = target.gui.displayArcs.getVertexData();
20876
+ var lastId = internal.absArcId(path[path.length - 1]);
20877
+ if (info.seq === 0 && info.offset === 0) return true;
20878
+ return info.seq === path.length - 1 && info.offset === data.nn[lastId] - 1;
20622
20879
  }
20880
+ }
20623
20881
 
20882
+ // Test if an arc's bounding box is too far from a point to contain a hit
20883
+ function arcIsOutOfRange(bb, arcId, p, dist) {
20884
+ var i = arcId * 4;
20885
+ return p[0] + dist < bb[i] || p[0] - dist > bb[i + 2] ||
20886
+ p[1] + dist < bb[i + 1] || p[1] - dist > bb[i + 3];
20887
+ }
20888
+
20889
+ // Position of point p along the segment from (x1, y1) to (x2, y2), as a
20890
+ // fraction of the segment's length
20891
+ function getSegmentPosition(x1, y1, x2, y2, p) {
20892
+ var len = geom.distance2D(x1, y1, x2, y2);
20893
+ return len > 0 ? geom.distance2D(x1, y1, p[0], p[1]) / len : 0;
20624
20894
  }
20625
20895
 
20626
20896
  function initInteractiveEditing(gui, ext, hit) {
@@ -21822,6 +22092,15 @@
21822
22092
  _ctx.fill();
21823
22093
  _ctx.closePath();
21824
22094
  }
22095
+
22096
+ if (style.pending_snip) {
22097
+ _ctx.beginPath();
22098
+ _ctx.fillStyle = style.pending_snip_color || 'black';
22099
+ p = style.pending_snip;
22100
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 2.5, _ctx);
22101
+ _ctx.fill();
22102
+ _ctx.closePath();
22103
+ }
21825
22104
  };
21826
22105
 
21827
22106
  // Optimized to draw paths in same-style batches (faster Canvas drawing)
package/www/mapshaper.js CHANGED
@@ -75897,7 +75897,7 @@ ${svg}
75897
75897
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
75898
75898
  }
75899
75899
 
75900
- var version = "0.7.51";
75900
+ var version = "0.7.52";
75901
75901
 
75902
75902
  // Parse command line args into commands and run them
75903
75903
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -76559,6 +76559,256 @@ ${svg}
76559
76559
  return false;
76560
76560
  }
76561
76561
 
76562
+ // Utilities for dividing a path at one or two points ("snipping").
76563
+ //
76564
+ // The new arcs are appended to the end of the collection; the original arc is
76565
+ // left in place (it may still be referenced by other shapes). Callers are
76566
+ // responsible for updating the shapes that should use the new arcs.
76567
+
76568
+ // Divide an arc's vertices into slices at one or two cut locations.
76569
+ // n: number of vertices in the arc
76570
+ // cuts: one or two cut objects, ordered from the start of the arc:
76571
+ // {offset: i, point: [x, y]} cuts the segment between vertices i and i + 1
76572
+ // {offset: i, point: null} cuts at vertex i (which is duplicated, so that
76573
+ // both adjacent slices contain it)
76574
+ // Returns slice descriptors {start, end, prefix, suffix}, where start and end
76575
+ // are an inclusive vertex range (empty if start > end) and prefix/suffix are
76576
+ // points inserted at a cut location.
76577
+ function getArcSlices(n, cuts) {
76578
+ var slices = [];
76579
+ var start = 0;
76580
+ var prefix = null;
76581
+ cuts.forEach(function(cut) {
76582
+ var i = cut.offset;
76583
+ if (cut.point) {
76584
+ if (i < 0 || i > n - 2) error('Out-of-range arc cut offset:', i);
76585
+ slices.push({start: start, end: i, prefix: prefix, suffix: cut.point});
76586
+ prefix = cut.point;
76587
+ start = i + 1;
76588
+ } else {
76589
+ if (i < 1 || i > n - 2) error('Out-of-range arc cut offset:', i);
76590
+ slices.push({start: start, end: i, prefix: prefix, suffix: null});
76591
+ prefix = null;
76592
+ start = i;
76593
+ }
76594
+ });
76595
+ slices.push({start: start, end: n - 1, prefix: prefix, suffix: null});
76596
+ return slices;
76597
+ }
76598
+
76599
+ function getArcSliceLength(slice) {
76600
+ var n = slice.end >= slice.start ? slice.end - slice.start + 1 : 0;
76601
+ if (slice.prefix) n++;
76602
+ if (slice.suffix) n++;
76603
+ return n;
76604
+ }
76605
+
76606
+ // Append the slices of a divided arc to an ArcCollection.
76607
+ // Returns the ids of the new arcs, ordered from the start of the original arc.
76608
+ function splitArcAtCuts(arcs, arcId, cuts) {
76609
+ var data = arcs.getVertexData();
76610
+ var zlimit = arcs.getRetainedInterval();
76611
+ var arcStart = data.ii[arcId];
76612
+ var arcCount = data.nn.length;
76613
+ var slices = getArcSlices(data.nn[arcId], cuts);
76614
+ var pointCount = data.xx.length;
76615
+ var added = slices.reduce(function(memo, slice) {
76616
+ return memo + getArcSliceLength(slice);
76617
+ }, 0);
76618
+ var xx2 = new Float64Array(pointCount + added);
76619
+ var yy2 = new Float64Array(pointCount + added);
76620
+ var zz2 = arcs.isFlat() ? null : new Float64Array(pointCount + added);
76621
+ var nn2 = new Uint32Array(arcCount + slices.length);
76622
+ var i = pointCount;
76623
+ var ids = [];
76624
+
76625
+ xx2.set(data.xx);
76626
+ yy2.set(data.yy);
76627
+ nn2.set(data.nn);
76628
+ if (zz2) zz2.set(data.zz);
76629
+
76630
+ slices.forEach(function(slice, sliceId) {
76631
+ var first = i;
76632
+ var j;
76633
+ if (slice.prefix) {
76634
+ xx2[i] = slice.prefix[0];
76635
+ yy2[i] = slice.prefix[1];
76636
+ i++;
76637
+ }
76638
+ for (j = slice.start; j <= slice.end; j++) {
76639
+ xx2[i] = data.xx[arcStart + j];
76640
+ yy2[i] = data.yy[arcStart + j];
76641
+ if (zz2) zz2[i] = data.zz[arcStart + j];
76642
+ i++;
76643
+ }
76644
+ if (slice.suffix) {
76645
+ xx2[i] = slice.suffix[0];
76646
+ yy2[i] = slice.suffix[1];
76647
+ i++;
76648
+ }
76649
+ if (zz2) {
76650
+ // arc endpoints must never be removed by simplification
76651
+ zz2[first] = Infinity;
76652
+ zz2[i - 1] = Infinity;
76653
+ }
76654
+ nn2[arcCount + sliceId] = i - first;
76655
+ ids.push(arcCount + sliceId);
76656
+ });
76657
+
76658
+ arcs.updateVertexData(nn2, xx2, yy2, zz2);
76659
+ arcs.setRetainedInterval(zlimit);
76660
+ return ids;
76661
+ }
76662
+
76663
+ // A cut identifies one location along a path (an array of arc ids):
76664
+ // seq: index into the path of the arc containing the cut
76665
+ // offset: vertex index within that arc, in traversal orientation
76666
+ // point: coords of the cut, or null to cut at vertex <offset>
76667
+ // displayPoint: coords of the cut in the GUI's display CRS, or null
76668
+ // t: position along the segment (0-1), used to order two cuts
76669
+ // that land on the same segment
76670
+
76671
+ // Work out how to divide a path at one cut (open path) or two cuts (ring),
76672
+ // without mutating anything.
76673
+ //
76674
+ // path: array of arc ids
76675
+ // cutsArg: one cut (open path) or two cuts (ring), in any order
76676
+ // arcLen: function(arcId) returning the number of vertices in an arc
76677
+ // baseId: arcs.size(), used to predict the ids of arcs that will be appended
76678
+ // closed: whether the path is a ring
76679
+ //
76680
+ // Returns {splits, a, b}, where splits are arc divisions to perform in order
76681
+ // and a and b are the two resulting paths, or null if the cuts are degenerate
76682
+ // (at the endpoint of an open path, or two cuts in the same place on a ring).
76683
+ //
76684
+ function planPathDivision(path, cutsArg, arcLen, baseId, closed) {
76685
+ var cuts = sortCuts(cutsArg);
76686
+ var boundaryCuts = []; // cuts that fall between two arcs
76687
+ var interiorCuts = {}; // cuts that fall inside an arc, by path index
76688
+ var expanded = []; // path with divided arcs replaced by their pieces
76689
+ var positions = []; // position of each cut within the expanded path
76690
+ var splits = [];
76691
+ var nextArcId = baseId;
76692
+ var a, b, q, q1, q2;
76693
+
76694
+ if (cuts.length != (closed ? 2 : 1)) return null;
76695
+
76696
+ cuts.forEach(function(cut, cutId) {
76697
+ var n = arcLen(absArcId(path[cut.seq]));
76698
+ var seq;
76699
+ if (!cut.point && (cut.offset === 0 || cut.offset === n - 1)) {
76700
+ // a cut at an arc endpoint doesn't divide the arc, it divides the
76701
+ // sequence of arcs
76702
+ seq = cut.offset === 0 ? cut.seq : cut.seq + 1;
76703
+ if (closed && seq === path.length) seq = 0; // a ring's seam is one place
76704
+ boundaryCuts.push({cutId: cutId, seq: seq});
76705
+ } else {
76706
+ if (!interiorCuts[cut.seq]) interiorCuts[cut.seq] = [];
76707
+ interiorCuts[cut.seq].push({cutId: cutId, cut: cut, arcLen: n});
76708
+ }
76709
+ });
76710
+
76711
+ for (var k=0; k<=path.length; k++) {
76712
+ boundaryCuts.forEach(function(item) {
76713
+ if (item.seq === k) positions[item.cutId] = expanded.length;
76714
+ });
76715
+ if (k === path.length) break;
76716
+ if (!interiorCuts[k]) {
76717
+ expanded.push(path[k]);
76718
+ continue;
76719
+ }
76720
+ addDividedArc(interiorCuts[k], path[k]);
76721
+ }
76722
+
76723
+ if (!closed) {
76724
+ q = positions[0];
76725
+ if (q === 0 || q === expanded.length) return null; // cut at a path endpoint
76726
+ a = expanded.slice(0, q);
76727
+ b = expanded.slice(q);
76728
+ } else {
76729
+ q1 = Math.min(positions[0], positions[1]);
76730
+ q2 = Math.max(positions[0], positions[1]);
76731
+ if (q1 === q2) return null; // both cuts in the same place
76732
+ a = expanded.slice(q1, q2);
76733
+ b = expanded.slice(q2).concat(expanded.slice(0, q1));
76734
+ }
76735
+ return {splits: splits, a: a, b: b};
76736
+
76737
+ function addDividedArc(group, arcId) {
76738
+ var fwd = arcId >= 0;
76739
+ var n = group[0].arcLen;
76740
+ // convert traversal offsets to forward-orientation offsets
76741
+ var fwdCuts = group.map(function(item) {
76742
+ return {
76743
+ offset: fwd ? item.cut.offset :
76744
+ n - (item.cut.point ? 2 : 1) - item.cut.offset,
76745
+ point: item.cut.point || null,
76746
+ displayPoint: item.cut.displayPoint || null
76747
+ };
76748
+ });
76749
+ // a reversed arc is traversed from its last vertex, so traversal order is
76750
+ // the reverse of forward order
76751
+ var pieces = getSplitArcIds(nextArcId, group.length, fwd);
76752
+ splits.push({
76753
+ arcId: absArcId(arcId),
76754
+ cuts: fwd ? fwdCuts : fwdCuts.concat().reverse()
76755
+ });
76756
+ nextArcId += group.length + 1;
76757
+ pieces.forEach(function(id, i) {
76758
+ if (i > 0) positions[group[i - 1].cutId] = expanded.length;
76759
+ expanded.push(id);
76760
+ });
76761
+ }
76762
+ }
76763
+
76764
+ // Ids of the pieces of a divided arc, in traversal order
76765
+ function getSplitArcIds(baseId, cutCount, fwd) {
76766
+ var ids = [];
76767
+ for (var i=0; i<=cutCount; i++) {
76768
+ ids.push(fwd ? baseId + i : ~(baseId + cutCount - i));
76769
+ }
76770
+ return ids;
76771
+ }
76772
+
76773
+ function sortCuts(cuts) {
76774
+ return cuts.concat().sort(function(a, b) {
76775
+ return a.seq - b.seq || getCutOffset(a) - getCutOffset(b);
76776
+ });
76777
+ }
76778
+
76779
+ // A sortable offset within one arc. A cut on the segment following a vertex
76780
+ // comes after the vertex itself; two cuts on the same segment are ordered by
76781
+ // their position along it.
76782
+ function getCutOffset(cut) {
76783
+ if (!cut.point) return cut.offset;
76784
+ return cut.offset + 0.5 + (cut.t > 0 ? Math.min(cut.t, 1) : 0) * 0.4;
76785
+ }
76786
+
76787
+ function getPlanarPathLength(ids, arcs) {
76788
+ var iter = arcs.getShapeIter(ids);
76789
+ var len = 0;
76790
+ var x, y;
76791
+ if (iter.hasNext()) {
76792
+ x = iter.x;
76793
+ y = iter.y;
76794
+ }
76795
+ while (iter.hasNext()) {
76796
+ len += distance2D$1(x, y, iter.x, iter.y);
76797
+ x = iter.x;
76798
+ y = iter.y;
76799
+ }
76800
+ return len;
76801
+ }
76802
+
76803
+ var ArcSplit = /*#__PURE__*/Object.freeze({
76804
+ __proto__: null,
76805
+ getArcSliceLength: getArcSliceLength,
76806
+ getArcSlices: getArcSlices,
76807
+ getPlanarPathLength: getPlanarPathLength,
76808
+ planPathDivision: planPathDivision,
76809
+ splitArcAtCuts: splitArcAtCuts
76810
+ });
76811
+
76562
76812
  var BOX_BLUR_PASSES = 3;
76563
76813
 
76564
76814
  function blurRasterGrid(raster, optsArg, crs) {
@@ -77735,6 +77985,7 @@ ${svg}
77735
77985
  AnchorPoints,
77736
77986
  ArcClassifier,
77737
77987
  ArcDissolve,
77988
+ ArcSplit,
77738
77989
  ArcUtils,
77739
77990
  Bbox2Clipping,
77740
77991
  BinArray$1,
package/www/page.css CHANGED
@@ -1972,10 +1972,15 @@ div.basemap-style-btn.active {
1972
1972
 
1973
1973
  .map-layers.add-points,
1974
1974
  .map-layers.draw-tool,
1975
+ .map-layers.snip-tool,
1975
1976
  .map-layers.ruler-tool {
1976
1977
  cursor: crosshair;
1977
1978
  }
1978
1979
 
1980
+ .map-layers.snip-tool.no-snip {
1981
+ cursor: default;
1982
+ }
1983
+
1979
1984
  .map-layers.ruler-dragging {
1980
1985
  cursor: grabbing;
1981
1986
  }