mapshaper 0.6.71 → 0.6.73

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
@@ -3208,13 +3208,13 @@
3208
3208
  // Used by mapshaper-undershoots.js
3209
3209
  // TODO: make more robust, make sure result is compatible with segmentIntersection()
3210
3210
  // (rounding errors currently must be handled downstream)
3211
- function findClosestPointOnSeg(px, py, ax, ay, bx, by) {
3211
+ function findClosestPointOnSeg(px, py, ax, ay, bx, by, snapArg) {
3212
3212
  var dx = bx - ax,
3213
3213
  dy = by - ay,
3214
3214
  dotp = (px - ax) * dx + (py - ay) * dy,
3215
3215
  abSq = dx * dx + dy * dy,
3216
3216
  k = abSq === 0 ? -1 : dotp / abSq,
3217
- eps = 0.1, // 1e-6, // snap to endpoint
3217
+ eps = snapArg >= 0 ? snapArg : 0.1, // 1e-6, // snap to endpoint
3218
3218
  p;
3219
3219
  if (k <= eps) {
3220
3220
  p = [ax, ay];
@@ -45492,7 +45492,7 @@ ${svg}
45492
45492
  });
45493
45493
  }
45494
45494
 
45495
- var version = "0.6.71";
45495
+ var version = "0.6.73";
45496
45496
 
45497
45497
  // Parse command line args into commands and run them
45498
45498
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -46238,6 +46238,7 @@ ${svg}
46238
46238
  Rounding,
46239
46239
  RunCommands,
46240
46240
  Scalebar,
46241
+ SegmentGeom,
46241
46242
  SegmentIntersection,
46242
46243
  ShapeIter$1,
46243
46244
  ShapeUtils,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.6.71",
3
+ "version": "0.6.73",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -1092,16 +1092,21 @@
1092
1092
  return _el;
1093
1093
  }
1094
1094
 
1095
- function showPopupAlert(msg, title) {
1095
+ function showPopupAlert(msg, title, optsArg) {
1096
+ var opts = optsArg || {};
1096
1097
  var self = {}, html = '';
1097
1098
  var _cancel, _close;
1098
1099
  var warningRxp = /^Warning: /;
1099
- var el = El('div').appendTo('body').addClass('alert-wrapper');
1100
+ var el = El('div').appendTo('body').addClass('alert-wrapper')
1101
+ .classed('non-blocking', opts.non_blocking);
1100
1102
  var infoBox = El('div').appendTo(el).addClass('alert-box info-box selectable');
1101
1103
  El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() {
1102
1104
  if (_cancel) _cancel();
1103
1105
  self.close();
1104
1106
  });
1107
+ if (opts.max_width) {
1108
+ infoBox.node().style.maxWidth = opts.max_width;
1109
+ }
1105
1110
  var container = El('div').appendTo(infoBox);
1106
1111
  if (!title && warningRxp.test(msg)) {
1107
1112
  title = 'Warning';
@@ -1112,7 +1117,7 @@
1112
1117
  }
1113
1118
  var content = El('div').appendTo(infoBox);
1114
1119
  if (msg) {
1115
- content.html(`<p class="error-message">${msg}</p>`);
1120
+ content.html(`<p class="alert-message">${msg}</p>`);
1116
1121
  }
1117
1122
 
1118
1123
  self.container = function() { return content; };
@@ -2704,8 +2709,8 @@
2704
2709
  if (opts.interactionMode == 'vertices') {
2705
2710
  return getVertexStyle(baseLyr, o);
2706
2711
  }
2707
- if (opts.interactionMode == 'edit-lines') {
2708
- return getLineEditingStyle(baseLyr, o);
2712
+ if (opts.interactionMode == 'drawing') {
2713
+ return getLineEditingStyle(o);
2709
2714
  }
2710
2715
  var geomType = baseLyr.geometry_type;
2711
2716
  var topId = o.id; // pinned id (if pinned) or hover id
@@ -2787,7 +2792,6 @@
2787
2792
  strokeColor: black,
2788
2793
  strokeWidth: 1.5,
2789
2794
  vertices: true,
2790
- vertex_overlay_color: violet,
2791
2795
  vertex_overlay: o.hit_coordinates || null,
2792
2796
  selected_points: o.selected_points || null,
2793
2797
  fillColor: null
@@ -2795,14 +2799,15 @@
2795
2799
  }
2796
2800
 
2797
2801
  // style for vertex edit mode
2798
- function getLineEditingStyle(lyr, o) {
2802
+ function getLineEditingStyle(o) {
2799
2803
  return {
2800
2804
  ids: o.ids,
2801
2805
  overlay: true,
2802
2806
  strokeColor: 'black',
2803
2807
  strokeWidth: 1.2,
2804
2808
  vertices: true,
2805
- vertex_overlay_color: violet,
2809
+ vertex_overlay_color: o.hit_type == 'vertex' ? violet : black,
2810
+ vertex_overlay_scale: o.hit_type == 'vertex' ? 2.4 : 1.7,
2806
2811
  vertex_overlay: o.hit_coordinates || null,
2807
2812
  selected_points: o.selected_points || null,
2808
2813
  fillColor: null
@@ -2942,10 +2947,15 @@
2942
2947
  d['label-text'] = 'TBD'; // without text, new labels will be invisible
2943
2948
  } else if (layer.geometry_type == 'point' && fields.includes('r')) {
2944
2949
  d.r = 3; // show a black circle if layer is styled
2945
- } else if (true || layer.geometry_type == 'polyline' && fields.includes('stroke')) {
2946
- d.stroke = 'black';
2947
- } else if (layer.geometry_type == 'polygon' && fields.includes('stroke')) {
2948
- d.stroke = 'black';
2950
+ }
2951
+ if (layer.geometry_type == 'polyline' || layer.geometry_type == 'polygon') {
2952
+ if (fields.includes('stroke')) d.stroke = 'black';
2953
+ if (fields.includes('stroke-width')) d['stroke-width'] = 1;
2954
+ }
2955
+ if (layer.geometry_type == 'polygon') {
2956
+ if (fields.includes('fill')) {
2957
+ d.fill = 'rgba(0,0,0,0.10)'; // 'rgba(249,120,249,0.20)';
2958
+ }
2949
2959
  }
2950
2960
  // TODO: better styling
2951
2961
  layer.data.getRecords().push(d);
@@ -5156,7 +5166,7 @@
5156
5166
  var startPoint = e.point; // in data coords
5157
5167
  var endPoint = getVertexCoords(target, e.ids[0]);
5158
5168
  var undo = function() {
5159
- if (e.insertion) {
5169
+ if (e.data.type == 'interpolated') {
5160
5170
  deleteVertex$1(target, e.ids[0]);
5161
5171
  } else {
5162
5172
  setVertexCoords(target, e.ids, startPoint);
@@ -5420,9 +5430,9 @@
5420
5430
 
5421
5431
  var menus = {
5422
5432
  standard: ['info', 'selection', 'box'],
5423
- polygons: ['info', 'selection', 'box', 'vertices'],
5424
- rectangles: ['info', 'selection', 'box', 'rectangles', 'vertices'],
5425
- lines: ['info', 'selection', 'box' , 'edit-lines'],
5433
+ polygons: ['info', 'selection', 'box', 'drawing'],
5434
+ rectangles: ['info', 'selection', 'box', 'rectangles'], // 'vertices'
5435
+ lines: ['info', 'selection', 'box' , 'drawing'],
5426
5436
  table: ['info', 'selection'],
5427
5437
  labels: ['info', 'selection', 'box', 'labels', 'location', 'add-points'],
5428
5438
  points: ['info', 'selection', 'box', 'location', 'add-points']
@@ -5444,8 +5454,7 @@
5444
5454
  vertices: 'edit vertices',
5445
5455
  selection: 'select features',
5446
5456
  'add-points': 'add points',
5447
- 'draw-lines': 'draw lines',
5448
- 'edit-lines': 'draw/modify lines',
5457
+ drawing: 'draw/reshape tool',
5449
5458
  rectangles: 'drag-to-resize',
5450
5459
  off: 'turn off'
5451
5460
  };
@@ -5501,7 +5510,7 @@
5501
5510
  };
5502
5511
 
5503
5512
  this.modeUsesHitDetection = function(mode) {
5504
- return ['info', 'selection', 'data', 'labels', 'location', 'vertices', 'rectangles', 'edit-lines'].includes(mode);
5513
+ return ['info', 'selection', 'data', 'labels', 'location', 'vertices', 'rectangles', 'drawing'].includes(mode);
5505
5514
  };
5506
5515
 
5507
5516
  this.modeUsesPopup = function(mode) {
@@ -7242,7 +7251,11 @@
7242
7251
  test = getGraduatedCircleTest(getRadiusFunction(displayLayer.style));
7243
7252
  } else if (geoType == 'point') {
7244
7253
  test = pointTest;
7245
- } else if (interactionMode == 'vertices' || interactionMode == 'edit-lines') {
7254
+ } else if (interactionMode == 'drawing' && geoType == 'polygon') {
7255
+ test = polygonVertexTest;
7256
+ } else if (
7257
+ interactionMode == 'vertices' ||
7258
+ interactionMode == 'drawing') {
7246
7259
  test = vertexTest;
7247
7260
  } else if (geoType == 'polyline') {
7248
7261
  test = polylineTest;
@@ -7289,6 +7302,30 @@
7289
7302
  };
7290
7303
  }
7291
7304
 
7305
+ function polygonVertexTest(x, y) {
7306
+ var a = polygonTest(x, y);
7307
+ var b = polylineTest(x, y, 5);
7308
+ return {
7309
+ ids: utils$1.uniq(b.ids.concat(a.ids))
7310
+ };
7311
+ }
7312
+
7313
+ function vertexTest(x, y) {
7314
+ return polylineTest(x, y, 0);
7315
+ }
7316
+
7317
+ function polylineTest(x, y, bufArg) {
7318
+ var maxDist = getZoomAdjustedHitBuffer(15, 2),
7319
+ bufPix = bufArg >= 0 ? bufArg : 0.05, // tiny threshold for hitting almost-identical lines
7320
+ bufDist = getZoomAdjustedHitBuffer(bufPix),
7321
+ cands = findHitCandidates(x, y, maxDist);
7322
+ sortByDistance(x, y, cands, displayLayer.arcs);
7323
+ cands = pickNearestCandidates(cands, bufDist, maxDist);
7324
+ return {
7325
+ ids: utils$1.pluck(cands, 'id')
7326
+ };
7327
+ }
7328
+
7292
7329
  function pickNearestCandidates(sorted, bufDist, maxDist) {
7293
7330
  var hits = [],
7294
7331
  cand, minDist;
@@ -7306,28 +7343,6 @@
7306
7343
  return hits;
7307
7344
  }
7308
7345
 
7309
- function vertexTest(x, y) {
7310
- var bufferPix = 15; // 25;
7311
- var maxDist = getZoomAdjustedHitBuffer(bufferPix, 2),
7312
- cands = findHitCandidates(x, y, maxDist);
7313
- sortByDistance(x, y, cands, displayLayer.arcs);
7314
- cands = pickNearestCandidates(cands, 0, maxDist);
7315
- return {
7316
- ids: utils$1.pluck(cands, 'id')
7317
- };
7318
- }
7319
-
7320
- function polylineTest(x, y) {
7321
- var maxDist = getZoomAdjustedHitBuffer(15, 2),
7322
- bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines
7323
- cands = findHitCandidates(x, y, maxDist);
7324
- sortByDistance(x, y, cands, displayLayer.arcs);
7325
- cands = pickNearestCandidates(cands, bufDist, maxDist);
7326
- return {
7327
- ids: utils$1.pluck(cands, 'id')
7328
- };
7329
- }
7330
-
7331
7346
  function sortByDistance(x, y, cands, arcs) {
7332
7347
  var cand;
7333
7348
  for (var i=0; i<cands.length; i++) {
@@ -7417,6 +7432,7 @@
7417
7432
  };
7418
7433
  }
7419
7434
 
7435
+ // Returns array of shape ids for shapes that pass a buffered bounding-box test
7420
7436
  function findHitCandidates(x, y, dist) {
7421
7437
  var arcs = displayLayer.arcs,
7422
7438
  index = {},
@@ -7667,13 +7683,12 @@
7667
7683
 
7668
7684
  function draggable() {
7669
7685
  return interactionMode == 'vertices' || interactionMode == 'location' ||
7670
- interactionMode == 'labels' || interactionMode == 'edit-lines';
7686
+ interactionMode == 'labels' || interactionMode == 'drawing';
7671
7687
  }
7672
7688
 
7673
7689
  function clickable() {
7674
7690
  // click used to pin popup and select features
7675
7691
  return interactionMode == 'data' || interactionMode == 'info' ||
7676
- // interactionMode == 'edit-lines';
7677
7692
  interactionMode == 'selection' || interactionMode == 'rectangles';
7678
7693
  }
7679
7694
 
@@ -7710,7 +7725,7 @@
7710
7725
  };
7711
7726
 
7712
7727
  // manually set the selected feature id(s)
7713
- // used when hit detection is turned off, e.g. 'edit-lines' mode
7728
+ // used when hit detection is turned off, e.g. 'drawing' mode
7714
7729
  self.setDrawingId = function(id) {
7715
7730
  if (id == drawingId) return;
7716
7731
  drawingId = id >= 0 ? id : -1;
@@ -7733,12 +7748,14 @@
7733
7748
  if (!active || !p) return;
7734
7749
  if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
7735
7750
  storedData.hit_coordinates = p;
7751
+ storedData.hit_type = type || '';
7736
7752
  triggerHitEvent('change');
7737
7753
  };
7738
7754
 
7739
7755
  self.clearHoverVertex = function() {
7740
7756
  if (!storedData.hit_coordinates) return;
7741
7757
  delete storedData.hit_coordinates;
7758
+ delete storedData.hit_type;
7742
7759
  triggerHitEvent('change');
7743
7760
  };
7744
7761
 
@@ -7820,7 +7837,7 @@
7820
7837
  if (clickable()) {
7821
7838
  updateSelectionState(convertClickDataToSelectionData(hitTest(e)));
7822
7839
  }
7823
- triggerHitEvent('click', e.data);
7840
+ triggerHitEvent('click', e);
7824
7841
  }, null, priority);
7825
7842
 
7826
7843
  // Hits are re-detected on 'hover' (if hit detection is active)
@@ -7926,7 +7943,7 @@
7926
7943
  // check if an event is used in the current interaction mode
7927
7944
  function eventIsEnabled(type) {
7928
7945
  if (!active) return false;
7929
- if (interactionMode == 'edit-lines' && (type == 'hover' || type == 'dblclick')) {
7946
+ if (interactionMode == 'drawing' && (type == 'hover' || type == 'dblclick')) {
7930
7947
  return true; // special case -- using hover for line drawing animation
7931
7948
  }
7932
7949
 
@@ -7948,10 +7965,18 @@
7948
7965
  return e.x >= 0 && e.y >= 0 && e.x < ext.width() && e.y < ext.height();
7949
7966
  }
7950
7967
 
7968
+ function possiblyStopPropagation(e) {
7969
+ if (interactionMode == 'drawing') {
7970
+ // handled conditionally in the control
7971
+ return;
7972
+ }
7973
+ e.stopPropagation();
7974
+ }
7975
+
7951
7976
  function handlePointerEvent(e) {
7952
7977
  if (eventIsEnabled(e.type)) {
7953
- e.stopPropagation(); // block navigation
7954
- triggerHitEvent(e.type, e.data);
7978
+ possiblyStopPropagation(e);
7979
+ triggerHitEvent(e.type, e);
7955
7980
  }
7956
7981
  }
7957
7982
 
@@ -7959,10 +7984,12 @@
7959
7984
  function triggerHitEvent(type, evt) {
7960
7985
  var eventData = {
7961
7986
  mode: interactionMode,
7962
- overMap: evt ? isOverMap(evt) : null
7987
+ overMap: evt ? isOverMap(evt) : null,
7988
+ originalEvent: evt ? evt : null
7963
7989
  };
7964
7990
  // Merge stored hit data into the event data
7965
- utils$1.extend(eventData, evt || {}, storedData);
7991
+ utils$1.defaults(eventData, evt && evt.data || {}, storedData);
7992
+ // utils.extend(eventData, storedData);
7966
7993
  if (transientIds.length) {
7967
7994
  // add transient ids to any other hit ids
7968
7995
  eventData.ids = utils$1.uniq(transientIds.concat(eventData.ids || []));
@@ -9675,32 +9702,31 @@
9675
9702
  }
9676
9703
 
9677
9704
  // pointer thresholds for hovering near a vertex or segment midpoint
9678
- var HOVER_THRESHOLD$1 = 8;
9679
- var MIDPOINT_THRESHOLD$1 = 11;
9705
+ var HOVER_THRESHOLD$1 = 10;
9680
9706
 
9681
9707
  function initLineEditing(gui, ext, hit) {
9682
- var insertionPoint; // not used in this mode
9683
- var dragVertexInfo;
9684
9708
  var hoverVertexInfo;
9685
9709
  var prevClickEvent;
9686
9710
  var prevHoverEvent;
9711
+ var initialArcCount = -1;
9687
9712
  var drawingId = -1; // feature id of path being drawn
9713
+ var alert;
9714
+ var _dragging = false;
9688
9715
 
9689
9716
  function active() {
9690
- return gui.interaction.getMode() == 'edit-lines';
9717
+ return !!alert;
9691
9718
  }
9692
9719
 
9693
9720
  function dragging() {
9694
- return active() && !!dragVertexInfo;
9721
+ return _dragging;
9695
9722
  }
9696
9723
 
9697
9724
  function drawing() {
9698
9725
  return drawingId > -1;
9699
9726
  }
9700
9727
 
9701
- function setHoverVertex(id) {
9702
- var target = hit.getHitTarget();
9703
- hit.setHoverVertex(target.arcs.getVertex2(id));
9728
+ function polygonMode() {
9729
+ return active() && hit.getHitTarget().layer.geometry_type == 'polygon';
9704
9730
  }
9705
9731
 
9706
9732
  function clearHoverVertex() {
@@ -9708,12 +9734,15 @@
9708
9734
  hoverVertexInfo = null;
9709
9735
  }
9710
9736
 
9737
+ gui.addMode('drawing_tool', turnOn, turnOff);
9738
+
9711
9739
  gui.on('interaction_mode_change', function(e) {
9712
- gui.container.findChild('.map-layers').classed('edit-lines', e.mode == 'edit-lines');
9713
- if (e.mode == 'edit-lines') {
9714
- turnOn();
9715
- } else {
9716
- turnOff();
9740
+ gui.container.findChild('.map-layers').classed('drawing', e.mode == 'drawing');
9741
+ var prevMode = gui.getMode();
9742
+ if (e.mode == 'drawing') {
9743
+ gui.enterMode('drawing_tool');
9744
+ } else if (active()) {
9745
+ gui.clearMode();
9717
9746
  }
9718
9747
  }, null, 10); // higher priority than hit control, so turnOff() has correct hit target
9719
9748
 
@@ -9754,63 +9783,116 @@
9754
9783
  }
9755
9784
  });
9756
9785
 
9757
- function turnOn() {}
9786
+ function turnOn() {
9787
+ var target = hit.getHitTarget();
9788
+ var pathStr = polygonMode() ? 'closed paths' : 'paths';
9789
+ var isMac = navigator.userAgent.includes('Mac');
9790
+ var symbol = isMac ? '⌘' : '^';
9791
+ var msg = `Instructions: Click on the map to draw ${pathStr}. Drag vertices to reshape a path. Type ${symbol}Z/${symbol}Y to undo/redo.`;
9792
+ initialArcCount = hit.getHitTarget().arcs.size();
9793
+ alert = showPopupAlert(msg, null, {non_blocking: true, max_width: '360px'});
9794
+ }
9758
9795
 
9759
9796
  function turnOff() {
9760
9797
  finishPath();
9798
+ if (polygonMode()) {
9799
+ finishPolygons();
9800
+ }
9761
9801
  clearDrawingInfo();
9762
- insertionPoint = null;
9802
+ alert.close();
9803
+ alert = null;
9804
+ initialArcCount = -1;
9805
+ if (gui.interaction.getMode() == 'drawing') {
9806
+ // mode change was not initiated by interactive menu -- turn off interactivity
9807
+ gui.interaction.turnOff();
9808
+ }
9809
+ }
9810
+
9811
+ function finish() {
9812
+ if (polygonMode()) {
9813
+ finishPolygons();
9814
+ }
9815
+ }
9816
+
9817
+ function finishPolygons() {
9818
+ // step1: make a polyline layer containing just newly drawn paths
9819
+ var target = hit.getHitTarget();
9820
+ if (target.arcs.size() <= initialArcCount) return; // no paths added
9821
+ var polygonLyr = target.source.layer;
9822
+ var polygonRecords = polygonLyr.data ? polygonLyr.data.getRecords() : null;
9823
+ var templateRecord;
9824
+ if (polygonRecords) {
9825
+ // use one of the newly created records as a template (they should all be the same)
9826
+ templateRecord = polygonRecords.pop();
9827
+ polygonRecords.splice(initialArcCount); // remove new records
9828
+ }
9829
+ var polylineLyr = {
9830
+ geometry_type: 'polyline',
9831
+ shapes: polygonLyr.shapes.splice(initialArcCount) // move new shapes to polyline layer
9832
+ };
9833
+
9834
+ // step2: convert polylines to polygons
9835
+ //
9836
+ // create a temp dataset containing both layers (so original arcs are preserved)
9837
+ var tmp = Object.assign({}, target.source.dataset);
9838
+ tmp.layers = tmp.layers.concat(polylineLyr);
9839
+ var outputLayers = mapshaper.cmd.polygons([polylineLyr], tmp, {});
9840
+
9841
+ // step3: add new polygons to the original polygons
9842
+ outputLayers[0].shapes.forEach(function(shp) {
9843
+ polygonLyr.shapes.push(shp);
9844
+ if (polygonRecords) {
9845
+ polygonRecords.push(internal.copyRecord(templateRecord));
9846
+ }
9847
+ });
9848
+
9849
+ // step4: update map
9850
+ gui.model.updated({arc_count: true});
9763
9851
  }
9764
9852
 
9765
9853
  function clearDrawingInfo() {
9766
9854
  hit.clearDrawingId();
9767
9855
  drawingId = -1;
9768
- dragVertexInfo = hoverVertexInfo = null;
9856
+ hoverVertexInfo = null;
9769
9857
  prevClickEvent = prevHoverEvent = null;
9770
9858
  }
9771
9859
 
9772
9860
  hit.on('dragstart', function(e) {
9773
- if (!active()) return;
9774
- if (insertionPoint) {
9775
- var target = hit.getHitTarget();
9776
- insertVertex$1(target, insertionPoint.i, insertionPoint.point);
9777
- dragVertexInfo = {
9778
- target: target,
9779
- insertion: true,
9780
- point: insertionPoint.point,
9781
- ids: [insertionPoint.i]
9782
- };
9783
- insertionPoint = null;
9784
- } else if (!drawing()) {
9785
- dragVertexInfo = findDraggableVertices(e);
9786
- }
9787
- if (dragVertexInfo) {
9788
- setHoverVertex(dragVertexInfo.ids[0]);
9861
+ if (!active() || drawing() || !hoverVertexInfo) return;
9862
+ e.originalEvent.stopPropagation();
9863
+ _dragging = true;
9864
+ if (hoverVertexInfo.type == 'interpolated') {
9865
+ insertVertex$1(hit.getHitTarget(), hoverVertexInfo.i, hoverVertexInfo.point);
9866
+ hoverVertexInfo.ids = [hoverVertexInfo.i];
9789
9867
  }
9868
+ hit.setHoverVertex(hoverVertexInfo.displayPoint, hoverVertexInfo.type);
9790
9869
  });
9791
9870
 
9792
9871
  hit.on('drag', function(e) {
9793
9872
  if (!dragging() || drawing()) return;
9873
+ e.originalEvent.stopPropagation();
9794
9874
  var target = hit.getHitTarget();
9795
9875
  var p = ext.translatePixelCoords(e.x, e.y);
9796
9876
  if (gui.keyboard.shiftIsPressed()) {
9797
- internal.snapPointToArcEndpoint(p, dragVertexInfo.ids, target.arcs);
9877
+ internal.snapPointToArcEndpoint(p, hoverVertexInfo.ids, target.arcs);
9798
9878
  }
9799
- internal.snapVerticesToPoint(dragVertexInfo.ids, p, target.arcs);
9800
- setHoverVertex(dragVertexInfo.ids[0]);
9879
+ internal.snapVerticesToPoint(hoverVertexInfo.ids, p, target.arcs);
9880
+ hit.setHoverVertex(p, '');
9881
+
9801
9882
  // redrawing the whole map updates the data layer as well as the overlay layer
9802
9883
  // gui.dispatchEvent('map-needs-refresh');
9803
9884
  });
9804
9885
 
9805
9886
  hit.on('dragend', function(e) {
9806
9887
  if (!dragging()) return;
9888
+ _dragging = false;
9807
9889
  // kludge to get dataset to recalculate internal bounding boxes
9808
- hit.getHitTarget().arcs.transformPoints(function() {});
9890
+ var target = hit.getHitTarget();
9891
+ target.arcs.transformPoints(function() {});
9892
+ updateVertexCoords(target, hoverVertexInfo.ids);
9893
+ gui.dispatchEvent('vertex_dragend', hoverVertexInfo);
9894
+ gui.dispatchEvent('map-needs-refresh'); // redraw basemap
9809
9895
  clearHoverVertex();
9810
- updateVertexCoords(dragVertexInfo.target, dragVertexInfo.ids);
9811
- gui.dispatchEvent('vertex_dragend', dragVertexInfo);
9812
- gui.dispatchEvent('map-needs-refresh');
9813
- dragVertexInfo = null;
9814
9896
  });
9815
9897
 
9816
9898
  // shift + double-click deletes a vertex (when not drawing)
@@ -9823,53 +9905,38 @@
9823
9905
  // now: second click is suppressed
9824
9906
  // deleteLastVertex(hit.getHitTarget());
9825
9907
  finishPath();
9826
- e.stopPropagation(); // prevent dblclick zoom
9908
+ e.originalEvent.stopPropagation(); // prevent dblclick zoom
9827
9909
  return;
9828
9910
  }
9829
9911
  });
9830
9912
 
9831
9913
  // hover event highlights the nearest point in close proximity to the pointer
9832
- // ... or the closest segment midpoint (for adding a new vertex)
9914
+ // ... or the closest point along the segment (for adding a new vertex)
9833
9915
  hit.on('hover', function(e) {
9834
9916
  if (!active() || dragging()) return;
9835
- if (drawing() && !e.overMap) {
9836
- finishPath();
9837
- return;
9838
- }
9839
9917
  if (drawing()) {
9918
+ if (!e.overMap) {
9919
+ finishPath();
9920
+ return;
9921
+ }
9840
9922
  if (gui.keyboard.shiftIsPressed()) {
9841
9923
  alignPointerPosition(e, prevClickEvent);
9842
9924
  }
9843
9925
  updatePathEndpoint(pixToDataCoords(e.x, e.y));
9844
-
9845
- // highlight nearby snappable vertex (the closest vertex on a nearby line,
9846
- // or the first vertex of the current drawing path if not near a line)
9847
- hoverVertexInfo = e.id >= 0 && findDraggableVertices(e) || findPathStartInfo(e);
9848
- if (hoverVertexInfo) {
9849
- // hovering near a vertex: highlight the vertex
9850
- setHoverVertex(hoverVertexInfo.ids[0]);
9851
- } else {
9852
- clearHoverVertex();
9853
- }
9854
- prevHoverEvent = e;
9855
- return;
9856
9926
  }
9857
- if (e.id >= 0 === false) {
9858
- // pointer is not near a path
9859
- return;
9860
- }
9861
- hoverVertexInfo = findDraggableVertices(e);
9862
- insertionPoint = hoverVertexInfo ? null : findMidpointInsertionPoint(e);
9927
+
9928
+ // highlight nearby snappable vertex (the closest vertex on a nearby line,
9929
+ // or the first vertex of the current drawing path if not near a line)
9930
+ hoverVertexInfo = e.id >= 0 && findDraggableVertices(e) ||
9931
+ drawing() && findPathStartInfo(e) ||
9932
+ e.id >= 0 && findInterpolatedPoint(e);
9863
9933
  if (hoverVertexInfo) {
9864
9934
  // hovering near a vertex: highlight the vertex
9865
- setHoverVertex(hoverVertexInfo.ids[0]);
9866
- } else if (insertionPoint) {
9867
- // hovering near a segment midpoint: highlight the midpoint
9868
- hit.setHoverVertex(insertionPoint.displayPoint);
9935
+ hit.setHoverVertex(hoverVertexInfo.displayPoint, hoverVertexInfo.type);
9869
9936
  } else {
9870
- // pointer is not over a vertex: clear any hover effect
9871
9937
  clearHoverVertex();
9872
9938
  }
9939
+ prevHoverEvent = e;
9873
9940
  }, null, 100);
9874
9941
 
9875
9942
  // click starts or extends a new path
@@ -9899,6 +9966,7 @@
9899
9966
  }
9900
9967
  });
9901
9968
 
9969
+ // detect second 'click' event of a double-click action
9902
9970
  function detectDoubleClick(evt) {
9903
9971
  if (!prevClickEvent) return false;
9904
9972
  var elapsed = evt.time - prevClickEvent.time;
@@ -9939,8 +10007,8 @@
9939
10007
  var x0 = prevEvt.x;
9940
10008
  var y0 = prevEvt.y;
9941
10009
  var dist = geom.distance2D(thisEvt.x, thisEvt.y, x0, y0);
9942
- var dist2 = dist / Math.sqrt(2);
9943
10010
  if (dist < 1) return;
10011
+ var dist2 = dist / Math.sqrt(2);
9944
10012
  var minDist = Infinity;
9945
10013
  var cands = [
9946
10014
  {x: x0, y: y0 + dist},
@@ -9976,23 +10044,20 @@
9976
10044
  gui.model.updated({arc_count: true});
9977
10045
  }
9978
10046
 
9979
- function startPath(p) {
10047
+ // p: [x, y] source data coordinates
10048
+ function startPath(p2) {
9980
10049
  var target = hit.getHitTarget();
9981
- var p1 = hoverVertexInfo ? getVertexCoords(target, hoverVertexInfo.ids[0]) : p;
9982
- var p2 = p;
10050
+ var p1 = hoverVertexInfo ? hoverVertexInfo.point : p2;
9983
10051
  appendNewPath(target, p1, p2);
9984
10052
  gui.dispatchEvent('path_add', {target, p1, p2});
9985
10053
  drawingId = target.layer.shapes.length - 1;
9986
10054
  hit.setDrawingId(drawingId);
9987
10055
  }
9988
10056
 
10057
+ // p: [x, y] source data coordinates
9989
10058
  function extendPath(p) {
9990
10059
  var target = hit.getHitTarget();
9991
- var len = getLastArcLength(target);
9992
- if (false && len == 2) {
9993
- var pathCoords = getLastArcCoords(target);
9994
- gui.dispatchEvent('path_add', {target, p1: pathCoords[0], p2: pathCoords[1]});
9995
- } else if (len >= 2) {
10060
+ if (getLastArcLength(target) >= 2) {
9996
10061
  gui.dispatchEvent('path_extend', {target, p});
9997
10062
  }
9998
10063
  appendVertex$1(target, p);
@@ -10004,7 +10069,7 @@
10004
10069
  var target = hit.getHitTarget();
10005
10070
  var i = target.arcs.getPointCount() - 1;
10006
10071
  if (hoverVertexInfo) {
10007
- p = getVertexCoords(target, hoverVertexInfo.ids[0]); // snap to selected point
10072
+ p = hoverVertexInfo.point; // snap to selected point
10008
10073
  }
10009
10074
  setVertexCoords(target, [i], p);
10010
10075
  hit.triggerChangeEvent();
@@ -10014,9 +10079,9 @@
10014
10079
  var target = hit.getHitTarget();
10015
10080
  var arcId = target.arcs.size() - 1;
10016
10081
  var data = target.arcs.getVertexData();
10017
- var id = data.ii[arcId];
10018
- var x = data.xx[id];
10019
- var y = data.yy[id];
10082
+ var i = data.ii[arcId];
10083
+ var x = data.xx[i];
10084
+ var y = data.yy[i];
10020
10085
  var p = ext.translatePixelCoords(e.x, e.y);
10021
10086
  var dist = geom.distance2D(p[0], p[1], x, y);
10022
10087
  var pathLen = data.nn[arcId];
@@ -10024,9 +10089,9 @@
10024
10089
  if (pixelDist > HOVER_THRESHOLD$1 || pathLen < 4) {
10025
10090
  return null;
10026
10091
  }
10027
- var point = translateDisplayPoint([x, y]);
10092
+ var point = translateDisplayPoint(target, [x, y]);
10028
10093
  return {
10029
- target, ids: [id], extendable: false, point
10094
+ target, ids: [i], extendable: false, point, displayPoint: [x, y], type: 'vertex'
10030
10095
  };
10031
10096
  }
10032
10097
 
@@ -10049,50 +10114,48 @@
10049
10114
  // (which could be extended by a newly drawn path)
10050
10115
  var extendable = ids.length == 1 &&
10051
10116
  internal.vertexIsArcEndpoint(ids[0], target.arcs);
10052
- return {target, ids, extendable, point};
10117
+ var displayPoint = target.arcs.getVertex2(ids[0]);
10118
+ return {target, ids, extendable, point, displayPoint, type: 'vertex'};
10053
10119
  }
10054
10120
 
10055
- function findMidpointInsertionPoint(e) {
10121
+ function findInterpolatedPoint(e) {
10056
10122
  var target = hit.getHitTarget();
10057
10123
  //// vertex insertion not supported with simplification
10058
10124
  // if (!target.arcs.isFlat()) return null;
10059
10125
  var p = ext.translatePixelCoords(e.x, e.y);
10060
- var midpoint = findNearestMidpoint$1(p, e.id, target);
10061
- if (!midpoint ||
10062
- midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD$1) return null;
10063
- return midpoint;
10064
- }
10065
- }
10066
-
10126
+ var minDist = Infinity;
10127
+ var shp = target.layer.shapes[e.id];
10128
+ var closest;
10129
+ internal.forEachSegmentInShape(shp, target.arcs, function(i, j, xx, yy) {
10130
+ var x1 = xx[i],
10131
+ y1 = yy[i],
10132
+ x2 = xx[j],
10133
+ y2 = yy[j],
10134
+ // switching from midpoint to nearest point to the mouse
10135
+ // cx = (x1 + x2) / 2,
10136
+ // cy = (y1 + y2) / 2,
10137
+ // p2 = [cx, cy],
10138
+ p2 = internal.findClosestPointOnSeg(p[0], p[1], x1, y1, x2, y2, 0),
10139
+ dist = geom.distance2D(p2[0], p2[1], p[0], p[1]);
10140
+ if (dist < minDist) {
10141
+ minDist = dist;
10142
+ closest = {
10143
+ i: (i < j ? i : j) + 1, // insertion vertex id
10144
+ displayPoint: p2,
10145
+ distance: dist
10146
+ };
10147
+ }
10148
+ });
10067
10149
 
10068
- // Given a location @p (e.g. corresponding to the mouse pointer location),
10069
- // find the midpoint of two vertices on @shp suitable for inserting a new vertex
10070
- function findNearestMidpoint$1(p, fid, target) {
10071
- var arcs = target.arcs;
10072
- var shp = target.layer.shapes[fid];
10073
- var minDist = Infinity, v;
10074
- internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) {
10075
- var x1 = xx[i],
10076
- y1 = yy[i],
10077
- x2 = xx[j],
10078
- y2 = yy[j],
10079
- cx = (x1 + x2) / 2,
10080
- cy = (y1 + y2) / 2,
10081
- midpoint = [cx, cy],
10082
- dist = geom.distance2D(cx, cy, p[0], p[1]);
10083
- if (dist < minDist) {
10084
- minDist = dist;
10085
- v = {
10086
- i: (i < j ? i : j) + 1, // insertion point
10087
- segment: [i, j],
10088
- segmentLen: geom.distance2D(x1, y1, x2, y2),
10089
- displayPoint: midpoint,
10090
- point: translateDisplayPoint(target, midpoint),
10091
- distance: dist
10092
- };
10150
+ if (closest.distance / ext.getPixelSize() > HOVER_THRESHOLD$1) {
10151
+ return null;
10093
10152
  }
10094
- });
10095
- return v || null;
10153
+ closest.point = translateDisplayPoint(target, closest.displayPoint);
10154
+ closest.type = 'interpolated';
10155
+ closest.target = target;
10156
+ return closest;
10157
+ }
10158
+
10096
10159
  }
10097
10160
 
10098
10161
  function initPointDrawing(gui, ext, hit) {
@@ -10839,7 +10902,7 @@
10839
10902
  var iter = new internal.ShapeIter(arcs);
10840
10903
  var t = getScaledTransform(_ext);
10841
10904
  var bounds = _ext.getBounds();
10842
- var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext, style);
10905
+ var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 1.8) * GUI.getPixelRatio() * getScaledLineScale(_ext, style);
10843
10906
  var color = style.strokeColor || 'black';
10844
10907
 
10845
10908
  var i, j, p;
@@ -10863,7 +10926,8 @@
10863
10926
  _ctx.beginPath();
10864
10927
  _ctx.fillStyle = style.vertex_overlay_color || 'black';
10865
10928
  p = style.vertex_overlay;
10866
- drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 2, _ctx);
10929
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius *
10930
+ (style.vertex_overlay_scale || 2), _ctx);
10867
10931
  _ctx.fill();
10868
10932
  _ctx.closePath();
10869
10933
  }
package/www/mapshaper.js CHANGED
@@ -3208,13 +3208,13 @@
3208
3208
  // Used by mapshaper-undershoots.js
3209
3209
  // TODO: make more robust, make sure result is compatible with segmentIntersection()
3210
3210
  // (rounding errors currently must be handled downstream)
3211
- function findClosestPointOnSeg(px, py, ax, ay, bx, by) {
3211
+ function findClosestPointOnSeg(px, py, ax, ay, bx, by, snapArg) {
3212
3212
  var dx = bx - ax,
3213
3213
  dy = by - ay,
3214
3214
  dotp = (px - ax) * dx + (py - ay) * dy,
3215
3215
  abSq = dx * dx + dy * dy,
3216
3216
  k = abSq === 0 ? -1 : dotp / abSq,
3217
- eps = 0.1, // 1e-6, // snap to endpoint
3217
+ eps = snapArg >= 0 ? snapArg : 0.1, // 1e-6, // snap to endpoint
3218
3218
  p;
3219
3219
  if (k <= eps) {
3220
3220
  p = [ax, ay];
@@ -45492,7 +45492,7 @@ ${svg}
45492
45492
  });
45493
45493
  }
45494
45494
 
45495
- var version = "0.6.71";
45495
+ var version = "0.6.73";
45496
45496
 
45497
45497
  // Parse command line args into commands and run them
45498
45498
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -46238,6 +46238,7 @@ ${svg}
46238
46238
  Rounding,
46239
46239
  RunCommands,
46240
46240
  Scalebar,
46241
+ SegmentGeom,
46241
46242
  SegmentIntersection,
46242
46243
  ShapeIter$1,
46243
46244
  ShapeUtils,
package/www/page.css CHANGED
@@ -276,7 +276,15 @@ body {
276
276
  left: 0;
277
277
  }
278
278
 
279
- .alert-wrapper p.error-message {
279
+ .alert-wrapper.non-blocking {
280
+ pointer-events: none;
281
+ }
282
+
283
+ .non-blocking > div {
284
+ pointer-events: auto;
285
+ }
286
+
287
+ .alert-wrapper p.alert-message {
280
288
  margin: 1px 0 0 0;
281
289
  }
282
290
 
@@ -286,7 +294,7 @@ body {
286
294
  margin-bottom: 7px;
287
295
  }
288
296
 
289
- div.alert-title, div.error-message {
297
+ div.alert-title, div.alert-message {
290
298
  font-size: 16px;
291
299
  }
292
300
 
@@ -1194,7 +1202,7 @@ div.basemap-style-btn.active img {
1194
1202
  }
1195
1203
 
1196
1204
  .map-layers.add-points,
1197
- .map-layers.edit-lines {
1205
+ .map-layers.drawing {
1198
1206
  cursor: crosshair;
1199
1207
  }
1200
1208