mapshaper 0.7.63 → 0.7.65

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
@@ -27981,6 +27981,12 @@
27981
27981
  // invariant under translation, rotation and scaling (Hobby 1986), which is
27982
27982
  // what lets the tool fit in display coordinates while storing knots in map
27983
27983
  // coordinates.
27984
+ //
27985
+ // Left alone, the method bulges: a hard turn followed by a much shorter
27986
+ // segment throws the long segment into a wide arc, several times the length of
27987
+ // the short one. Two limits below hold that in, MAX_HANDLE and
27988
+ // MAX_CHORD_RATIO. Both are inert on a path whose chords are of comparable
27989
+ // length, so an ordinary label path is fitted exactly as Hobby describes.
27984
27990
 
27985
27991
  // Curl controls how the curve behaves at the two ends of a run. At 0 it
27986
27992
  // approaches a straight line there; at 1 (Metafont's default) it approaches a
@@ -28068,6 +28074,24 @@
28068
28074
  VEL_B = 1 / 16,
28069
28075
  VEL_C = (3 - Math.sqrt(5)) / 2;
28070
28076
 
28077
+ // Longest a control point may sit from its knot, as a fraction of the chord
28078
+ // length that handleLimit() works out for it. See "Bulge" in the design doc: a
28079
+ // hard turn followed by a short segment otherwise throws the long segment into
28080
+ // a wide arc, which during editing is most of the time, because a short
28081
+ // segment is what a half-dragged knot leaves behind.
28082
+ //
28083
+ // The value is a floor, not a preference. Four points on a circle want 0.3905,
28084
+ // which is the roundest any sensible path asks for, so a limit just above it
28085
+ // never touches a curve that was not already misbehaving.
28086
+ var MAX_HANDLE = 0.4;
28087
+
28088
+ // How much longer one chord may be than its neighbour *for the purposes of the
28089
+ // solve*. Hobby equalizes mock curvature across each knot, and a short chord
28090
+ // carries a lot of curvature, so without this a short segment dictates a wide
28091
+ // arc to a long one. Capping the ratio limits how far that reaches without
28092
+ // moving any knot or touching the curve where the chords are even.
28093
+ var MAX_CHORD_RATIO = 1.5;
28094
+
28071
28095
  // Fits a run of knots, returning a cubic per interval.
28072
28096
  function fitRun(pts) {
28073
28097
  var n = pts.length - 1, // segment count
@@ -28084,16 +28108,54 @@
28084
28108
  psi.push(0);
28085
28109
  for (i = 1; i < n; i++) psi.push(wrapAngle(om[i] - om[i - 1]));
28086
28110
 
28087
- theta = solveDepartureAngles(dd, psi, n);
28111
+ theta = solveDepartureAngles(evenOutChords(dd, n), psi, n);
28088
28112
  phi = getArrivalAngles(theta, psi, n);
28089
28113
 
28090
28114
  for (i = 0; i < n; i++) {
28091
28115
  segments.push(buildSegment(pts[i], pts[i + 1], dd[i], om[i],
28092
- theta[i], phi[i + 1]));
28116
+ theta[i], phi[i + 1],
28117
+ handleLimit(dd[i], i > 0 ? dd[i - 1] : Infinity),
28118
+ handleLimit(dd[i], i < n - 1 ? dd[i + 1] : Infinity)));
28093
28119
  }
28094
28120
  return segments;
28095
28121
  }
28096
28122
 
28123
+ // Longest a handle may be at one end of a segment whose own chord is @own and
28124
+ // whose neighbour across that knot is @neighbor. (At the first and last knot
28125
+ // of a run there is no neighbour, and the segment's own chord is the measure.)
28126
+ //
28127
+ // The scale is the geometric mean of the two, rather than the shorter of them.
28128
+ // Measuring against the shorter alone holds the bulge down just as well, but
28129
+ // it makes a long segment meeting a short one spend nearly all of its shape at
28130
+ // its far end: on a 490-unit arm meeting a 50-unit leg the two handles came
28131
+ // out 9.8:1, so the arm ran almost straight and then hooked hard into the
28132
+ // knot, and the curvature either side of that knot differed by a factor of 14.
28133
+ // The geometric mean gives the long segment a handle in proportion to how
28134
+ // lopsided the pair actually is, which brings the same case to 3.1:1 and the
28135
+ // curvature to within a factor of 1.4 -- about what Hobby's mock curvature
28136
+ // leaves on an ordinary path anyway -- for two points of extra bulge.
28137
+ function handleLimit(own, neighbor) {
28138
+ return MAX_HANDLE * Math.sqrt(own * Math.min(own, neighbor));
28139
+ }
28140
+
28141
+ // The chord lengths the solve sees, with the ratio between neighbours held to
28142
+ // MAX_CHORD_RATIO. Each length becomes the smallest that any chord permits it
28143
+ // to be, allowing for a factor of MAX_CHORD_RATIO per step -- a forward pass
28144
+ // and a backward pass compute that exactly, so this needs no iteration.
28145
+ //
28146
+ // Only the tangent directions are affected. The curve is still built on the
28147
+ // true chord lengths.
28148
+ function evenOutChords(dd, n) {
28149
+ var out = dd.concat(), i;
28150
+ for (i = 1; i < n; i++) {
28151
+ if (out[i] > out[i - 1] * MAX_CHORD_RATIO) out[i] = out[i - 1] * MAX_CHORD_RATIO;
28152
+ }
28153
+ for (i = n - 2; i >= 0; i--) {
28154
+ if (out[i] > out[i + 1] * MAX_CHORD_RATIO) out[i] = out[i + 1] * MAX_CHORD_RATIO;
28155
+ }
28156
+ return out;
28157
+ }
28158
+
28097
28159
  // The tridiagonal system of Hobby's mock-curvature conditions, in the
28098
28160
  // departure angle at each knot measured from the chord leaving it. Written for
28099
28161
  // unit tension throughout; the general form carries a tension per knot, which
@@ -28137,21 +28199,30 @@
28137
28199
 
28138
28200
  // Turns a pair of angles into a cubic, by way of Hobby's velocity functions --
28139
28201
  // how far along each tangent the control point sits, as a multiple of a third
28140
- // of the chord.
28141
- function buildSegment(p0, p1, d, om, theta, phi) {
28202
+ // of the chord -- with each distance held to the caller's limit.
28203
+ //
28204
+ // Shortening a handle leaves its direction alone, so the tangent the two
28205
+ // segments share at a knot is untouched and the curve stays smooth through it.
28206
+ // What it gives up is Hobby's mock-curvature match across that knot, and only
28207
+ // at a knot lopsided enough for the limit to bite.
28208
+ function buildSegment(p0, p1, d, om, theta, phi, maxOut, maxIn) {
28142
28209
  var alpha = VEL_A * (Math.sin(theta) - VEL_B * Math.sin(phi)) *
28143
28210
  (Math.sin(phi) - VEL_B * Math.sin(theta)) *
28144
28211
  (Math.cos(theta) - Math.cos(phi)),
28145
28212
  rho = (2 + alpha) /
28146
28213
  (1 + (1 - VEL_C) * Math.cos(theta) + VEL_C * Math.cos(phi)),
28147
28214
  sigma = (2 - alpha) /
28148
- (1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta));
28215
+ (1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta)),
28216
+ out = d * rho / 3,
28217
+ arr = d * sigma / 3;
28218
+ if (out > maxOut) out = maxOut;
28219
+ if (arr > maxIn) arr = maxIn;
28149
28220
  return {
28150
28221
  p0: p0,
28151
- c1: [p0[0] + d * rho / 3 * Math.cos(theta + om),
28152
- p0[1] + d * rho / 3 * Math.sin(theta + om)],
28153
- c2: [p1[0] - d * sigma / 3 * Math.cos(om - phi),
28154
- p1[1] - d * sigma / 3 * Math.sin(om - phi)],
28222
+ c1: [p0[0] + out * Math.cos(theta + om),
28223
+ p0[1] + out * Math.sin(theta + om)],
28224
+ c2: [p1[0] - arr * Math.cos(om - phi),
28225
+ p1[1] - arr * Math.sin(om - phi)],
28155
28226
  p3: p1
28156
28227
  };
28157
28228
  }
@@ -82718,7 +82789,7 @@ ${svg}
82718
82789
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
82719
82790
  }
82720
82791
 
82721
- var version = "0.7.63";
82792
+ var version = "0.7.65";
82722
82793
 
82723
82794
  // Parse command line args into commands and run them
82724
82795
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.63",
3
+ "version": "0.7.65",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -16663,10 +16663,15 @@
16663
16663
  // aligned: whether it carries a label-align (see below)
16664
16664
  // delta: {dx, dy} -- how far the pointer has moved, in the same space.
16665
16665
  //
16666
- // Returns {dx, dy, text-anchor, x}, where x is where the text will be drawn
16667
- // once the other three are written -- the same as dx except on an aligned
16668
- // label. The three have to be written together: applying dx without its
16669
- // text-anchor moves the text by half its own width or by all of it.
16666
+ // Returns two descriptions of the same placement:
16667
+ //
16668
+ // dx, dy, text-anchor: what the drag writes to the record. The three have to
16669
+ // be written together: applying dx without its text-anchor moves the text
16670
+ // by half its own width or by all of it.
16671
+ // x, anchor: where the text sits and how it is justified while it is drawn,
16672
+ // which is what the preview writes onto the rendered label. The same as
16673
+ // dx and text-anchor except on an aligned label, whose stored offset is
16674
+ // measured against a justification it is not drawn with.
16670
16675
  function getOffsetDragValues(start, delta) {
16671
16676
  var width = start.width > 0 ? start.width : 0;
16672
16677
  var drawnAnchor = normalizeAnchor(start.anchor);
@@ -16685,12 +16690,18 @@
16685
16690
  // if the alignment were later removed.
16686
16691
  var anchor = start.aligned ? 'start' :
16687
16692
  width > 0 ? getAnchorForCentre(left + width / 2, width) : drawnAnchor;
16693
+ // An aligned label goes on being drawn with the anchor its alignment gives
16694
+ // it, whatever the drag writes: the alignment is unchanged, so the renderer
16695
+ // will justify it the same way afterwards. Drawing it as 'start' instead
16696
+ // would slide the text right by half its width or by all of it for the
16697
+ // length of the drag, and drop it back on release.
16698
+ var drawn = start.aligned ? drawnAnchor : anchor;
16688
16699
  return {
16689
16700
  dx: round$1(left + anchorOffsets[anchor] * width),
16690
16701
  dy: round$1(start.dy + delta.dy),
16691
16702
  'text-anchor': anchor,
16692
- x: round$1(start.aligned ? start.dx + delta.dx :
16693
- left + anchorOffsets[anchor] * width)
16703
+ x: round$1(left + anchorOffsets[drawn] * width),
16704
+ anchor: drawn
16694
16705
  };
16695
16706
  }
16696
16707
 
@@ -17070,8 +17081,12 @@
17070
17081
  applyInlineCss(cssInput.node().value.trim());
17071
17082
  });
17072
17083
 
17084
+ // The caption for the grid and the drag-mode toggle below it, and what
17085
+ // gives Fixed|Draggable something to be an adjective of. "Position" read as
17086
+ // the label's own position -- which a drag moves in either mode -- and made
17087
+ // Fixed sound like a lock on a label that is still perfectly draggable.
17073
17088
  var positionRow = El('label').addClass('label-style-row').appendTo(textSection);
17074
- El('span').appendTo(positionRow).text('Position');
17089
+ El('span').appendTo(positionRow).text('Offset from anchor');
17075
17090
 
17076
17091
  // Whether the label has a symbol is one question and which symbol it has is
17077
17092
  // another, so the first is a switch on the section's heading rather than a
@@ -25332,6 +25347,9 @@
25332
25347
  // See docs/development/label-tool-design.md.
25333
25348
 
25334
25349
  var SVG_NS$2 = 'http://www.w3.org/2000/svg';
25350
+ // How far outside its text a label's outline is drawn. Exported because the
25351
+ // tool grabs a label by the same box: what looks like the object is what takes
25352
+ // a drag on it.
25335
25353
  var BOX_PADDING = 3;
25336
25354
  var ANCHOR_RADIUS = 3.5;
25337
25355
  var KNOT_RADIUS = 3;
@@ -26524,7 +26542,10 @@
26524
26542
  // ended.
26525
26543
  setMultilineAttribute(nodes.text, 'x', o.values.x);
26526
26544
  nodes.text.setAttribute('y', o.values.dy);
26527
- nodes.text.setAttribute('text-anchor', o.values['text-anchor']);
26545
+ // The anchor the label is drawn with, not the one the drag writes: on an
26546
+ // aligned label the two differ, and the stored one belongs with the stored
26547
+ // dx rather than with the x being previewed here.
26548
+ nodes.text.setAttribute('text-anchor', o.values.anchor);
26528
26549
  // The cue is drawn around the text rather than moved with it, so it has to
26529
26550
  // be rebuilt to follow -- one getBBox on one label per mouse move.
26530
26551
  selection.refresh(true);
@@ -26679,9 +26700,34 @@
26679
26700
  hoverTextId = id > -1 && (!hoverHandle || glyphsOutrankHandle(id)) ? id : -1;
26680
26701
  }
26681
26702
 
26703
+ // The exception is about the pointer being on the glyphs, not about the mode:
26704
+ // an offset label's anchor is out from under its text, and taking the handle
26705
+ // away there left it with no anchor handle at all -- dragging the symbol of a
26706
+ // label positioned ne slid its text instead of moving the label.
26682
26707
  function glyphsOutrankHandle(id) {
26683
26708
  return hoverHandle.id == id && labelTextIsDraggable(gui) &&
26684
- !isPathLabel(hit.getHitTarget(), id);
26709
+ !isPathLabel(hit.getHitTarget(), id) && pointerIsOverText(id);
26710
+ }
26711
+
26712
+ // Whether the pointer is within the box drawn around a label's text -- the
26713
+ // outline the selection cue draws, padding and all, so that the thing on
26714
+ // screen that says "this is the object" is the thing that takes the drag.
26715
+ //
26716
+ // From the last hover rather than from an event, because this is asked from
26717
+ // the hit control's 'change' as well, which arrives without a position; the
26718
+ // pointer has not moved since the hover that preceded it.
26719
+ function pointerIsOverText(id) {
26720
+ var target = hit.getHitTarget();
26721
+ var shapes = target && getDisplayShapes(target);
26722
+ var shp = shapes && shapes[id];
26723
+ var nodes = findLabelNodes(target, id);
26724
+ var box = nodes && !nodes.textPath ? measureNode(nodes.text) : null;
26725
+ var pix = hoverPoint && ext.translateCoords(hoverPoint[0], hoverPoint[1]);
26726
+ var p;
26727
+ if (!box || !pix || !shp) return false;
26728
+ p = getLabelSpacePoint(shp[0], {x: pix[0], y: pix[1]});
26729
+ return p.x >= box.x - BOX_PADDING && p.x <= box.x + box.width + BOX_PADDING &&
26730
+ p.y >= box.y - BOX_PADDING && p.y <= box.y + box.height + BOX_PADDING;
26685
26731
  }
26686
26732
 
26687
26733
  // The label a drag on the glyphs would act on, or -1. Only a selected label
package/www/mapshaper.js CHANGED
@@ -27981,6 +27981,12 @@
27981
27981
  // invariant under translation, rotation and scaling (Hobby 1986), which is
27982
27982
  // what lets the tool fit in display coordinates while storing knots in map
27983
27983
  // coordinates.
27984
+ //
27985
+ // Left alone, the method bulges: a hard turn followed by a much shorter
27986
+ // segment throws the long segment into a wide arc, several times the length of
27987
+ // the short one. Two limits below hold that in, MAX_HANDLE and
27988
+ // MAX_CHORD_RATIO. Both are inert on a path whose chords are of comparable
27989
+ // length, so an ordinary label path is fitted exactly as Hobby describes.
27984
27990
 
27985
27991
  // Curl controls how the curve behaves at the two ends of a run. At 0 it
27986
27992
  // approaches a straight line there; at 1 (Metafont's default) it approaches a
@@ -28068,6 +28074,24 @@
28068
28074
  VEL_B = 1 / 16,
28069
28075
  VEL_C = (3 - Math.sqrt(5)) / 2;
28070
28076
 
28077
+ // Longest a control point may sit from its knot, as a fraction of the chord
28078
+ // length that handleLimit() works out for it. See "Bulge" in the design doc: a
28079
+ // hard turn followed by a short segment otherwise throws the long segment into
28080
+ // a wide arc, which during editing is most of the time, because a short
28081
+ // segment is what a half-dragged knot leaves behind.
28082
+ //
28083
+ // The value is a floor, not a preference. Four points on a circle want 0.3905,
28084
+ // which is the roundest any sensible path asks for, so a limit just above it
28085
+ // never touches a curve that was not already misbehaving.
28086
+ var MAX_HANDLE = 0.4;
28087
+
28088
+ // How much longer one chord may be than its neighbour *for the purposes of the
28089
+ // solve*. Hobby equalizes mock curvature across each knot, and a short chord
28090
+ // carries a lot of curvature, so without this a short segment dictates a wide
28091
+ // arc to a long one. Capping the ratio limits how far that reaches without
28092
+ // moving any knot or touching the curve where the chords are even.
28093
+ var MAX_CHORD_RATIO = 1.5;
28094
+
28071
28095
  // Fits a run of knots, returning a cubic per interval.
28072
28096
  function fitRun(pts) {
28073
28097
  var n = pts.length - 1, // segment count
@@ -28084,16 +28108,54 @@
28084
28108
  psi.push(0);
28085
28109
  for (i = 1; i < n; i++) psi.push(wrapAngle(om[i] - om[i - 1]));
28086
28110
 
28087
- theta = solveDepartureAngles(dd, psi, n);
28111
+ theta = solveDepartureAngles(evenOutChords(dd, n), psi, n);
28088
28112
  phi = getArrivalAngles(theta, psi, n);
28089
28113
 
28090
28114
  for (i = 0; i < n; i++) {
28091
28115
  segments.push(buildSegment(pts[i], pts[i + 1], dd[i], om[i],
28092
- theta[i], phi[i + 1]));
28116
+ theta[i], phi[i + 1],
28117
+ handleLimit(dd[i], i > 0 ? dd[i - 1] : Infinity),
28118
+ handleLimit(dd[i], i < n - 1 ? dd[i + 1] : Infinity)));
28093
28119
  }
28094
28120
  return segments;
28095
28121
  }
28096
28122
 
28123
+ // Longest a handle may be at one end of a segment whose own chord is @own and
28124
+ // whose neighbour across that knot is @neighbor. (At the first and last knot
28125
+ // of a run there is no neighbour, and the segment's own chord is the measure.)
28126
+ //
28127
+ // The scale is the geometric mean of the two, rather than the shorter of them.
28128
+ // Measuring against the shorter alone holds the bulge down just as well, but
28129
+ // it makes a long segment meeting a short one spend nearly all of its shape at
28130
+ // its far end: on a 490-unit arm meeting a 50-unit leg the two handles came
28131
+ // out 9.8:1, so the arm ran almost straight and then hooked hard into the
28132
+ // knot, and the curvature either side of that knot differed by a factor of 14.
28133
+ // The geometric mean gives the long segment a handle in proportion to how
28134
+ // lopsided the pair actually is, which brings the same case to 3.1:1 and the
28135
+ // curvature to within a factor of 1.4 -- about what Hobby's mock curvature
28136
+ // leaves on an ordinary path anyway -- for two points of extra bulge.
28137
+ function handleLimit(own, neighbor) {
28138
+ return MAX_HANDLE * Math.sqrt(own * Math.min(own, neighbor));
28139
+ }
28140
+
28141
+ // The chord lengths the solve sees, with the ratio between neighbours held to
28142
+ // MAX_CHORD_RATIO. Each length becomes the smallest that any chord permits it
28143
+ // to be, allowing for a factor of MAX_CHORD_RATIO per step -- a forward pass
28144
+ // and a backward pass compute that exactly, so this needs no iteration.
28145
+ //
28146
+ // Only the tangent directions are affected. The curve is still built on the
28147
+ // true chord lengths.
28148
+ function evenOutChords(dd, n) {
28149
+ var out = dd.concat(), i;
28150
+ for (i = 1; i < n; i++) {
28151
+ if (out[i] > out[i - 1] * MAX_CHORD_RATIO) out[i] = out[i - 1] * MAX_CHORD_RATIO;
28152
+ }
28153
+ for (i = n - 2; i >= 0; i--) {
28154
+ if (out[i] > out[i + 1] * MAX_CHORD_RATIO) out[i] = out[i + 1] * MAX_CHORD_RATIO;
28155
+ }
28156
+ return out;
28157
+ }
28158
+
28097
28159
  // The tridiagonal system of Hobby's mock-curvature conditions, in the
28098
28160
  // departure angle at each knot measured from the chord leaving it. Written for
28099
28161
  // unit tension throughout; the general form carries a tension per knot, which
@@ -28137,21 +28199,30 @@
28137
28199
 
28138
28200
  // Turns a pair of angles into a cubic, by way of Hobby's velocity functions --
28139
28201
  // how far along each tangent the control point sits, as a multiple of a third
28140
- // of the chord.
28141
- function buildSegment(p0, p1, d, om, theta, phi) {
28202
+ // of the chord -- with each distance held to the caller's limit.
28203
+ //
28204
+ // Shortening a handle leaves its direction alone, so the tangent the two
28205
+ // segments share at a knot is untouched and the curve stays smooth through it.
28206
+ // What it gives up is Hobby's mock-curvature match across that knot, and only
28207
+ // at a knot lopsided enough for the limit to bite.
28208
+ function buildSegment(p0, p1, d, om, theta, phi, maxOut, maxIn) {
28142
28209
  var alpha = VEL_A * (Math.sin(theta) - VEL_B * Math.sin(phi)) *
28143
28210
  (Math.sin(phi) - VEL_B * Math.sin(theta)) *
28144
28211
  (Math.cos(theta) - Math.cos(phi)),
28145
28212
  rho = (2 + alpha) /
28146
28213
  (1 + (1 - VEL_C) * Math.cos(theta) + VEL_C * Math.cos(phi)),
28147
28214
  sigma = (2 - alpha) /
28148
- (1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta));
28215
+ (1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta)),
28216
+ out = d * rho / 3,
28217
+ arr = d * sigma / 3;
28218
+ if (out > maxOut) out = maxOut;
28219
+ if (arr > maxIn) arr = maxIn;
28149
28220
  return {
28150
28221
  p0: p0,
28151
- c1: [p0[0] + d * rho / 3 * Math.cos(theta + om),
28152
- p0[1] + d * rho / 3 * Math.sin(theta + om)],
28153
- c2: [p1[0] - d * sigma / 3 * Math.cos(om - phi),
28154
- p1[1] - d * sigma / 3 * Math.sin(om - phi)],
28222
+ c1: [p0[0] + out * Math.cos(theta + om),
28223
+ p0[1] + out * Math.sin(theta + om)],
28224
+ c2: [p1[0] - arr * Math.cos(om - phi),
28225
+ p1[1] - arr * Math.sin(om - phi)],
28155
28226
  p3: p1
28156
28227
  };
28157
28228
  }
@@ -82718,7 +82789,7 @@ ${svg}
82718
82789
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
82719
82790
  }
82720
82791
 
82721
- var version = "0.7.63";
82792
+ var version = "0.7.65";
82722
82793
 
82723
82794
  // Parse command line args into commands and run them
82724
82795
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.