mapshaper 0.7.63 → 0.7.64

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.64";
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.64",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
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.64";
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.