mapshaper 0.7.28 → 0.7.29

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
@@ -44900,7 +44900,7 @@ ${svg}
44900
44900
  message('[medial] sample sites: ' + sites.coords.length);
44901
44901
  }
44902
44902
  profileStart('medial:computeSegments');
44903
- var medial = computeMedialSegments(sites, coordDistances);
44903
+ var medial = computeMedialSegments(sites, coordDistances, sites.grid);
44904
44904
  profileEnd('medial:computeSegments');
44905
44905
  if (medial.segments.length === 0) return null;
44906
44906
  // Stitch the individual Voronoi edges (2-point segments that meet at shared
@@ -45165,7 +45165,11 @@ ${svg}
45165
45165
  // extrapolated as an outward ray). Dropping the touching interior borders and
45166
45166
  // the no-feature coastline shrinks the one remaining Delaunay and avoids
45167
45167
  // building a redundant medial where the source boundary already partitions.
45168
- return keptSites(sites, grid, coordDistances);
45168
+ var kept = keptSites(sites, grid, coordDistances);
45169
+ // Keep the segment grid with the sites so computeMedialSegments can re-measure
45170
+ // the true source gap when the sample-pair proximity test is too coarse.
45171
+ kept.grid = grid;
45172
+ return kept;
45169
45173
  }
45170
45174
 
45171
45175
  // Bucket every boundary segment into a uniform grid so the nearest cross-feature
@@ -45280,6 +45284,50 @@ ${svg}
45280
45284
  return best;
45281
45285
  }
45282
45286
 
45287
+ // True when medial vertex c lies in the buffer overlap of features fp and fq:
45288
+ // within fp's radius of an fp-owned source segment AND within fq's radius of an
45289
+ // fq-owned source segment. Measured against the actual source segments via the
45290
+ // grid, so it is correct regardless of how coarsely the banks were sampled --
45291
+ // unlike the sample-pair distance, which overestimates the gap when the nearest
45292
+ // samples on opposite banks are staggered or far from the true closest approach.
45293
+ // Slack on each reach when rescuing a cross-feature edge whose sample endpoints
45294
+ // fell outside the cheap proximity test. It absorbs the discretization of the
45295
+ // medial graph near a pinch point: the connecting Voronoi edge is bounded by the
45296
+ // site spacing (capped at the buffer distance), so a genuinely contested edge can
45297
+ // run up to ~1.5x reach and its medial vertices can land a similar fraction
45298
+ // outside the overlap. 1.3 covers the worst real case observed (~1.18) with
45299
+ // headroom, while spurious edges between sites contested with *other* features
45300
+ // miss by far more (>=1.5 or have no nearby source segment) and stay pruned.
45301
+ var MEDIAL_OVERLAP_SLACK = 1.3;
45302
+
45303
+ function medialVertexInOverlap(ctx, c, fp, fq, rp, rq) {
45304
+ var sp = rp * MEDIAL_OVERLAP_SLACK, sq = rq * MEDIAL_OVERLAP_SLACK;
45305
+ return pointFeatureDistSq(ctx, c[0], c[1], fp) <= sp * sp &&
45306
+ pointFeatureDistSq(ctx, c[0], c[1], fq) <= sq * sq;
45307
+ }
45308
+
45309
+ // Squared distance from (x, y) to the nearest segment owned by feature @feat,
45310
+ // probing the 3x3 grid-cell neighborhood (cell == max reach, so any segment
45311
+ // within a single feature's radius is in the window). Infinity if none.
45312
+ function pointFeatureDistSq(ctx, x, y, feat) {
45313
+ var seg = ctx.seg, grid = ctx.grid;
45314
+ var cx = ctx.colOf(x), cy = ctx.rowOf(y);
45315
+ var best = Infinity;
45316
+ for (var gx = cx - 1; gx <= cx + 1; gx++) {
45317
+ for (var gy = cy - 1; gy <= cy + 1; gy++) {
45318
+ var bucket = grid.get(ctx.cellKey(gx, gy));
45319
+ if (!bucket) continue;
45320
+ for (var b = 0; b < bucket.length; b++) {
45321
+ var s = bucket[b];
45322
+ if (seg.feat[s] !== feat) continue;
45323
+ var d2 = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
45324
+ if (d2 < best) best = d2;
45325
+ }
45326
+ }
45327
+ }
45328
+ return best;
45329
+ }
45330
+
45283
45331
  // Keep only the sites that border a real gap: a different feature within reach
45284
45332
  // (finite gap) but farther than the touching threshold. These are the only sites
45285
45333
  // that can shape the medial axis. Touching/coincident interior borders (gap ~ 0,
@@ -45460,7 +45508,7 @@ ${svg}
45460
45508
  return Math.floor(e / 3);
45461
45509
  }
45462
45510
 
45463
- function computeMedialSegments(sites, coordDistances) {
45511
+ function computeMedialSegments(sites, coordDistances, ctx) {
45464
45512
  var coords = sites.coords;
45465
45513
  var owner = sites.owner;
45466
45514
  profileStart('medial:delaunay');
@@ -45485,18 +45533,28 @@ ${svg}
45485
45533
  var opp = halfedges[e];
45486
45534
  var p = triangles[e];
45487
45535
  var q = triangles[nextHalfedge(e)];
45488
- if (owner[p] === owner[q]) continue;
45536
+ var fp = owner[p], fq = owner[q];
45537
+ if (fp === fq) continue;
45489
45538
  var dx = coords[p][0] - coords[q][0];
45490
45539
  var dy = coords[p][1] - coords[q][1];
45491
45540
  var siteDist = Math.sqrt(dx * dx + dy * dy);
45492
- var reach = coordDistances[owner[p]] + coordDistances[owner[q]];
45493
- // sites whose sources are farther apart than the sum of their radii can
45494
- // never have overlapping buffers, so their bisector is not a contested edge
45495
- if (siteDist > reach) continue;
45541
+ var rp = coordDistances[fp], rq = coordDistances[fq];
45542
+ var reach = rp + rq;
45496
45543
  var t1 = triangleOfEdge(e);
45497
45544
  var c1 = verts[t1];
45498
45545
  if (!c1) continue; // degenerate (near-collinear) triangle
45546
+ // Sites within the sum of their radii are accepted directly; this is the
45547
+ // common, cheap case. When they are farther apart, the bisector might still
45548
+ // be contested -- the nearest sample pair overestimates the true source gap
45549
+ // where banks are sampled coarsely or staggered. Re-measure the actual gap
45550
+ // at the medial vertex against the source segments (the grid) and rescue the
45551
+ // edge if it really lies in the buffer overlap. Without the rescue the medial
45552
+ // axis fragments at such spots, leaving the equidistant cut wall open so the
45553
+ // overlap face is never subdivided and a whole contested corridor is assigned
45554
+ // to one feature (a feature wrapping a neighbor's enclosed island).
45555
+ var near = siteDist <= reach;
45499
45556
  if (opp === -1) {
45557
+ if (!near && !(ctx && medialVertexInOverlap(ctx, c1, fp, fq, rp, rq))) continue;
45500
45558
  // Hull edge: the Voronoi edge here is an unbounded ray (the bisector of
45501
45559
  // two sites on the convex hull). Emit it as an outward ray from the
45502
45560
  // circumcenter so the medial line reaches and crosses the buffer
@@ -45517,6 +45575,9 @@ ${svg}
45517
45575
  var t2 = triangleOfEdge(opp);
45518
45576
  var c2 = verts[t2];
45519
45577
  if (!c2) continue;
45578
+ if (!near && !(ctx &&
45579
+ (medialVertexInOverlap(ctx, c1, fp, fq, rp, rq) ||
45580
+ medialVertexInOverlap(ctx, c2, fp, fq, rp, rq)))) continue;
45520
45581
  var sx = c1[0] - c2[0], sy = c1[1] - c2[1];
45521
45582
  var segLen = Math.sqrt(sx * sx + sy * sy);
45522
45583
  // a real medial edge inside the overlap is short (on the order of the site
@@ -63697,7 +63758,7 @@ ${svg}
63697
63758
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
63698
63759
  }
63699
63760
 
63700
- var version = "0.7.28";
63761
+ var version = "0.7.29";
63701
63762
 
63702
63763
  // Parse command line args into commands and run them
63703
63764
  // 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.28",
3
+ "version": "0.7.29",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/mapshaper.js CHANGED
@@ -44900,7 +44900,7 @@ ${svg}
44900
44900
  message('[medial] sample sites: ' + sites.coords.length);
44901
44901
  }
44902
44902
  profileStart('medial:computeSegments');
44903
- var medial = computeMedialSegments(sites, coordDistances);
44903
+ var medial = computeMedialSegments(sites, coordDistances, sites.grid);
44904
44904
  profileEnd('medial:computeSegments');
44905
44905
  if (medial.segments.length === 0) return null;
44906
44906
  // Stitch the individual Voronoi edges (2-point segments that meet at shared
@@ -45165,7 +45165,11 @@ ${svg}
45165
45165
  // extrapolated as an outward ray). Dropping the touching interior borders and
45166
45166
  // the no-feature coastline shrinks the one remaining Delaunay and avoids
45167
45167
  // building a redundant medial where the source boundary already partitions.
45168
- return keptSites(sites, grid, coordDistances);
45168
+ var kept = keptSites(sites, grid, coordDistances);
45169
+ // Keep the segment grid with the sites so computeMedialSegments can re-measure
45170
+ // the true source gap when the sample-pair proximity test is too coarse.
45171
+ kept.grid = grid;
45172
+ return kept;
45169
45173
  }
45170
45174
 
45171
45175
  // Bucket every boundary segment into a uniform grid so the nearest cross-feature
@@ -45280,6 +45284,50 @@ ${svg}
45280
45284
  return best;
45281
45285
  }
45282
45286
 
45287
+ // True when medial vertex c lies in the buffer overlap of features fp and fq:
45288
+ // within fp's radius of an fp-owned source segment AND within fq's radius of an
45289
+ // fq-owned source segment. Measured against the actual source segments via the
45290
+ // grid, so it is correct regardless of how coarsely the banks were sampled --
45291
+ // unlike the sample-pair distance, which overestimates the gap when the nearest
45292
+ // samples on opposite banks are staggered or far from the true closest approach.
45293
+ // Slack on each reach when rescuing a cross-feature edge whose sample endpoints
45294
+ // fell outside the cheap proximity test. It absorbs the discretization of the
45295
+ // medial graph near a pinch point: the connecting Voronoi edge is bounded by the
45296
+ // site spacing (capped at the buffer distance), so a genuinely contested edge can
45297
+ // run up to ~1.5x reach and its medial vertices can land a similar fraction
45298
+ // outside the overlap. 1.3 covers the worst real case observed (~1.18) with
45299
+ // headroom, while spurious edges between sites contested with *other* features
45300
+ // miss by far more (>=1.5 or have no nearby source segment) and stay pruned.
45301
+ var MEDIAL_OVERLAP_SLACK = 1.3;
45302
+
45303
+ function medialVertexInOverlap(ctx, c, fp, fq, rp, rq) {
45304
+ var sp = rp * MEDIAL_OVERLAP_SLACK, sq = rq * MEDIAL_OVERLAP_SLACK;
45305
+ return pointFeatureDistSq(ctx, c[0], c[1], fp) <= sp * sp &&
45306
+ pointFeatureDistSq(ctx, c[0], c[1], fq) <= sq * sq;
45307
+ }
45308
+
45309
+ // Squared distance from (x, y) to the nearest segment owned by feature @feat,
45310
+ // probing the 3x3 grid-cell neighborhood (cell == max reach, so any segment
45311
+ // within a single feature's radius is in the window). Infinity if none.
45312
+ function pointFeatureDistSq(ctx, x, y, feat) {
45313
+ var seg = ctx.seg, grid = ctx.grid;
45314
+ var cx = ctx.colOf(x), cy = ctx.rowOf(y);
45315
+ var best = Infinity;
45316
+ for (var gx = cx - 1; gx <= cx + 1; gx++) {
45317
+ for (var gy = cy - 1; gy <= cy + 1; gy++) {
45318
+ var bucket = grid.get(ctx.cellKey(gx, gy));
45319
+ if (!bucket) continue;
45320
+ for (var b = 0; b < bucket.length; b++) {
45321
+ var s = bucket[b];
45322
+ if (seg.feat[s] !== feat) continue;
45323
+ var d2 = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
45324
+ if (d2 < best) best = d2;
45325
+ }
45326
+ }
45327
+ }
45328
+ return best;
45329
+ }
45330
+
45283
45331
  // Keep only the sites that border a real gap: a different feature within reach
45284
45332
  // (finite gap) but farther than the touching threshold. These are the only sites
45285
45333
  // that can shape the medial axis. Touching/coincident interior borders (gap ~ 0,
@@ -45460,7 +45508,7 @@ ${svg}
45460
45508
  return Math.floor(e / 3);
45461
45509
  }
45462
45510
 
45463
- function computeMedialSegments(sites, coordDistances) {
45511
+ function computeMedialSegments(sites, coordDistances, ctx) {
45464
45512
  var coords = sites.coords;
45465
45513
  var owner = sites.owner;
45466
45514
  profileStart('medial:delaunay');
@@ -45485,18 +45533,28 @@ ${svg}
45485
45533
  var opp = halfedges[e];
45486
45534
  var p = triangles[e];
45487
45535
  var q = triangles[nextHalfedge(e)];
45488
- if (owner[p] === owner[q]) continue;
45536
+ var fp = owner[p], fq = owner[q];
45537
+ if (fp === fq) continue;
45489
45538
  var dx = coords[p][0] - coords[q][0];
45490
45539
  var dy = coords[p][1] - coords[q][1];
45491
45540
  var siteDist = Math.sqrt(dx * dx + dy * dy);
45492
- var reach = coordDistances[owner[p]] + coordDistances[owner[q]];
45493
- // sites whose sources are farther apart than the sum of their radii can
45494
- // never have overlapping buffers, so their bisector is not a contested edge
45495
- if (siteDist > reach) continue;
45541
+ var rp = coordDistances[fp], rq = coordDistances[fq];
45542
+ var reach = rp + rq;
45496
45543
  var t1 = triangleOfEdge(e);
45497
45544
  var c1 = verts[t1];
45498
45545
  if (!c1) continue; // degenerate (near-collinear) triangle
45546
+ // Sites within the sum of their radii are accepted directly; this is the
45547
+ // common, cheap case. When they are farther apart, the bisector might still
45548
+ // be contested -- the nearest sample pair overestimates the true source gap
45549
+ // where banks are sampled coarsely or staggered. Re-measure the actual gap
45550
+ // at the medial vertex against the source segments (the grid) and rescue the
45551
+ // edge if it really lies in the buffer overlap. Without the rescue the medial
45552
+ // axis fragments at such spots, leaving the equidistant cut wall open so the
45553
+ // overlap face is never subdivided and a whole contested corridor is assigned
45554
+ // to one feature (a feature wrapping a neighbor's enclosed island).
45555
+ var near = siteDist <= reach;
45499
45556
  if (opp === -1) {
45557
+ if (!near && !(ctx && medialVertexInOverlap(ctx, c1, fp, fq, rp, rq))) continue;
45500
45558
  // Hull edge: the Voronoi edge here is an unbounded ray (the bisector of
45501
45559
  // two sites on the convex hull). Emit it as an outward ray from the
45502
45560
  // circumcenter so the medial line reaches and crosses the buffer
@@ -45517,6 +45575,9 @@ ${svg}
45517
45575
  var t2 = triangleOfEdge(opp);
45518
45576
  var c2 = verts[t2];
45519
45577
  if (!c2) continue;
45578
+ if (!near && !(ctx &&
45579
+ (medialVertexInOverlap(ctx, c1, fp, fq, rp, rq) ||
45580
+ medialVertexInOverlap(ctx, c2, fp, fq, rp, rq)))) continue;
45520
45581
  var sx = c1[0] - c2[0], sy = c1[1] - c2[1];
45521
45582
  var segLen = Math.sqrt(sx * sx + sy * sy);
45522
45583
  // a real medial edge inside the overlap is short (on the order of the site
@@ -63697,7 +63758,7 @@ ${svg}
63697
63758
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
63698
63759
  }
63699
63760
 
63700
- var version = "0.7.28";
63761
+ var version = "0.7.29";
63701
63762
 
63702
63763
  // Parse command line args into commands and run them
63703
63764
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.