mapshaper 0.6.112 → 0.6.113

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/www/mapshaper.js CHANGED
@@ -2107,13 +2107,13 @@
2107
2107
 
2108
2108
  // TODO: remove this constant, use actual data from dataset CRS,
2109
2109
  // also consider using ellipsoidal formulas where greater accuracy might be important.
2110
- var R$1 = WGS84.SEMIMAJOR_AXIS;
2110
+ var R$2 = WGS84.SEMIMAJOR_AXIS;
2111
2111
  var D2R = Math.PI / 180;
2112
2112
  var R2D = 180 / Math.PI;
2113
2113
 
2114
2114
  // Equirectangular projection
2115
2115
  function degreesToMeters(deg) {
2116
- return deg * D2R * R$1;
2116
+ return deg * D2R * R$2;
2117
2117
  }
2118
2118
 
2119
2119
  function distance3D(ax, ay, az, bx, by, bz) {
@@ -2210,7 +2210,8 @@
2210
2210
  lat1 *= D2R;
2211
2211
  lat2 *= D2R;
2212
2212
  var y = Math.sin(lng2-lng1) * Math.cos(lat2),
2213
- x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(lng2-lng1);
2213
+ x = Math.cos(lat1) * Math.sin(lat2) -
2214
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(lng2-lng1);
2214
2215
  return Math.atan2(y, x);
2215
2216
  }
2216
2217
 
@@ -2270,9 +2271,9 @@
2270
2271
  lng *= D2R;
2271
2272
  lat *= D2R;
2272
2273
  cosLat = Math.cos(lat);
2273
- p[0] = Math.cos(lng) * cosLat * R$1;
2274
- p[1] = Math.sin(lng) * cosLat * R$1;
2275
- p[2] = Math.sin(lat) * R$1;
2274
+ p[0] = Math.cos(lng) * cosLat * R$2;
2275
+ p[1] = Math.sin(lng) * cosLat * R$2;
2276
+ p[2] = Math.sin(lat) * R$2;
2276
2277
  }
2277
2278
 
2278
2279
  // Haversine formula (well conditioned at small distances)
@@ -2291,7 +2292,7 @@
2291
2292
  function greatCircleDistance(lng1, lat1, lng2, lat2) {
2292
2293
  var D2R = Math.PI / 180,
2293
2294
  dist = sphericalDistance(lng1 * D2R, lat1 * D2R, lng2 * D2R, lat2 * D2R);
2294
- return dist * R$1;
2295
+ return dist * R$2;
2295
2296
  }
2296
2297
 
2297
2298
 
@@ -2389,7 +2390,7 @@
2389
2390
  var Geom = /*#__PURE__*/Object.freeze({
2390
2391
  __proto__: null,
2391
2392
  D2R: D2R,
2392
- R: R$1,
2393
+ R: R$2,
2393
2394
  R2D: R2D,
2394
2395
  bearing: bearing,
2395
2396
  bearing2D: bearing2D,
@@ -16948,47 +16949,6 @@
16948
16949
  splitPathByIds: splitPathByIds
16949
16950
  });
16950
16951
 
16951
- // TODO: also delete positive-space rings nested inside holes
16952
- function deleteHoles(lyr, arcs) {
16953
- editShapes(lyr.shapes, function(path) {
16954
- if (geom.getPathArea(path, arcs) <= 0) {
16955
- return null; // null deletes the path
16956
- }
16957
- });
16958
- }
16959
-
16960
- // Returns a function that separates rings in a polygon into space-enclosing rings
16961
- // and holes. Also fixes self-intersections.
16962
- //
16963
- function getHoleDivider(nodes, spherical) {
16964
- var split = getSelfIntersectionSplitter(nodes);
16965
- // var split = internal.getSelfIntersectionSplitter_v1(nodes); console.log('split')
16966
-
16967
- return function(rings, cw, ccw) {
16968
- var pathArea = spherical ? geom.getSphericalPathArea : geom.getPlanarPathArea;
16969
- forEachShapePart(rings, function(ringIds) {
16970
- var splitRings = split(ringIds);
16971
- if (splitRings.length === 0) {
16972
- debug("[getRingDivider()] Defective path:", ringIds);
16973
- }
16974
- splitRings.forEach(function(ringIds, i) {
16975
- var ringArea = pathArea(ringIds, nodes.arcs);
16976
- if (ringArea > 0) {
16977
- cw.push(ringIds);
16978
- } else if (ringArea < 0) {
16979
- ccw.push(ringIds);
16980
- }
16981
- });
16982
- });
16983
- };
16984
- }
16985
-
16986
- var PolygonHoles = /*#__PURE__*/Object.freeze({
16987
- __proto__: null,
16988
- deleteHoles: deleteHoles,
16989
- getHoleDivider: getHoleDivider
16990
- });
16991
-
16992
16952
  // Remap any references to duplicate arcs in paths to use the same arcs
16993
16953
  // Remove any unused arcs from the dataset's ArcCollection.
16994
16954
  // Return a NodeCollection
@@ -17593,6 +17553,47 @@
17593
17553
  return idx;
17594
17554
  }
17595
17555
 
17556
+ // TODO: also delete positive-space rings nested inside holes
17557
+ function deleteHoles(lyr, arcs) {
17558
+ editShapes(lyr.shapes, function(path) {
17559
+ if (geom.getPathArea(path, arcs) <= 0) {
17560
+ return null; // null deletes the path
17561
+ }
17562
+ });
17563
+ }
17564
+
17565
+ // Returns a function that separates rings in a polygon into space-enclosing rings
17566
+ // and holes. Also fixes self-intersections.
17567
+ //
17568
+ function getHoleDivider(nodes, spherical) {
17569
+ var split = getSelfIntersectionSplitter(nodes);
17570
+ // var split = internal.getSelfIntersectionSplitter_v1(nodes); console.log('split')
17571
+
17572
+ return function(rings, cw, ccw) {
17573
+ var pathArea = spherical ? geom.getSphericalPathArea : geom.getPlanarPathArea;
17574
+ forEachShapePart(rings, function(ringIds) {
17575
+ var splitRings = split(ringIds);
17576
+ if (splitRings.length === 0) {
17577
+ debug("[getRingDivider()] Defective path:", ringIds);
17578
+ }
17579
+ splitRings.forEach(function(ringIds, i) {
17580
+ var ringArea = pathArea(ringIds, nodes.arcs);
17581
+ if (ringArea > 0) {
17582
+ cw.push(ringIds);
17583
+ } else if (ringArea < 0) {
17584
+ ccw.push(ringIds);
17585
+ }
17586
+ });
17587
+ });
17588
+ };
17589
+ }
17590
+
17591
+ var PolygonHoles = /*#__PURE__*/Object.freeze({
17592
+ __proto__: null,
17593
+ deleteHoles: deleteHoles,
17594
+ getHoleDivider: getHoleDivider
17595
+ });
17596
+
17596
17597
  // Associate mosaic tiles with shapes (i.e. identify the groups of tiles that
17597
17598
  // belong to each shape)
17598
17599
  //
@@ -17701,7 +17702,6 @@
17701
17702
  function MosaicIndex(lyr, nodes, optsArg) {
17702
17703
  var opts = optsArg || {};
17703
17704
  var shapes = lyr.shapes;
17704
- getHoleDivider(nodes);
17705
17705
  var mosaic = buildPolygonMosaic(nodes).mosaic;
17706
17706
  // map arc ids to tile ids
17707
17707
  var arcTileIndex = new ShapeArcIndex(mosaic, nodes.arcs);
@@ -31076,7 +31076,7 @@ ${svg}
31076
31076
  rewindPolygonParts: rewindPolygonParts
31077
31077
  });
31078
31078
 
31079
- function dissolveBufferDataset(dataset, optsArg) {
31079
+ function dissolveBufferDataset2(dataset, optsArg) {
31080
31080
  var opts = optsArg || {};
31081
31081
  var lyr = dataset.layers[0];
31082
31082
  var tmp;
@@ -31084,6 +31084,55 @@ ${svg}
31084
31084
  return; // raw offset path
31085
31085
  }
31086
31086
  var nodes = addIntersectionCuts(dataset, {rebuild_topology: true});
31087
+ var mosaicIndex = new MosaicIndex(lyr, nodes, {flat: false, no_holes: false});
31088
+
31089
+ // rewindPolygonParts(lyr, nodes);
31090
+ lyr.shapes = lyr.shapes.map(function(shp, i) {
31091
+ var tiles = mosaicIndex.getTilesByShapeIds([i]);
31092
+ if (!tiles.length) return null;
31093
+ return tiles.reduce(function(memo, tile) {
31094
+ return memo.concat(tile);
31095
+ }, []);
31096
+ });
31097
+
31098
+ if (opts.debug_winding) {
31099
+ return;
31100
+ }
31101
+
31102
+ if (opts.debug_mosaic) {
31103
+ tmp = composeMosaicLayer(lyr, mosaicIndex.mosaic);
31104
+ lyr.shapes = tmp.shapes;
31105
+ lyr.data = tmp.data;
31106
+ return;
31107
+ }
31108
+ var pathfind = getRingIntersector(mosaicIndex.nodes);
31109
+ var shapes2 = lyr.shapes.map(function(shp, shapeId) {
31110
+ var tiles = mosaicIndex.getTilesByShapeIds([shapeId]);
31111
+ var rings = [];
31112
+ for (var i=0; i<tiles.length; i++) {
31113
+ rings.push(tiles[i][0]);
31114
+ }
31115
+ return pathfind(rings, 'dissolve');
31116
+ });
31117
+ lyr.shapes = shapes2;
31118
+ if (!opts.no_dissolve) {
31119
+ dissolveArcs(dataset);
31120
+ }
31121
+ }
31122
+
31123
+ // TODO: try geodesic option
31124
+ function getIntersectionFunction(crs) {
31125
+ return bufferIntersection$2;
31126
+ }
31127
+
31128
+ function dissolveBufferDataset(dataset, optsArg) {
31129
+ var opts = {};
31130
+ var lyr = dataset.layers[0];
31131
+ var tmp;
31132
+ if (opts.debug_offset) {
31133
+ return; // raw offset path
31134
+ }
31135
+ var nodes = addIntersectionCuts(dataset, {rebuild_topology: true});
31087
31136
  if (opts.debug_winding) {
31088
31137
  rewindPolygonParts(lyr, nodes);
31089
31138
  // debugRingsAndHoles(lyr, nodes);
@@ -31138,7 +31187,23 @@ ${svg}
31138
31187
  };
31139
31188
  }
31140
31189
 
31141
- function BufferBuilder(opts) {
31190
+
31191
+ function bufferIntersection$2(a, b, c, d) {
31192
+ return bufferIntersection2(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]);
31193
+ }
31194
+
31195
+ // Exclude segments with non-intersecting bounding boxes before
31196
+ // calling intersection function
31197
+ // Possibly slightly faster than direct call... not worth it?
31198
+ function bufferIntersection2(ax, ay, bx, by, cx, cy, dx, dy) {
31199
+ if (ax < cx && ax < dx && bx < cx && bx < dx ||
31200
+ ax > cx && ax > dx && bx > cx && bx > dx ||
31201
+ ay < cy && ay < dy && by < cy && by < dy ||
31202
+ ay > cy && ay > dy && by > cy && by > dy) return null;
31203
+ return segmentIntersection(ax, ay, bx, by, cx, cy, dx, dy);
31204
+ }
31205
+
31206
+ function BufferBuilder(bufferIntersection, opts) {
31142
31207
  var self = {};
31143
31208
  var buffer, path, insideFlags;
31144
31209
  var points = [];
@@ -31222,26 +31287,18 @@ ${svg}
31222
31287
  // TODO: consider using a geodetic intersection function for lat-long datasets
31223
31288
  // TODO: consider case of an endpoint hit
31224
31289
  // TODO: consider case of collinear hit
31225
- hit = bufferIntersection$2(a[0], a[1], b[0], b[1], prevP[0], prevP[1], p[0], p[1]);
31290
+ hit = bufferIntersection(a, b, prevP, p);
31226
31291
  if (!hit) {
31227
31292
  // continue scanning backward for an intersection
31228
31293
  continue;
31229
31294
  }
31230
- // console.log("hit")
31295
+
31231
31296
  flagA = insideFlags[idx];
31232
31297
  flagB = insideFlags[idx + 1];
31233
31298
  if (flagA && flagB) {
31234
31299
  // newest segment collides with an interior segment - do not rewind
31235
31300
  continue;
31236
31301
  }
31237
- if (prevFlag === false) {
31238
- // if segment intersects an outside segment from outside the buffer,
31239
- // we are likely closing a loop,
31240
- // so we stop backtracking and reset the backtrack limit to avoid
31241
- // removing the loop
31242
- backtrackStopIdx = i + 1;
31243
- break;
31244
- }
31245
31302
 
31246
31303
  // newest segment collides with an exterior segment
31247
31304
  // * assume we're going from inside to outside
@@ -31282,24 +31339,160 @@ ${svg}
31282
31339
  return a[0] === b[0] && a[1] === b[1];
31283
31340
  }
31284
31341
 
31285
- // Exclude segments with non-intersecting bounding boxes before
31286
- // calling intersection function
31287
- // Possibly slightly faster than direct call... not worth it?
31288
- function bufferIntersection$2(ax, ay, bx, by, cx, cy, dx, dy) {
31289
- if (ax < cx && ax < dx && bx < cx && bx < dx ||
31290
- ax > cx && ax > dx && bx > cx && bx > dx ||
31291
- ay < cy && ay < dy && by < cy && by < dy ||
31292
- ay > cy && ay > dy && by > cy && by > dy) return null;
31293
- return segmentIntersection(ax, ay, bx, by, cx, cy, dx, dy);
31342
+ // function countExtendedHits(p0, p1, buffer) {
31343
+ // var bbox = getBbox();
31344
+ // var width = Math.max(bbox[2] - bbox[0], bbox[3] - bbox[1]);
31345
+ // var p2 = [extend(p1[0], p1[0] - p0[0], size), extend(p1[1], p1[1] - p0[1], size)];
31346
+ // var hits = 0;
31347
+ // for (var i=backtrackStopIdx, n = buffer.length-1; i<n; i++) {
31348
+ // if (bufferIntersection(buffer[i], buffer[i+1], p1, p2)) {
31349
+ // hits++;
31350
+ // }
31351
+ // }
31352
+ // return hits;
31353
+ // };
31354
+
31355
+ var R$1 = WGS84.SEMIMAJOR_AXIS;
31356
+
31357
+ // GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
31358
+ // https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
31359
+ // https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
31360
+ function getGeodesic(P) {
31361
+ if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
31362
+ var f = P.es / (1 + Math.sqrt(P.one_es));
31363
+ // var GeographicLib = require('mproj').internal.GeographicLib;
31364
+ var GeographicLib = require$1('geographiclib-geodesic');
31365
+ // return new GeographicLib.Geodesic.Geodesic(P.a, 0)
31366
+ return new GeographicLib.Geodesic.Geodesic(P.a, f);
31367
+ }
31368
+
31369
+ function interpolatePoint2D(ax, ay, bx, by, k) {
31370
+ var j = 1 - k;
31371
+ return [ax * j + bx * k, ay * j + by * k];
31372
+ }
31373
+
31374
+ function getInterpolationFunction(P) {
31375
+ var spherical = P && isLatLngCRS(P);
31376
+ if (!spherical) return interpolatePoint2D;
31377
+ var geod = getGeodesic(P);
31378
+ return function(lng, lat, lng2, lat2, k) {
31379
+ var r = geod.Inverse(lat, lng, lat2, lng2);
31380
+ var dist = r.s12 * k;
31381
+ var r2 = geod.Direct(lat, lng, r.azi1, dist);
31382
+ return [r2.lon2, r2.lat2];
31383
+ };
31384
+ }
31385
+
31386
+ function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
31387
+ var rad = bearing / 180 * Math.PI;
31388
+ var dx = Math.sin(rad) * meterDist;
31389
+ var dy = Math.cos(rad) * meterDist;
31390
+ return [x + dx, y + dy];
31294
31391
  }
31295
31392
 
31393
+ // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
31394
+ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
31395
+ var D2R = Math.PI / 180;
31396
+ var cos = Math.cos(lat * D2R);
31397
+ var cos2 = 2 * cos * cos - 1;
31398
+ var cos3 = 2 * cos * cos2 - cos;
31399
+ var cos4 = 2 * cos * cos3 - cos2;
31400
+ var cos5 = 2 * cos * cos4 - cos3;
31401
+ var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
31402
+ var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
31403
+ var bearingRad = bearing * D2R;
31404
+ var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
31405
+ var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
31406
+ return [lng2, lat2];
31407
+ }
31408
+
31409
+ function wrap(deg) {
31410
+ while (deg < -180) deg += 360;
31411
+ while (deg > 180) deg -= 360;
31412
+ return deg;
31413
+ }
31414
+
31415
+ function fastGeodeticBearingFunction(lng1, lat1, lng2, lat2) {
31416
+ var D2R = Math.PI / 180;
31417
+ var f = 1 / 298.257223563;
31418
+ var e2 = f * (2 - f);
31419
+ var m = R$1 * D2R;
31420
+ var coslat = Math.cos(lat1 * D2R);
31421
+ var w2 = 1 / (1 - e2 * (1 - coslat * coslat));
31422
+ var w = Math.sqrt(w2);
31423
+ var kx = m * w * coslat;
31424
+ var ky = m * w * w2 * (1 - e2);
31425
+ var dx = wrap(lng2 - lng1) * kx;
31426
+ var dy = (lat2 - lat1) * ky;
31427
+ return Math.atan2(dx, dy) / D2R;
31428
+ }
31429
+
31430
+ function getGeodeticSegmentFunction(P) {
31431
+ if (!isLatLngCRS(P)) {
31432
+ return getPlanarSegmentEndpoint;
31433
+ }
31434
+ var g = getGeodesic(P);
31435
+ return function(lng, lat, bearing, meterDist) {
31436
+ var o = g.Direct(lat, lng, bearing, meterDist);
31437
+ var p = [o.lon2, o.lat2];
31438
+ return p;
31439
+ };
31440
+ }
31441
+
31442
+ function getFastGeodeticSegmentFunction(P) {
31443
+ // CAREFUL: this function has higher error at very large distances and at the poles
31444
+ // also, it wouldn't work for other planets than Earth
31445
+ return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
31446
+ }
31447
+
31448
+ // return function to calculate bearing of a segment in degrees
31449
+ function getBearingFunction(dataset) {
31450
+ var P = getDatasetCRS(dataset);
31451
+ // var g = getGeodesic(P);
31452
+ // if (isLatLngCRS) {
31453
+ // return function(lng1, lat1, lng2, lat2) {
31454
+ // var tmp = g.Inverse(lat1, lng1, lat2, lng2);
31455
+ // return tmp.azi1;
31456
+ // // return bearingDegrees(lng1, lat1, lng2, lat2);
31457
+ // };
31458
+ // }
31459
+ // return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
31460
+ return isLatLngCRS(P) ? fastGeodeticBearingFunction : bearingDegrees2D;
31461
+ }
31462
+
31463
+ // get bearing in degrees from point ab to point cd
31464
+ function bearingDegrees(a, b, c, d) {
31465
+ return geom.bearing(a, b, c, d) * 180 / Math.PI;
31466
+ }
31467
+
31468
+ function bearingDegrees2D(a, b, c, d) {
31469
+ return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
31470
+ }
31471
+
31472
+ var Geodesic = /*#__PURE__*/Object.freeze({
31473
+ __proto__: null,
31474
+ bearingDegrees: bearingDegrees,
31475
+ bearingDegrees2D: bearingDegrees2D,
31476
+ getBearingFunction: getBearingFunction,
31477
+ getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
31478
+ getGeodeticSegmentFunction: getGeodeticSegmentFunction,
31479
+ getInterpolationFunction: getInterpolationFunction,
31480
+ getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
31481
+ interpolatePoint2D: interpolatePoint2D
31482
+ });
31483
+
31296
31484
  // Returns a function for generating GeoJSON MultiPolygon geometries
31297
- function getPolylineBufferMaker$2(arcs, geod, getBearing, opts) {
31485
+ function getPolylineBufferMaker$2(dataset, opts) {
31298
31486
  // var sliceLen = opts.slice_length || Infinity;
31487
+ var crs = getDatasetCRS(dataset);
31488
+ var geod = getFastGeodeticSegmentFunction(crs);
31489
+ var bufferIntersection = getIntersectionFunction();
31490
+ var getBearing = getBearingFunction(dataset);
31299
31491
  var segsPerQuadrant = opts.arc_quality >= 2 ? opts.arc_quality : 12;
31492
+ var segAngle = 360 / segsPerQuadrant / 4;
31300
31493
  var capStyle = opts.cap_style || 'round'; // expect 'round' or 'flat'
31301
- var pathIter = new ShapeIter(arcs);
31302
- var builder = new BufferBuilder(opts);
31494
+ var pathIter = new ShapeIter(dataset.arcs);
31495
+ var builder = new BufferBuilder(bufferIntersection, opts);
31303
31496
 
31304
31497
  return function makeBufferGeoJSON(shape, distance) {
31305
31498
  var rings = [];
@@ -31341,14 +31534,17 @@ ${svg}
31341
31534
  var rings = [];
31342
31535
  var x0, y0, x1, y1, x2, y2; // path traversal coords
31343
31536
  var p1, p2; // extruded points
31344
- var bearing1, bearing2, prevBearing, joinAngle;
31537
+ var p1Prev, p2Prev;
31538
+ var bearing1, bearing2, bearing2Prev, joinAngle, hit;
31345
31539
  var firstBearing;
31540
+ var joinPoints;
31346
31541
  var i = 0;
31347
31542
  pathIter.init(path);
31348
31543
 
31349
31544
  if (pathIter.hasNext()) {
31350
31545
  x0 = x2 = pathIter.x;
31351
31546
  y0 = y2 = pathIter.y;
31547
+ builder.addPathVertex([x0, y0]); // start building the path side
31352
31548
  i++;
31353
31549
  }
31354
31550
 
@@ -31358,43 +31554,67 @@ ${svg}
31358
31554
  debug("skipping a duplicate point");
31359
31555
  continue;
31360
31556
  }
31557
+
31361
31558
  x1 = x2;
31362
31559
  y1 = y2;
31363
31560
  x2 = pathIter.x;
31364
31561
  y2 = pathIter.y;
31562
+ builder.addPathVertex([x2, y2]); // extend path
31365
31563
 
31366
- // calculate bearing at both segment points
31564
+ // bearing (direction) of the segment is slightly different at the first
31565
+ // and second endpoint, using geodesic math
31367
31566
  // TODO: no need to calculate twice with planar coordinates
31368
- prevBearing = bearing2;
31369
31567
  bearing1 = getBearing(x1, y1, x2, y2);
31370
31568
  bearing2 = getBearing(x2, y2, x1, y1) - 180;
31569
+ // extrude current segment to the left
31371
31570
  p1 = geod(x1, y1, bearing1 - 90, dist);
31372
31571
  p2 = geod(x2, y2, bearing2 - 90, dist);
31373
31572
 
31374
31573
  if (i == 1) {
31375
31574
  firstBearing = bearing1;
31376
- builder.addPathVertex([x1, y1]);
31377
31575
  } else {
31378
- joinAngle = getJoinAngle(prevBearing, bearing1);
31576
+ joinAngle = getJoinAngle(bearing2Prev, bearing1);
31379
31577
  }
31380
31578
 
31381
- builder.addPathVertex([x2, y2]);
31382
-
31383
- if (i > 1 && joinAngle > 0) {
31384
- builder.addBufferVertices(makeRoundJoin(x1, y1, prevBearing - 90, joinAngle, dist));
31579
+ // various connections between current extruded segment and prev segment
31580
+ if (i == 1) {
31581
+ // first segment - no join
31582
+ builder.addBufferVertex(p1, false);
31583
+ } else if (joinAngle > segAngle * 1.5) {
31584
+ // round join
31585
+ // builder.addBufferVertex(p2Prev, false) // testing
31586
+ joinPoints = makeOutsideRoundJoin(x1, y1, bearing2Prev - 90, joinAngle, dist);
31587
+ builder.addBufferVertices(joinPoints);
31588
+ // builder.addBufferVertex(p1, false) // testing
31589
+ p1 = joinPoints.pop();
31590
+ } else if (joinAngle > 0 && (hit = elbowJoin(p1Prev, p2Prev, p1, p2)) ||
31591
+ joinAngle < 0 && (hit = bufferIntersection(p1Prev, p2Prev, p1, p2))) {
31592
+ // elbow join (concave or convex)
31593
+ builder.addBufferVertex(hit, false);
31594
+ p1 = hit;
31595
+ } else if (joinAngle == 0) {
31596
+ // collinear segments (can we really skip p2Prev on a sphere?)
31597
+ builder.addBufferVertex(p1, false);
31598
+ } else {
31599
+ // if (joinAngle > 0) {
31600
+ // probably a leftward bend and extruded segments do not intersect
31601
+ // // console.log("UNEXPECTED SEGMENT JOIN")
31602
+ // }
31603
+ builder.addBufferVertex(p2Prev);
31604
+ builder.addBufferVertex(p1, joinAngle < 0);
31385
31605
  }
31386
31606
 
31387
- builder.addBufferVertex(p1, joinAngle < 0);
31388
- builder.addBufferVertex(p2);
31389
-
31390
- // TODO: restore slicing?
31391
- // if (center.length - 1 >= sliceLen) {
31392
- // builder.done(rings);
31393
- // }
31394
-
31607
+ bearing2Prev = bearing2;
31608
+ p1Prev = p1;
31609
+ p2Prev = p2;
31395
31610
  i++;
31396
31611
  }
31397
31612
 
31613
+ // TODO: add this to cap and join code below
31614
+ if (p2Prev) {
31615
+ builder.addBufferVertex(p2Prev);
31616
+ }
31617
+
31398
31618
  if (x2 == x0 && y2 == y0) { // closed path
31399
31619
  // add join to finish closed path
31400
31620
  // TODO - figure out which bearing to use
@@ -31428,6 +31648,68 @@ ${svg}
31428
31648
  return points;
31429
31649
  }
31430
31650
 
31651
+ // The vertices of this join are outside of the arc that it approximates and
31652
+ // the segments of the join touch the arc at their midpoints.
31653
+ // The first and last of the returned vertices extend the segments on either
31654
+ // side of the join.
31655
+ function makeOutsideRoundJoin(cx, cy, startBearing, arcAngle, dist) {
31656
+ // point count of 1 would be an elbow joint
31657
+ // (elbow joins should be created elsewhere)
31658
+ var pointCount = Math.max(1, Math.round(arcAngle / segAngle));
31659
+ var stepAngle = arcAngle / pointCount;
31660
+ var points = [];
31661
+ var i = 0;
31662
+ var a, b, c, d, joinP, tanP, bearing;
31663
+ while (i <= pointCount) {
31664
+ bearing = startBearing + stepAngle * i;
31665
+ tanP = geod(cx, cy, bearing, dist);
31666
+ c = geod(tanP[0], tanP[1], bearing - 90, dist * 2);
31667
+ d = geod(tanP[0], tanP[1], bearing + 90, dist * 2);
31668
+ if (i > 0) {
31669
+ joinP = bufferIntersection(a, b, c, d);
31670
+ if (!joinP) {
31671
+ throw Error(`no intersection on ${i} of ${pointCount}`);
31672
+ } else {
31673
+ points.push(joinP);
31674
+ }
31675
+ }
31676
+ a = c;
31677
+ b = d;
31678
+ i++;
31679
+ }
31680
+ return points;
31681
+ }
31682
+
31683
+ function elbowJoin(a, b, c, d) {
31684
+ var k = 2 ** 13;
31685
+ var b2 = [extend(b[0], b[0] - a[0], k), extend(b[1], b[1] - a[1], k)];
31686
+ var c2 = [extend(c[0], c[0] - d[0], k), extend(c[1], c[1] - d[1], k)];
31687
+ return bufferIntersection(a, b2, c2, d);
31688
+ }
31689
+
31690
+
31691
+ // function getBbox() {
31692
+ // var lastIdx = buffer.length - 1;
31693
+ // var x, y;
31694
+ // if (!_bbox) {
31695
+ // _bbox = [-Infinity, -Infinity, Infinity, Infinity];
31696
+ // }
31697
+ // while (_idx < lastIdx) {
31698
+ // _idx++;
31699
+ // x = buffer[_idx][0];
31700
+ // y = buffer[_idx][1];
31701
+ // _bbox[0] = Math.min(x, _bbox[0]);
31702
+ // _bbox[1] = Math.min(y, _bbox[1]);
31703
+ // _bbox[2] = Math.max(x, _bbox[2]);
31704
+ // _bbox[3] = Math.max(y, _bbox[3]);
31705
+ // }
31706
+ // return _bbox;
31707
+ // }
31708
+
31709
+ function extend(n, d, k) {
31710
+ return n + d * k;
31711
+ }
31712
+
31431
31713
  // get interior vertices of an interpolated CW arc
31432
31714
  function makeRoundJoin(cx, cy, startBearing, arcAngle, dist) {
31433
31715
  var points = [];
@@ -31457,12 +31739,14 @@ ${svg}
31457
31739
  }
31458
31740
 
31459
31741
  // Returns a function for generating GeoJSON MultiPolygon geometries
31460
- function getPolylineBufferMaker$1(arcs, geod, getBearing, opts) {
31742
+ function getPolylineBufferMaker$1(dataset, opts) {
31743
+ var geod = getFastGeodeticSegmentFunction(getDatasetCRS(dataset));
31744
+ var getBearing = getBearingFunction(dataset);
31461
31745
  var sliceLen = opts.slice_length || Infinity;
31462
31746
  var backtrackSteps = opts.backtrack >= 0 ? opts.backtrack : 100;
31463
31747
  var segsPerQuadrant = opts.arc_quality >= 2 ? opts.arc_quality : 12;
31464
31748
  var capStyle = opts.cap_style || 'round'; // expect 'round' or 'flat'
31465
- var pathIter = new ShapeIter(arcs);
31749
+ var pathIter = new ShapeIter(dataset.arcs);
31466
31750
  var left, center, rings;
31467
31751
 
31468
31752
  return function makeBufferGeoJSON(shape, distance) {
@@ -31713,8 +31997,10 @@ ${svg}
31713
31997
  }
31714
31998
 
31715
31999
  // Returns a function for generating GeoJSON geometries (MultiLineString or MultiPolygon)
31716
- function getPolylineBufferMaker(arcs, geod, getBearing, opts) {
31717
- var maker = getPathBufferMaker(arcs, geod, getBearing, opts);
32000
+ function getPolylineBufferMaker(dataset, opts) {
32001
+ var geod = getFastGeodeticSegmentFunction(getDatasetCRS(dataset));
32002
+ var getBearing = getBearingFunction(dataset);
32003
+ var maker = getPathBufferMaker(dataset.arcs, geod, getBearing, opts);
31718
32004
  var geomType = opts.geometry_type;
31719
32005
  // polyline output could be used for debugging
31720
32006
  var outputGeom = opts.output_geometry == 'polyline' ? 'polyline' : 'polygon';
@@ -31915,107 +32201,12 @@ ${svg}
31915
32201
  return geom.segmentIntersection(ax, ay, bx, by, cx, cy, dx, dy);
31916
32202
  }
31917
32203
 
31918
- // GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
31919
- // https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
31920
- // https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
31921
- function getGeodesic(P) {
31922
- if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
31923
- var f = P.es / (1 + Math.sqrt(P.one_es));
31924
- var GeographicLib = require$1('mproj').internal.GeographicLib;
31925
- return new GeographicLib.Geodesic.Geodesic(P.a, f);
31926
- }
31927
-
31928
- function interpolatePoint2D(ax, ay, bx, by, k) {
31929
- var j = 1 - k;
31930
- return [ax * j + bx * k, ay * j + by * k];
31931
- }
31932
-
31933
- function getInterpolationFunction(P) {
31934
- var spherical = P && isLatLngCRS(P);
31935
- if (!spherical) return interpolatePoint2D;
31936
- var geod = getGeodesic(P);
31937
- return function(lng, lat, lng2, lat2, k) {
31938
- var r = geod.Inverse(lat, lng, lat2, lng2);
31939
- var dist = r.s12 * k;
31940
- var r2 = geod.Direct(lat, lng, r.azi1, dist);
31941
- return [r2.lon2, r2.lat2];
31942
- };
31943
- }
31944
-
31945
- function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
31946
- var rad = bearing / 180 * Math.PI;
31947
- var dx = Math.sin(rad) * meterDist;
31948
- var dy = Math.cos(rad) * meterDist;
31949
- return [x + dx, y + dy];
31950
- }
31951
-
31952
- // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
31953
- function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
31954
- var D2R = Math.PI / 180;
31955
- var cos = Math.cos(lat * D2R);
31956
- var cos2 = 2 * cos * cos - 1;
31957
- var cos3 = 2 * cos * cos2 - cos;
31958
- var cos4 = 2 * cos * cos3 - cos2;
31959
- var cos5 = 2 * cos * cos4 - cos3;
31960
- var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
31961
- var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
31962
- var bearingRad = bearing * D2R;
31963
- var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
31964
- var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
31965
- return [lng2, lat2];
31966
- }
31967
-
31968
- function getGeodeticSegmentFunction(P) {
31969
- if (!isLatLngCRS(P)) {
31970
- return getPlanarSegmentEndpoint;
31971
- }
31972
- var g = getGeodesic(P);
31973
- return function(lng, lat, bearing, meterDist) {
31974
- var o = g.Direct(lat, lng, bearing, meterDist);
31975
- var p = [o.lon2, o.lat2];
31976
- return p;
31977
- };
31978
- }
31979
-
31980
- function getFastGeodeticSegmentFunction(P) {
31981
- // CAREFUL: this function has higher error at very large distances and at the poles
31982
- // also, it wouldn't work for other planets than Earth
31983
- return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
31984
- }
31985
-
31986
-
31987
- function bearingDegrees(a, b, c, d) {
31988
- return geom.bearing(a, b, c, d) * 180 / Math.PI;
31989
- }
31990
-
31991
- function bearingDegrees2D(a, b, c, d) {
31992
- return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
31993
- }
31994
-
31995
- // return function to calculate bearing of a segment in degrees
31996
- function getBearingFunction(dataset) {
31997
- var P = getDatasetCRS(dataset);
31998
- return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
31999
- }
32000
-
32001
- var Geodesic = /*#__PURE__*/Object.freeze({
32002
- __proto__: null,
32003
- bearingDegrees: bearingDegrees,
32004
- bearingDegrees2D: bearingDegrees2D,
32005
- getBearingFunction: getBearingFunction,
32006
- getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
32007
- getGeodeticSegmentFunction: getGeodeticSegmentFunction,
32008
- getInterpolationFunction: getInterpolationFunction,
32009
- getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
32010
- interpolatePoint2D: interpolatePoint2D
32011
- });
32012
-
32013
32204
  function makePolylineBuffer(lyr, dataset, opts) {
32014
32205
  time('buffer');
32015
32206
  var geojson = makeShapeBufferGeoJSON(lyr, dataset, opts);
32016
32207
  var dataset2 = importGeoJSON(geojson, {});
32017
32208
  if (!opts.debug_points) {
32018
- dissolveBufferDataset(dataset2, opts);
32209
+ dissolveBufferDataset2(dataset2, opts);
32019
32210
  }
32020
32211
  timeEnd('buffer');
32021
32212
  return dataset2;
@@ -32024,13 +32215,11 @@ ${svg}
32024
32215
  function makeShapeBufferGeoJSON(lyr, dataset, opts) {
32025
32216
  var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
32026
32217
  // var toleranceFn = getBufferToleranceFunction(dataset, opts);
32027
- var geod = getFastGeodeticSegmentFunction(getDatasetCRS(dataset));
32028
- var getBearing = getBearingFunction(dataset);
32029
32218
  var makerOpts = Object.assign({geometry_type: lyr.geometry_type}, opts);
32030
32219
  var makeShapeBuffer =
32031
- opts.v2 && getPolylineBufferMaker$1(dataset.arcs, geod, getBearing, makerOpts) ||
32032
- opts.v3 && getPolylineBufferMaker$2(dataset.arcs, geod, getBearing, makerOpts) ||
32033
- getPolylineBufferMaker(dataset.arcs, geod, getBearing, makerOpts);
32220
+ opts.v2 && getPolylineBufferMaker$1(dataset, makerOpts) ||
32221
+ opts.v3 && getPolylineBufferMaker$2(dataset, makerOpts) ||
32222
+ getPolylineBufferMaker(dataset, makerOpts);
32034
32223
  // var records = lyr.data ? lyr.data.getRecords() : null;
32035
32224
  var arr = lyr.shapes.reduce(function(memo, shape, i) {
32036
32225
  var distance = distanceFn(i);
@@ -46659,7 +46848,7 @@ ${svg}
46659
46848
  });
46660
46849
  }
46661
46850
 
46662
- var version = "0.6.112";
46851
+ var version = "0.6.113";
46663
46852
 
46664
46853
  // Parse command line args into commands and run them
46665
46854
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.