mapshaper 0.7.47 → 0.7.49

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
@@ -78,7 +78,7 @@
78
78
  return obj === Object(obj); // via underscore
79
79
  }
80
80
 
81
- function clamp$4(val, min, max) {
81
+ function clamp$3(val, min, max) {
82
82
  return val < min ? min : (val > max ? max : val);
83
83
  }
84
84
 
@@ -721,7 +721,7 @@
721
721
  function findValueByRank(arr, rank) {
722
722
  if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
723
723
 
724
- rank = clamp$4(rank | 0, 1, arr.length);
724
+ rank = clamp$3(rank | 0, 1, arr.length);
725
725
  var k = rank - 1, // conv. rank to array index
726
726
  n = arr.length,
727
727
  l = 0,
@@ -1095,7 +1095,7 @@
1095
1095
  // self-import and the resulting Rollup circular-dependency warning.
1096
1096
  var utils = {
1097
1097
  addThousandsSep, addslashes, arrayToIndex,
1098
- clamp: clamp$4, cleanNumericString, contains, copyElements, countValues, createBuffer,
1098
+ clamp: clamp$3, cleanNumericString, contains, copyElements, countValues, createBuffer,
1099
1099
  defaults, difference,
1100
1100
  endsWith, every, expandoBuffer, extend: extend$1, extendBuffer,
1101
1101
  find: find$1, findMedian, findQuantile, findRankByValue, findStringPrefix,
@@ -4740,6 +4740,89 @@
4740
4740
  pathIsRectangle: pathIsRectangle
4741
4741
  });
4742
4742
 
4743
+ // Low-level access to raster grid samples, shared by the code that reads or
4744
+ // rewrites pixels (-proj, -blur, clipping and preview rendering). These used
4745
+ // to be reimplemented per module, which let the nodata and coverage rules
4746
+ // drift apart.
4747
+ //
4748
+ // Samples are band-interleaved: pixel i occupies
4749
+ // samples[i * bands] through samples[i * bands + bands - 1].
4750
+
4751
+ // A pixel is nodata only when every one of its color-carrying bands matches
4752
+ // the nodata value. An alpha band is ignored, so a transparent pixel whose
4753
+ // color channels hold real values is still treated as data.
4754
+ function samplesAreNodataAtOffset(samples, offset, bands, nodata) {
4755
+ var n = Math.min(bands, 3);
4756
+ if (nodata === null || nodata === undefined) return false;
4757
+ for (var i = 0; i < n; i++) {
4758
+ if (samples[offset + i] != nodata) return false;
4759
+ }
4760
+ return true;
4761
+ }
4762
+
4763
+ function rasterPixelIsNodata(grid, pixelId) {
4764
+ return samplesAreNodataAtOffset(grid.samples, pixelId * grid.bands, grid.bands, grid.nodata);
4765
+ }
4766
+
4767
+ // The coverage mask is set by reprojection to record which output pixels
4768
+ // received source content, independently of the nodata fill value.
4769
+ function rasterPixelIsCovered(grid, pixelId) {
4770
+ return !grid.coverage || grid.coverage[pixelId] > 0;
4771
+ }
4772
+
4773
+ function rasterPixelIsValid(grid, pixelId) {
4774
+ return rasterPixelIsCovered(grid, pixelId) && !rasterPixelIsNodata(grid, pixelId);
4775
+ }
4776
+
4777
+ // False when every pixel is known to be valid, so callers can skip
4778
+ // per-pixel validity testing entirely.
4779
+ function rasterGridHasInvalidPixels(grid) {
4780
+ return !!grid.coverage || grid.nodata !== null && grid.nodata !== undefined;
4781
+ }
4782
+
4783
+ // One byte per pixel, so that code sampling a grid repeatedly can test
4784
+ // validity with a single lookup instead of re-reading every band. Returns
4785
+ // the coverage mask itself when there is no nodata value to fold in, and
4786
+ // null when every pixel is valid.
4787
+ function getRasterValidityMask(grid) {
4788
+ var n = grid.width * grid.height;
4789
+ var mask, i;
4790
+ if (grid.nodata === null || grid.nodata === undefined) return grid.coverage || null;
4791
+ mask = new Uint8Array(n);
4792
+ for (i = 0; i < n; i++) {
4793
+ mask[i] = rasterPixelIsValid(grid, i) ? 1 : 0;
4794
+ }
4795
+ return mask;
4796
+ }
4797
+
4798
+ function rasterSamplesAreFloat(samples) {
4799
+ return samples instanceof Float32Array || samples instanceof Float64Array;
4800
+ }
4801
+
4802
+ // Value range of a sample array, used to clamp computed values before
4803
+ // writing them back into an integer array.
4804
+ function getSampleArrayRange(samples) {
4805
+ if (samples instanceof Uint8Array || samples instanceof Uint8ClampedArray) return {min: 0, max: 255};
4806
+ if (samples instanceof Int8Array) return {min: -128, max: 127};
4807
+ if (samples instanceof Uint16Array) return {min: 0, max: 65535};
4808
+ if (samples instanceof Int16Array) return {min: -32768, max: 32767};
4809
+ if (samples instanceof Uint32Array) return {min: 0, max: 4294967295};
4810
+ if (samples instanceof Int32Array) return {min: -2147483648, max: 2147483647};
4811
+ return {min: -Infinity, max: Infinity};
4812
+ }
4813
+
4814
+ var RasterGrid = /*#__PURE__*/Object.freeze({
4815
+ __proto__: null,
4816
+ getRasterValidityMask: getRasterValidityMask,
4817
+ getSampleArrayRange: getSampleArrayRange,
4818
+ rasterGridHasInvalidPixels: rasterGridHasInvalidPixels,
4819
+ rasterPixelIsCovered: rasterPixelIsCovered,
4820
+ rasterPixelIsNodata: rasterPixelIsNodata,
4821
+ rasterPixelIsValid: rasterPixelIsValid,
4822
+ rasterSamplesAreFloat: rasterSamplesAreFloat,
4823
+ samplesAreNodataAtOffset: samplesAreNodataAtOffset
4824
+ });
4825
+
4743
4826
  function runningInBrowser() {
4744
4827
  return typeof window !== 'undefined' && typeof window.document !== 'undefined';
4745
4828
  }
@@ -4821,6 +4904,9 @@
4821
4904
  function copyRasterGrid(grid) {
4822
4905
  var copy = utils.extend({}, grid);
4823
4906
  if (grid.samples) copy.samples = copyTypedArray(grid.samples);
4907
+ // The coverage mask is mutated in place by the reprojection code, so an
4908
+ // undo snapshot that shared it would track later edits.
4909
+ if (grid.coverage) copy.coverage = copyTypedArray(grid.coverage);
4824
4910
  if (grid.sampleBands) copy.sampleBands = grid.sampleBands.concat();
4825
4911
  if (grid.bbox) copy.bbox = grid.bbox.concat();
4826
4912
  if (grid.transform) copy.transform = copyObjectOrArray(grid.transform);
@@ -4928,6 +5014,69 @@
4928
5014
  return stats;
4929
5015
  }
4930
5016
 
5017
+ // Returns the band values and display color of the pixel containing a map
5018
+ // coordinate, for readouts like the GUI's right-click menu, or null if the point
5019
+ // falls outside the raster. Rotated grids are not supported, because pixels can
5020
+ // only be located through the axis-aligned bbox.
5021
+ // x, y: a point in the coordinate system of the raster's bbox
5022
+ function getRasterPixelAtMapXY(raster, x, y) {
5023
+ var grid = getRasterGrid(raster);
5024
+ var col, row, pixelId, offset, values, valid, isColor, band;
5025
+ if (!grid || !grid.samples || !grid.bbox || rasterGridIsRotated(grid)) return null;
5026
+ col = Math.floor(mapXToRasterPixel(grid, x));
5027
+ row = Math.floor(mapYToRasterPixel(grid, y));
5028
+ if (col < 0 || col >= grid.width || row < 0 || row >= grid.height) return null;
5029
+ pixelId = row * grid.width + col;
5030
+ offset = pixelId * grid.bands;
5031
+ values = [];
5032
+ for (band = 0; band < grid.bands; band++) {
5033
+ values.push(grid.samples[offset + band]);
5034
+ }
5035
+ // The renderer reads interleaved bands in display order, so three or more
5036
+ // bands are shown as R, G, B and (if present) alpha.
5037
+ isColor = grid.bands >= 3;
5038
+ valid = rasterPixelIsValid(grid, pixelId);
5039
+ return {
5040
+ col: col,
5041
+ row: row,
5042
+ values: values,
5043
+ isColor: isColor,
5044
+ isFloat: rasterSamplesAreFloat(grid.samples),
5045
+ valid: valid,
5046
+ // Only color images get a color, so that inspecting a measurement raster
5047
+ // does not trigger a scaling-stats scan of the whole grid.
5048
+ // [r, g, b, a] in the 0-255 range, for formatColor() or a canvas.
5049
+ color: isColor && valid ? getRasterPixelDisplayColor(raster, grid, pixelId) : null
5050
+ };
5051
+ }
5052
+
5053
+ // Returns [r, g, b, a] in the 0-255 range, using the same recipe and scaling as
5054
+ // the preview renderer, so that a readout agrees with how the pixel is drawn.
5055
+ function getRasterPixelDisplayColor(raster, grid, pixelId) {
5056
+ var view = getRasterView(raster);
5057
+ var recipe = getRasterViewRecipe(grid, view && view.recipe);
5058
+ var stats = getRasterViewScalingStats(raster, recipe);
5059
+ var sourceRange = recipe.scaling == 'none' ? getPixelTypeRange(grid.pixelType) : null;
5060
+ var displayRange = getDisplayRange(recipe.scaleRange);
5061
+ var offset = pixelId * grid.bands;
5062
+ var color = [0, 1, 2].map(function(band) {
5063
+ return scaleSample(grid.samples[offset + band], stats && stats[band], sourceRange, displayRange);
5064
+ });
5065
+ color.push(grid.bands >= 4 ?
5066
+ scaleSample(grid.samples[offset + 3], stats && stats[3], sourceRange, [0, 255]) : 255);
5067
+ return color;
5068
+ }
5069
+
5070
+ // Float samples print with binary noise at full precision (a Float32 holding
5071
+ // 1234.5678 prints as 1234.5677490234375), so they are rounded to the number of
5072
+ // digits that the type can actually represent.
5073
+ function formatRasterSampleValue(val, isFloat) {
5074
+ if (val === null || val === undefined) return '';
5075
+ if (!isFinite(val)) return String(val); // NaN, Infinity
5076
+ if (!isFloat) return String(val);
5077
+ return String(+val.toPrecision(7));
5078
+ }
5079
+
4931
5080
  function renderRasterGridPreview(grid, recipe, width, height, statsArg, sourceBbox, resamplingMethod) {
4932
5081
  var samples = grid.samples;
4933
5082
  var bands = grid.bands;
@@ -5170,10 +5319,22 @@
5170
5319
  return (sy * grid.width + sx) * bands;
5171
5320
  }
5172
5321
 
5322
+ // A grid is north-up when its affine transform has no rotation/skew terms.
5323
+ // Cropping and reprojection both index pixels through the axis-aligned bbox,
5324
+ // so they can only handle north-up grids.
5325
+ function rasterGridIsRotated(grid) {
5326
+ var t = grid && grid.transform;
5327
+ return !!t && (t[1] !== 0 || t[3] !== 0);
5328
+ }
5329
+
5173
5330
  function clipRasterToBBox(lyr, bbox, opts) {
5174
5331
  var raster = lyr.raster;
5175
5332
  var grid = getRasterGrid(raster);
5176
- var clipBbox = intersectBboxes(grid.bbox, bbox);
5333
+ var clipBbox;
5334
+ if (rasterGridIsRotated(grid)) {
5335
+ stop$1('Clipping a rotated or skewed raster is not supported');
5336
+ }
5337
+ clipBbox = intersectBboxes(grid.bbox, bbox);
5177
5338
  if (!clipBbox) {
5178
5339
  warn('Raster clipping rectangle does not intersect the raster layer');
5179
5340
  return false;
@@ -5234,11 +5395,30 @@
5234
5395
  width: crop.width,
5235
5396
  height: crop.height,
5236
5397
  samples: samples,
5398
+ coverage: cropRasterCoverage(grid, crop),
5237
5399
  bbox: bbox,
5238
5400
  transform: updateTransformForBBox(grid.transform, bbox, crop.width, crop.height)
5239
5401
  });
5240
5402
  }
5241
5403
 
5404
+ // The coverage mask is one byte per pixel (not per sample), and must be
5405
+ // cropped alongside the samples -- consumers index it with the cropped
5406
+ // grid's dimensions.
5407
+ function cropRasterCoverage(grid, crop) {
5408
+ var coverage = grid.coverage;
5409
+ var cropped, src, dest, x, y;
5410
+ if (!coverage) return coverage;
5411
+ cropped = new Uint8Array(crop.width * crop.height);
5412
+ for (y = 0; y < crop.height; y++) {
5413
+ src = (crop.y + y) * grid.width + crop.x;
5414
+ dest = y * crop.width;
5415
+ for (x = 0; x < crop.width; x++) {
5416
+ cropped[dest + x] = coverage[src + x];
5417
+ }
5418
+ }
5419
+ return cropped;
5420
+ }
5421
+
5242
5422
  function getRasterLayerBounds(lyr) {
5243
5423
  var bbox = lyr.raster && getRasterBBox(lyr.raster);
5244
5424
  return bbox && bbox.length == 4 ? new Bounds(bbox) : null;
@@ -5499,11 +5679,7 @@
5499
5679
  }
5500
5680
 
5501
5681
  function allSamplesAreNoData(data, offset, bands, noData) {
5502
- var n = Math.min(bands, 3);
5503
- for (var i = 0; i < n; i++) {
5504
- if (data[offset + i] != noData) return false;
5505
- }
5506
- return true;
5682
+ return samplesAreNodataAtOffset(data, offset, bands, noData);
5507
5683
  }
5508
5684
 
5509
5685
  function copyObjectOrArray(obj) {
@@ -5520,12 +5696,14 @@
5520
5696
  copyTypedArray: copyTypedArray,
5521
5697
  createRasterPreview: createRasterPreview,
5522
5698
  cropRasterGrid: cropRasterGrid,
5699
+ formatRasterSampleValue: formatRasterSampleValue,
5523
5700
  getRasterBBox: getRasterBBox,
5524
5701
  getRasterBandCount: getRasterBandCount,
5525
5702
  getRasterCrop: getRasterCrop,
5526
5703
  getRasterGrid: getRasterGrid,
5527
5704
  getRasterHeight: getRasterHeight,
5528
5705
  getRasterLayerBounds: getRasterLayerBounds,
5706
+ getRasterPixelAtMapXY: getRasterPixelAtMapXY,
5529
5707
  getRasterPixelType: getRasterPixelType,
5530
5708
  getRasterPreview: getRasterPreview,
5531
5709
  getRasterScalingStats: getRasterScalingStats,
@@ -5536,6 +5714,7 @@
5536
5714
  getRasterViewScalingStats: getRasterViewScalingStats,
5537
5715
  getRasterWidth: getRasterWidth,
5538
5716
  intersectBboxes: intersectBboxes,
5717
+ rasterGridIsRotated: rasterGridIsRotated,
5539
5718
  renderRasterExportPreview: renderRasterExportPreview,
5540
5719
  renderRasterPreview: renderRasterPreview,
5541
5720
  renderRasterViewportPreview: renderRasterViewportPreview,
@@ -5712,6 +5891,14 @@
5712
5891
  stop$1(layerTypeMessage(lyr, "Expected a polygon or polyline layer", msg));
5713
5892
  }
5714
5893
 
5894
+ // Guard for vector commands that would otherwise replace a raster layer with
5895
+ // a geometry-less result, silently discarding its pixels.
5896
+ function requireNotRasterLayer(lyr, msg) {
5897
+ if (layerHasRaster(lyr)) {
5898
+ stop$1(msg || 'This command does not support raster layers');
5899
+ }
5900
+ }
5901
+
5715
5902
  // Used by info command and gui layer menu
5716
5903
  function getLayerSourceFile(lyr, dataset) {
5717
5904
  var inputs = dataset.info && dataset.info.input_files;
@@ -5913,6 +6100,7 @@
5913
6100
  layerTypeMessage: layerTypeMessage,
5914
6101
  requireDataField: requireDataField,
5915
6102
  requireDataFields: requireDataFields,
6103
+ requireNotRasterLayer: requireNotRasterLayer,
5916
6104
  requirePathLayer: requirePathLayer,
5917
6105
  requirePointLayer: requirePointLayer,
5918
6106
  requirePolygonLayer: requirePolygonLayer,
@@ -6257,6 +6445,124 @@
6257
6445
  probablyDecimalDegreeBounds: probablyDecimalDegreeBounds
6258
6446
  });
6259
6447
 
6448
+ /*
6449
+ * Small geometric helpers shared by the polyhedral projection modules
6450
+ * (mapshaper-polyhedral-projection, mapshaper-lee-tetrahedral,
6451
+ * mapshaper-narukawa2022). These projections all unfold a polyhedral net into
6452
+ * affine layout copies and clip the copies to a rectangular frame, so they need
6453
+ * the same 2x3 affine arithmetic and the same half-plane polygon clipping.
6454
+ */
6455
+
6456
+ var EPS$4 = 1e-12;
6457
+
6458
+ // A 2x3 affine matrix, stored as [a, b, tx, c, d, ty].
6459
+ function applyMatrix(m, p) {
6460
+ return [
6461
+ m[0] * p[0] + m[1] * p[1] + m[2],
6462
+ m[3] * p[0] + m[4] * p[1] + m[5]
6463
+ ];
6464
+ }
6465
+
6466
+ function invertMatrix(m) {
6467
+ var det = m[0] * m[4] - m[1] * m[3];
6468
+ return [
6469
+ m[4] / det,
6470
+ -m[1] / det,
6471
+ (m[1] * m[5] - m[4] * m[2]) / det,
6472
+ -m[3] / det,
6473
+ m[0] / det,
6474
+ (m[3] * m[2] - m[0] * m[5]) / det
6475
+ ];
6476
+ }
6477
+
6478
+ // Sutherland-Hodgman clip of a [x, y] ring against one axis-aligned half-plane.
6479
+ function clipPolygonAxis(polygon, axis, value, keepGreater) {
6480
+ var output = [];
6481
+ if (!polygon.length) return output;
6482
+ var a = polygon[polygon.length - 1];
6483
+ var aInside = isInside(a[axis], value, keepGreater);
6484
+ polygon.forEach(function(b) {
6485
+ var bInside = isInside(b[axis], value, keepGreater);
6486
+ if (aInside != bInside) {
6487
+ var t = (value - a[axis]) / (b[axis] - a[axis]);
6488
+ var p = [
6489
+ a[0] + (b[0] - a[0]) * t,
6490
+ a[1] + (b[1] - a[1]) * t
6491
+ ];
6492
+ p[axis] = value;
6493
+ output.push(p);
6494
+ }
6495
+ if (bInside) output.push(b.concat());
6496
+ a = b;
6497
+ aInside = bInside;
6498
+ });
6499
+ return output;
6500
+ }
6501
+
6502
+ // As clipPolygonAxis, but for raster mesh vertices, which carry the source
6503
+ // grid position (sx, sy) and geographic position (lon, lat) alongside the
6504
+ // projected position. The axis argument is a property name ('x' or 'y').
6505
+ function clipRasterPolygonAxis(polygon, axis, value, keepGreater) {
6506
+ var output = [];
6507
+ if (!polygon.length) return output;
6508
+ var a = polygon[polygon.length - 1];
6509
+ var aInside = isInside(a[axis], value, keepGreater);
6510
+ polygon.forEach(function(b) {
6511
+ var bInside = isInside(b[axis], value, keepGreater);
6512
+ if (aInside != bInside) {
6513
+ var t = (value - a[axis]) / (b[axis] - a[axis]);
6514
+ var vertex = {
6515
+ x: a.x + (b.x - a.x) * t,
6516
+ y: a.y + (b.y - a.y) * t,
6517
+ sx: a.sx + (b.sx - a.sx) * t,
6518
+ sy: a.sy + (b.sy - a.sy) * t,
6519
+ lon: interpolateLongitude(a.lon, b.lon, t),
6520
+ lat: a.lat + (b.lat - a.lat) * t
6521
+ };
6522
+ vertex[axis] = value;
6523
+ output.push(vertex);
6524
+ }
6525
+ if (bInside) output.push(copyRasterVertex(b, b.x, b.y));
6526
+ a = b;
6527
+ aInside = bInside;
6528
+ });
6529
+ return output;
6530
+ }
6531
+
6532
+ function copyRasterVertex(vertex, x, y) {
6533
+ return {
6534
+ x: x,
6535
+ y: y,
6536
+ sx: vertex.sx,
6537
+ sy: vertex.sy,
6538
+ lon: vertex.lon,
6539
+ lat: vertex.lat
6540
+ };
6541
+ }
6542
+
6543
+ // Interpolate along the shorter of the two arcs, so a cell straddling the
6544
+ // antimeridian does not interpolate the long way around the globe.
6545
+ function interpolateLongitude(a, b, t) {
6546
+ var delta = b - a;
6547
+ if (delta > 180) delta -= 360;
6548
+ if (delta < -180) delta += 360;
6549
+ return normalizeLongitude$1(a + delta * t);
6550
+ }
6551
+
6552
+ function normalizeLongitude$1(lon) {
6553
+ while (lon > 180) lon -= 360;
6554
+ while (lon < -180) lon += 360;
6555
+ return lon;
6556
+ }
6557
+
6558
+ function clamp$2(value, min, max) {
6559
+ return Math.max(min, Math.min(max, value));
6560
+ }
6561
+
6562
+ function isInside(coord, value, keepGreater) {
6563
+ return keepGreater ? coord >= value - EPS$4 : coord <= value + EPS$4;
6564
+ }
6565
+
6260
6566
  /*
6261
6567
  * Polyhedral unfolding code adapted from d3-geo-polygon.
6262
6568
  *
@@ -6264,6 +6570,7 @@
6264
6570
  * ISC License: https://github.com/d3/d3-geo-polygon/blob/main/LICENSE
6265
6571
  */
6266
6572
 
6573
+
6267
6574
  var D2R$7 = Math.PI / 180;
6268
6575
  var R2D$6 = 180 / Math.PI;
6269
6576
  var EPS$3 = 1e-12;
@@ -6345,6 +6652,7 @@
6345
6652
  outline: outline,
6346
6653
  forward: forward,
6347
6654
  forwardFace: forwardFace,
6655
+ invertFace: invertFace,
6348
6656
  findFace: findFace,
6349
6657
  findTransitionRegion: findTransitionRegion,
6350
6658
  getTopology: getTopology
@@ -6377,13 +6685,44 @@
6377
6685
 
6378
6686
  function projectFacePoint(face, lam, phi) {
6379
6687
  var p = face.project(lam, phi);
6380
- p = applyMatrix$1(face.transform, p);
6688
+ p = applyMatrix(face.transform, p);
6381
6689
  p = transformOutputPoint(p);
6382
6690
  p[0] -= centerX;
6383
6691
  p[1] -= centerY;
6384
6692
  return p;
6385
6693
  }
6386
6694
 
6695
+ function invertFace(x, y, faceId) {
6696
+ var face = faces[faceId];
6697
+ var p, rotated;
6698
+ if (!face || !face.project.invert) return null;
6699
+ p = inverseTransformOutputPoint([x + centerX, y + centerY]);
6700
+ p = applyMatrix(invertMatrix(face.transform), p);
6701
+ rotated = face.project.invert(p[0], p[1]);
6702
+ if (!rotated) return null;
6703
+ return rotateRadians(
6704
+ rotated[0],
6705
+ rotated[1],
6706
+ config.rotation[0] * D2R$7,
6707
+ config.rotation[1] * D2R$7,
6708
+ config.rotation[2] * D2R$7,
6709
+ true
6710
+ );
6711
+ }
6712
+
6713
+ function inverseTransformOutputPoint(p) {
6714
+ var x = p[0];
6715
+ var y = p[1];
6716
+ if (planarAngle) {
6717
+ var cos = Math.cos(planarAngle);
6718
+ var sin = Math.sin(planarAngle);
6719
+ var x2 = x * cos + y * sin;
6720
+ y = -x * sin + y * cos;
6721
+ x = x2;
6722
+ }
6723
+ return [x / scale, -y / scale];
6724
+ }
6725
+
6387
6726
  function findFace(lam, phi) {
6388
6727
  var rotated = rotateRadians(
6389
6728
  lam,
@@ -6451,9 +6790,9 @@
6451
6790
  config.rotation[2] * D2R$7,
6452
6791
  true
6453
6792
  );
6454
- return [normalizeLongitude$3(q[0] * R2D$6 + lon0), q[1] * R2D$6];
6793
+ return [normalizeLongitude$1(q[0] * R2D$6 + lon0), q[1] * R2D$6];
6455
6794
  });
6456
- var paths = splitPathAtAntimeridian$2(
6795
+ var paths = splitPathAtAntimeridian$3(
6457
6796
  interpolateGreatCircle(endpoints[0], endpoints[1], 0.05)
6458
6797
  );
6459
6798
  paths.forEach(function(path) {
@@ -6481,15 +6820,15 @@
6481
6820
  }),
6482
6821
  seams: seams,
6483
6822
  findRegion: function(lon, lat) {
6484
- var lam = normalizeLongitude$3(lon - lon0) * D2R$7;
6823
+ var lam = normalizeLongitude$1(lon - lon0) * D2R$7;
6485
6824
  return findFace(lam, lat * D2R$7);
6486
6825
  },
6487
6826
  findTransitionRegion: function(lon, lat) {
6488
- var lam = normalizeLongitude$3(lon - lon0) * D2R$7;
6827
+ var lam = normalizeLongitude$1(lon - lon0) * D2R$7;
6489
6828
  return findTransitionRegion(lam, lat * D2R$7);
6490
6829
  },
6491
6830
  projectRegion: function(lon, lat, regionId) {
6492
- var lam = normalizeLongitude$3(lon - lon0) * D2R$7;
6831
+ var lam = normalizeLongitude$1(lon - lon0) * D2R$7;
6493
6832
  return forwardFace(lam, lat * D2R$7, regionId);
6494
6833
  },
6495
6834
  regionsAreAttached: function(a, b) {
@@ -6597,8 +6936,8 @@
6597
6936
  var adjacent = findAdjacentFace(faces, face.id, a, b);
6598
6937
  if (adjacent >= 0 && attachedPairs.has(pairKey(face.id, adjacent))) continue;
6599
6938
  edges.push([
6600
- applyMatrix$1(face.transform, face.project(a[0] * D2R$7, a[1] * D2R$7)),
6601
- applyMatrix$1(face.transform, face.project(b[0] * D2R$7, b[1] * D2R$7))
6939
+ applyMatrix(face.transform, face.project(a[0] * D2R$7, a[1] * D2R$7)),
6940
+ applyMatrix(face.transform, face.project(b[0] * D2R$7, b[1] * D2R$7))
6602
6941
  ]);
6603
6942
  }
6604
6943
  });
@@ -6715,13 +7054,6 @@
6715
7054
  ];
6716
7055
  }
6717
7056
 
6718
- function applyMatrix$1(m, p) {
6719
- return [
6720
- m[0] * p[0] + m[1] * p[1] + m[2],
6721
- m[3] * p[0] + m[4] * p[1] + m[5]
6722
- ];
6723
- }
6724
-
6725
7057
  function rotateRadians(lam, phi, deltaLam, deltaPhi, deltaGamma, invert) {
6726
7058
  if (invert) {
6727
7059
  var inv = rotatePhiGamma$1(lam, phi, deltaPhi, deltaGamma, true);
@@ -6747,21 +7079,21 @@
6747
7079
  return [
6748
7080
  Math.atan2(y * cosDeltaGamma + z * sinDeltaGamma,
6749
7081
  x * cosDeltaPhi + k * sinDeltaPhi),
6750
- Math.asin(clamp$3(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
7082
+ Math.asin(clamp$2(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
6751
7083
  ];
6752
7084
  }
6753
7085
  k = z * cosDeltaPhi + x * sinDeltaPhi;
6754
7086
  return [
6755
7087
  Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma,
6756
7088
  x * cosDeltaPhi - z * sinDeltaPhi),
6757
- Math.asin(clamp$3(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
7089
+ Math.asin(clamp$2(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
6758
7090
  ];
6759
7091
  }
6760
7092
 
6761
7093
  function interpolateGreatCircle(a, b, interval) {
6762
7094
  var av = degreesToVector$2(a[0], a[1]);
6763
7095
  var bv = degreesToVector$2(b[0], b[1]);
6764
- var angle = Math.acos(clamp$3(dot$2(av, bv), -1, 1));
7096
+ var angle = Math.acos(clamp$2(dot$2(av, bv), -1, 1));
6765
7097
  var n = Math.max(1, Math.ceil(angle * R2D$6 / interval));
6766
7098
  var sinAngle = Math.sin(angle);
6767
7099
  var points = [];
@@ -6784,7 +7116,7 @@
6784
7116
  return points;
6785
7117
  }
6786
7118
 
6787
- function splitPathAtAntimeridian$2(path) {
7119
+ function splitPathAtAntimeridian$3(path) {
6788
7120
  var paths = [];
6789
7121
  var part = [path[0]];
6790
7122
  for (var i = 1; i < path.length; i++) {
@@ -6820,7 +7152,7 @@
6820
7152
  }
6821
7153
 
6822
7154
  function angularDistance(a, b) {
6823
- return Math.acos(clamp$3(dot$2(degreesToVector$2(a[0], a[1]),
7155
+ return Math.acos(clamp$2(dot$2(degreesToVector$2(a[0], a[1]),
6824
7156
  degreesToVector$2(b[0], b[1])), -1, 1));
6825
7157
  }
6826
7158
 
@@ -6836,7 +7168,7 @@
6836
7168
  function vectorToDegrees$2(p) {
6837
7169
  return [
6838
7170
  Math.atan2(p[1], p[0]) * R2D$6,
6839
- Math.asin(clamp$3(p[2], -1, 1)) * R2D$6
7171
+ Math.asin(clamp$2(p[2], -1, 1)) * R2D$6
6840
7172
  ];
6841
7173
  }
6842
7174
 
@@ -6891,16 +7223,6 @@
6891
7223
  return lam;
6892
7224
  }
6893
7225
 
6894
- function normalizeLongitude$3(lon) {
6895
- while (lon > 180) lon -= 360;
6896
- while (lon < -180) lon += 360;
6897
- return lon;
6898
- }
6899
-
6900
- function clamp$3(val, min, max) {
6901
- return Math.max(min, Math.min(max, val));
6902
- }
6903
-
6904
7226
  /*
6905
7227
  * Buckminster Fuller's Airocean arrangement of the icosahedron.
6906
7228
  *
@@ -8106,6 +8428,7 @@
8106
8428
  * equations, not Imago's power-law approximation.
8107
8429
  */
8108
8430
 
8431
+
8109
8432
  var D2R$2 = Math.PI / 180;
8110
8433
  var R2D$2 = 180 / Math.PI;
8111
8434
  var HALF_PI = Math.PI / 2;
@@ -8254,7 +8577,7 @@
8254
8577
  boundary: sector.sphericalBoundary.map(function(p) {
8255
8578
  var q = fromCanonical(p[0], p[1], orientation);
8256
8579
  return [
8257
- normalizeLongitude$2(q[0] * R2D$2 + lon0),
8580
+ normalizeLongitude$1(q[0] * R2D$2 + lon0),
8258
8581
  q[1] * R2D$2
8259
8582
  ];
8260
8583
  })
@@ -8334,7 +8657,7 @@
8334
8657
  return applyOobTransform(p, facet, oobKey);
8335
8658
  });
8336
8659
  [0, 1].forEach(function(folded) {
8337
- var foldedPolygon = clipPolygonAxis$1(oobPolygon, 0, 0, folded == 0);
8660
+ var foldedPolygon = clipPolygonAxis(oobPolygon, 0, 0, folded == 0);
8338
8661
  if (foldedPolygon.length < 3) return;
8339
8662
  foldedPolygon = foldedPolygon.map(function(p) {
8340
8663
  return applyFoldTransform(p, folded);
@@ -8463,26 +8786,26 @@
8463
8786
  var polygon = clipRasterOobPolygon(vertices, piece.oobKey);
8464
8787
  polygon = polygon.map(function(vertex) {
8465
8788
  var p = applyOobTransform([vertex.x, vertex.y], facet, piece.oobKey);
8466
- return copyRasterVertex$1(vertex, p[0], p[1]);
8789
+ return copyRasterVertex(vertex, p[0], p[1]);
8467
8790
  });
8468
- polygon = clipRasterPolygonAxis$1(
8791
+ polygon = clipRasterPolygonAxis(
8469
8792
  polygon, 'x', 0, piece.folded == 0);
8470
8793
  polygon = polygon.map(function(vertex) {
8471
8794
  var p = applyFoldTransform([vertex.x, vertex.y], piece.folded);
8472
- return copyRasterVertex$1(vertex, p[0], p[1]);
8795
+ return copyRasterVertex(vertex, p[0], p[1]);
8473
8796
  });
8474
8797
  polygon = polygon.map(function(vertex) {
8475
- return copyRasterVertex$1(
8798
+ return copyRasterVertex(
8476
8799
  vertex,
8477
8800
  vertex.x + LAYOUT_SHIFT +
8478
8801
  piece.wrap * 2 * BLOCK_HEIGHT - BLOCK_HEIGHT,
8479
8802
  vertex.y + 1.5
8480
8803
  );
8481
8804
  });
8482
- polygon = clipRasterPolygonAxis$1(polygon, 'x', XMIN, true);
8483
- polygon = clipRasterPolygonAxis$1(polygon, 'x', XMAX, false);
8484
- polygon = clipRasterPolygonAxis$1(polygon, 'y', YMIN, true);
8485
- polygon = clipRasterPolygonAxis$1(polygon, 'y', YMAX, false);
8805
+ polygon = clipRasterPolygonAxis(polygon, 'x', XMIN, true);
8806
+ polygon = clipRasterPolygonAxis(polygon, 'x', XMAX, false);
8807
+ polygon = clipRasterPolygonAxis(polygon, 'y', YMIN, true);
8808
+ polygon = clipRasterPolygonAxis(polygon, 'y', YMAX, false);
8486
8809
  polygon.forEach(function(vertex) {
8487
8810
  vertex.x *= EDGE_SCALE;
8488
8811
  vertex.y *= EDGE_SCALE;
@@ -8492,62 +8815,22 @@
8492
8815
 
8493
8816
  function clipRasterOobPolygon(polygon, oobKey) {
8494
8817
  if (oobKey == 'xpos') {
8495
- return clipRasterPolygonAxis$1(polygon, 'x', 3, true);
8818
+ return clipRasterPolygonAxis(polygon, 'x', 3, true);
8496
8819
  }
8497
8820
  if (oobKey == 'xneg') {
8498
- return clipRasterPolygonAxis$1(polygon, 'x', -3, false);
8821
+ return clipRasterPolygonAxis(polygon, 'x', -3, false);
8499
8822
  }
8500
8823
  if (oobKey == 'ypos' || oobKey == 'yneg') {
8501
- polygon = clipRasterPolygonAxis$1(polygon, 'x', -3, true);
8502
- polygon = clipRasterPolygonAxis$1(polygon, 'x', 3, false);
8503
- return clipRasterPolygonAxis$1(
8824
+ polygon = clipRasterPolygonAxis(polygon, 'x', -3, true);
8825
+ polygon = clipRasterPolygonAxis(polygon, 'x', 3, false);
8826
+ return clipRasterPolygonAxis(
8504
8827
  polygon, 'y', oobKey == 'ypos' ? SQRT3$1 : -SQRT3$1,
8505
8828
  oobKey == 'ypos');
8506
8829
  }
8507
- polygon = clipRasterPolygonAxis$1(polygon, 'x', -3, true);
8508
- polygon = clipRasterPolygonAxis$1(polygon, 'x', 3, false);
8509
- polygon = clipRasterPolygonAxis$1(polygon, 'y', -SQRT3$1, true);
8510
- return clipRasterPolygonAxis$1(polygon, 'y', SQRT3$1, false);
8511
- }
8512
-
8513
- function clipRasterPolygonAxis$1(polygon, axis, value, keepGreater) {
8514
- var output = [];
8515
- if (polygon.length === 0) return output;
8516
- var a = polygon[polygon.length - 1];
8517
- var aInside = keepGreater ?
8518
- a[axis] >= value - EPS$1 : a[axis] <= value + EPS$1;
8519
- polygon.forEach(function(b) {
8520
- var bInside = keepGreater ?
8521
- b[axis] >= value - EPS$1 : b[axis] <= value + EPS$1;
8522
- if (aInside != bInside) {
8523
- var t = (value - a[axis]) / (b[axis] - a[axis]);
8524
- var vertex = {
8525
- x: a.x + (b.x - a.x) * t,
8526
- y: a.y + (b.y - a.y) * t,
8527
- sx: a.sx + (b.sx - a.sx) * t,
8528
- sy: a.sy + (b.sy - a.sy) * t,
8529
- lon: a.lon + (b.lon - a.lon) * t,
8530
- lat: a.lat + (b.lat - a.lat) * t
8531
- };
8532
- vertex[axis] = value;
8533
- output.push(vertex);
8534
- }
8535
- if (bInside) output.push(copyRasterVertex$1(b, b.x, b.y));
8536
- a = b;
8537
- aInside = bInside;
8538
- });
8539
- return output;
8540
- }
8541
-
8542
- function copyRasterVertex$1(vertex, x, y) {
8543
- return {
8544
- x: x,
8545
- y: y,
8546
- sx: vertex.sx,
8547
- sy: vertex.sy,
8548
- lon: vertex.lon,
8549
- lat: vertex.lat
8550
- };
8830
+ polygon = clipRasterPolygonAxis(polygon, 'x', -3, true);
8831
+ polygon = clipRasterPolygonAxis(polygon, 'x', 3, false);
8832
+ polygon = clipRasterPolygonAxis(polygon, 'y', -SQRT3$1, true);
8833
+ return clipRasterPolygonAxis(polygon, 'y', SQRT3$1, false);
8551
8834
  }
8552
8835
 
8553
8836
  function applyOobTransform(p, facet, oobKey) {
@@ -8568,51 +8851,28 @@
8568
8851
  }
8569
8852
 
8570
8853
  function clipOobPolygon(polygon, oobKey) {
8571
- if (oobKey == 'xpos') return clipPolygonAxis$1(polygon, 0, 3, true);
8572
- if (oobKey == 'xneg') return clipPolygonAxis$1(polygon, 0, -3, false);
8854
+ if (oobKey == 'xpos') return clipPolygonAxis(polygon, 0, 3, true);
8855
+ if (oobKey == 'xneg') return clipPolygonAxis(polygon, 0, -3, false);
8573
8856
  if (oobKey == 'ypos') {
8574
- return clipPolygonAxis$1(
8575
- clipPolygonAxis$1(
8576
- clipPolygonAxis$1(polygon, 0, -3, true), 0, 3, false),
8857
+ return clipPolygonAxis(
8858
+ clipPolygonAxis(
8859
+ clipPolygonAxis(polygon, 0, -3, true), 0, 3, false),
8577
8860
  1, SQRT3$1, true);
8578
8861
  }
8579
8862
  if (oobKey == 'yneg') {
8580
- return clipPolygonAxis$1(
8581
- clipPolygonAxis$1(
8582
- clipPolygonAxis$1(polygon, 0, -3, true), 0, 3, false),
8863
+ return clipPolygonAxis(
8864
+ clipPolygonAxis(
8865
+ clipPolygonAxis(polygon, 0, -3, true), 0, 3, false),
8583
8866
  1, -SQRT3$1, false);
8584
8867
  }
8585
8868
  return clipRectangle(polygon, -3, -SQRT3$1, 3, SQRT3$1);
8586
8869
  }
8587
8870
 
8588
8871
  function clipRectangle(polygon, xmin, ymin, xmax, ymax) {
8589
- polygon = clipPolygonAxis$1(polygon, 0, xmin, true);
8590
- polygon = clipPolygonAxis$1(polygon, 0, xmax, false);
8591
- polygon = clipPolygonAxis$1(polygon, 1, ymin, true);
8592
- return clipPolygonAxis$1(polygon, 1, ymax, false);
8593
- }
8594
-
8595
- function clipPolygonAxis$1(polygon, axis, value, keepGreater) {
8596
- var output = [];
8597
- if (polygon.length === 0) return output;
8598
- var a = polygon[polygon.length - 1];
8599
- var aInside = keepGreater ? a[axis] >= value - EPS$1 : a[axis] <= value + EPS$1;
8600
- polygon.forEach(function(b) {
8601
- var bInside = keepGreater ? b[axis] >= value - EPS$1 : b[axis] <= value + EPS$1;
8602
- if (aInside != bInside) {
8603
- var t = (value - a[axis]) / (b[axis] - a[axis]);
8604
- var p = [
8605
- a[0] + (b[0] - a[0]) * t,
8606
- a[1] + (b[1] - a[1]) * t
8607
- ];
8608
- p[axis] = value;
8609
- output.push(p);
8610
- }
8611
- if (bInside) output.push(b.concat());
8612
- a = b;
8613
- aInside = bInside;
8614
- });
8615
- return output;
8872
+ polygon = clipPolygonAxis(polygon, 0, xmin, true);
8873
+ polygon = clipPolygonAxis(polygon, 0, xmax, false);
8874
+ polygon = clipPolygonAxis(polygon, 1, ymin, true);
8875
+ return clipPolygonAxis(polygon, 1, ymax, false);
8616
8876
  }
8617
8877
 
8618
8878
  function interpolateSphericalRadians(a, b, interval) {
@@ -8905,11 +9165,11 @@
8905
9165
  var y = edge[0][1] + (edge[1][1] - edge[0][1]) * t;
8906
9166
  var p = inverse(x * EDGE_SCALE, y * EDGE_SCALE);
8907
9167
  path.push([
8908
- normalizeLongitude$2(p[0] * R2D$2 + lon0),
9168
+ normalizeLongitude$1(p[0] * R2D$2 + lon0),
8909
9169
  p[1] * R2D$2
8910
9170
  ]);
8911
9171
  }
8912
- return memo.concat(splitPathAtAntimeridian$1(path));
9172
+ return memo.concat(splitPathAtAntimeridian$2(path));
8913
9173
  }, []);
8914
9174
  return paths.map(function(part) {
8915
9175
  part.mask_width = 4e-5;
@@ -8917,7 +9177,7 @@
8917
9177
  });
8918
9178
  }
8919
9179
 
8920
- function splitPathAtAntimeridian$1(path) {
9180
+ function splitPathAtAntimeridian$2(path) {
8921
9181
  var paths = [];
8922
9182
  var part = [path[0]];
8923
9183
  for (var i = 1; i < path.length; i++) {
@@ -8996,20 +9256,194 @@
8996
9256
  return lam;
8997
9257
  }
8998
9258
 
8999
- function normalizeLongitude$2(lon) {
9000
- while (lon > 180) lon -= 360;
9001
- while (lon < -180) lon += 360;
9002
- return lon;
9259
+ var Narukawa2022 = /*#__PURE__*/Object.freeze({
9260
+ __proto__: null,
9261
+ getNarukawa2022Engine: getNarukawa2022Engine,
9262
+ registerNarukawa2022Projection: registerNarukawa2022Projection
9263
+ });
9264
+
9265
+ // Lightweight hierarchical profiler.
9266
+ // Intended for ad-hoc performance work on hot pipelines (e.g. addIntersectionCuts).
9267
+ //
9268
+ // Usage:
9269
+ // import { profileStart, profileEnd, profileWrap, ... } from './utils/mapshaper-profile';
9270
+ // profileStart('phase'); doWork(); profileEnd('phase');
9271
+ // var result = profileWrap('phase', () => doWork());
9272
+ //
9273
+ // When disabled (the default) every call short-circuits in ~one comparison; safe
9274
+ // to leave in hot code paths. Enable from the CLI with the `-profile` command,
9275
+ // from JS with enableProfiling(), or by setting the MAPSHAPER_PROFILE env var.
9276
+ //
9277
+ // The profiler tracks a stack of currently-open phases so calls can nest. Each
9278
+ // unique stack path accumulates ms-elapsed and a call count. profileReport()
9279
+ // returns a flat array; formatProfileReport() pretty-prints a tree.
9280
+
9281
+ var ENABLED = false;
9282
+ var ROOT = makeNode('<root>');
9283
+ var STACK = [ROOT];
9284
+ var WALL_START = 0;
9285
+
9286
+ function makeNode(label) {
9287
+ return {
9288
+ label: label,
9289
+ totalMs: 0,
9290
+ selfMs: 0,
9291
+ calls: 0,
9292
+ childMsAccum: 0,
9293
+ children: Object.create(null)
9294
+ };
9003
9295
  }
9004
9296
 
9005
- function clamp$2(value, min, max) {
9006
- return Math.max(min, Math.min(max, value));
9297
+ function nowMs() {
9298
+ if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {
9299
+ // bigint -> number division: precision down to 1 microsecond is fine for our needs
9300
+ return Number(process.hrtime.bigint()) / 1e6;
9301
+ }
9302
+ if (typeof performance !== 'undefined' && performance.now) {
9303
+ return performance.now();
9304
+ }
9305
+ return Date.now();
9007
9306
  }
9008
9307
 
9009
- var Narukawa2022 = /*#__PURE__*/Object.freeze({
9308
+ function enableProfiling() {
9309
+ ENABLED = true;
9310
+ WALL_START = nowMs();
9311
+ }
9312
+
9313
+ function disableProfiling() {
9314
+ ENABLED = false;
9315
+ }
9316
+
9317
+ function profileEnabled() {
9318
+ return ENABLED;
9319
+ }
9320
+
9321
+ function profileReset() {
9322
+ ROOT = makeNode('<root>');
9323
+ STACK = [ROOT];
9324
+ WALL_START = ENABLED ? nowMs() : 0;
9325
+ }
9326
+
9327
+ // Open a new phase. Cheap (~one branch) when disabled.
9328
+ function profileStart(label) {
9329
+ if (!ENABLED) return;
9330
+ var parent = STACK[STACK.length - 1];
9331
+ var node = parent.children[label];
9332
+ if (!node) {
9333
+ node = makeNode(label);
9334
+ parent.children[label] = node;
9335
+ }
9336
+ node._t0 = nowMs();
9337
+ STACK.push(node);
9338
+ }
9339
+
9340
+ function profileEnd(label) {
9341
+ if (!ENABLED) return;
9342
+ var node = STACK[STACK.length - 1];
9343
+ if (label && node.label !== label) {
9344
+ // Mismatched labels usually mean an early return forgot to call profileEnd().
9345
+ // Walk up the stack until we find a match, closing the intervening frames.
9346
+ while (STACK.length > 1 && STACK[STACK.length - 1].label !== label) {
9347
+ profileEnd(STACK[STACK.length - 1].label);
9348
+ }
9349
+ node = STACK[STACK.length - 1];
9350
+ if (node.label !== label) return; // give up rather than throw
9351
+ }
9352
+ var elapsed = nowMs() - node._t0;
9353
+ node._t0 = 0;
9354
+ node.totalMs += elapsed;
9355
+ node.calls += 1;
9356
+ STACK.pop();
9357
+ var parent = STACK[STACK.length - 1];
9358
+ parent.childMsAccum += elapsed;
9359
+ }
9360
+
9361
+ // Wrap a function call. Re-throws if the callback throws but still closes the
9362
+ // phase so the stack stays consistent.
9363
+ function profileWrap(label, fn) {
9364
+ if (!ENABLED) return fn();
9365
+ profileStart(label);
9366
+ try {
9367
+ return fn();
9368
+ } finally {
9369
+ profileEnd(label);
9370
+ }
9371
+ }
9372
+
9373
+ // Build a flat tree report: array of {depth, label, totalMs, selfMs, calls}.
9374
+ function profileReport() {
9375
+ var rows = [];
9376
+ function visit(node, depth) {
9377
+ if (depth > 0) {
9378
+ rows.push({
9379
+ depth: depth - 1,
9380
+ label: node.label,
9381
+ totalMs: node.totalMs,
9382
+ selfMs: Math.max(0, node.totalMs - node.childMsAccum),
9383
+ calls: node.calls
9384
+ });
9385
+ }
9386
+ var keys = Object.keys(node.children);
9387
+ keys.sort(function(a, b) {
9388
+ return node.children[b].totalMs - node.children[a].totalMs;
9389
+ });
9390
+ for (var i = 0; i < keys.length; i++) {
9391
+ visit(node.children[keys[i]], depth + 1);
9392
+ }
9393
+ }
9394
+ visit(ROOT, 0);
9395
+ return rows;
9396
+ }
9397
+
9398
+ function profileWallElapsedMs() {
9399
+ return ENABLED && WALL_START ? nowMs() - WALL_START : 0;
9400
+ }
9401
+
9402
+ // Pretty-print the current report as a column-aligned tree.
9403
+ function formatProfileReport(opts) {
9404
+ var rows = profileReport();
9405
+ if (rows.length === 0) return '(profile is empty)';
9406
+ opts = opts || {};
9407
+ var indent = ' ';
9408
+ var lines = [];
9409
+ // header
9410
+ lines.push(['phase', 'total ms', 'self ms', 'calls'].join('\t'));
9411
+ for (var i = 0; i < rows.length; i++) {
9412
+ var r = rows[i];
9413
+ var label = '';
9414
+ for (var j = 0; j < r.depth; j++) label += indent;
9415
+ label += r.label;
9416
+ lines.push([
9417
+ label,
9418
+ r.totalMs.toFixed(2),
9419
+ r.selfMs.toFixed(2),
9420
+ String(r.calls)
9421
+ ].join('\t'));
9422
+ }
9423
+ if (opts.includeWall) {
9424
+ lines.push('');
9425
+ lines.push('wall elapsed ms: ' + profileWallElapsedMs().toFixed(2));
9426
+ }
9427
+ return lines.join('\n');
9428
+ }
9429
+
9430
+ // Honour an env var so users (and CI harnesses) can opt in without code changes.
9431
+ if (typeof process !== 'undefined' && process.env && process.env.MAPSHAPER_PROFILE) {
9432
+ enableProfiling();
9433
+ }
9434
+
9435
+ var Profile = /*#__PURE__*/Object.freeze({
9010
9436
  __proto__: null,
9011
- getNarukawa2022Engine: getNarukawa2022Engine,
9012
- registerNarukawa2022Projection: registerNarukawa2022Projection
9437
+ disableProfiling: disableProfiling,
9438
+ enableProfiling: enableProfiling,
9439
+ formatProfileReport: formatProfileReport,
9440
+ profileEnabled: profileEnabled,
9441
+ profileEnd: profileEnd,
9442
+ profileReport: profileReport,
9443
+ profileReset: profileReset,
9444
+ profileStart: profileStart,
9445
+ profileWallElapsedMs: profileWallElapsedMs,
9446
+ profileWrap: profileWrap
9013
9447
  });
9014
9448
 
9015
9449
  /*
@@ -9033,11 +9467,41 @@
9033
9467
  var SQRT3 = Math.sqrt(3);
9034
9468
  var ASIN_ONE_THIRD = Math.asin(1 / 3);
9035
9469
  var MARKLEY_LATITUDE = Math.acos(1 / 3) * 0.5 * R2D$1;
9470
+ var LAYOUT_PERIOD = 8;
9471
+ var MARKLEY_LAYOUT_PHASE = -2;
9472
+ var CALM_LAYOUT_PHASE = -1.5;
9036
9473
  var RECT_XMIN = -7;
9037
9474
  var RECT_XMAX = 1;
9038
9475
  var RECT_YMIN = -2 * SQRT3;
9039
9476
  var RECT_YMAX = 0;
9040
9477
  var EPS = 1e-12;
9478
+ var MIN_PIECE_AREA = 1e-6;
9479
+
9480
+ // Frame-cut tracing parameters (see createFrameCutPaths).
9481
+ //
9482
+ // FRAME_INSET is how far inside the frame the traced sampling lines run, and it
9483
+ // is bounded on both sides. Measured over 18003 samples with a 6000-sample
9484
+ // edge, the safe window is about [4e-8, 1.3e-7] and this value is its centre:
9485
+ //
9486
+ // * Too small and invertLeeRaw becomes ill-conditioned, because the frame edge
9487
+ // coincides with a Lee face boundary. The share of samples that fail to
9488
+ // invert climbs from 0.2% inside the window to 4% at 3e-8 and 14% at 1e-8,
9489
+ // and *which* samples fail depends on last-bit floating-point behaviour, so
9490
+ // different JS engines traced different seams from the same input.
9491
+ // * Too large and the strip of sphere between the traced seam and the true
9492
+ // frame edge survives the cut, leaving a hairline connection that shows up as
9493
+ // a long chord running along the frame edge. This appears by 1.6e-7.
9494
+ //
9495
+ // The 0.2% of samples that fail inside the window are the four facet vertices,
9496
+ // which genuinely have no inverse.
9497
+ var FRAME_INSET = 8e-8;
9498
+ // Guards the lower bound above. Inside the window the failure rate is 0.2%, so
9499
+ // this only trips on a regression, and the resulting map would be mis-cut.
9500
+ var MAX_FAILED_SAMPLE_RATIO = 0.02;
9501
+ // Samples per frame edge. This sets the resolution of the cut along the frame
9502
+ // (about 8 km on an Earth-sized map).
9503
+ var FRAME_SAMPLES = 6000;
9504
+
9041
9505
  var engines = {};
9042
9506
 
9043
9507
  var TETRAHEDRON_VERTICES = [
@@ -9081,7 +9545,10 @@
9081
9545
  function getLeeTetrahedralEngine(id) {
9082
9546
  id = id || 'markley';
9083
9547
  if (!engines[id]) {
9084
- engines[id] = createLeeTetrahedralEngine(getProjectionRotation(id));
9548
+ engines[id] = createLeeTetrahedralEngine(
9549
+ getProjectionRotation(id),
9550
+ id == 'markley' ? MARKLEY_LAYOUT_PHASE : CALM_LAYOUT_PHASE
9551
+ );
9085
9552
  }
9086
9553
  return engines[id];
9087
9554
  }
@@ -9097,23 +9564,24 @@
9097
9564
  };
9098
9565
  P.__projection_topology = engine.getTopology(P.lam0 * R2D$1);
9099
9566
  P.__projected_outline = engine.outline;
9567
+ P.__graticule_precision = 1;
9100
9568
  }
9101
9569
 
9102
9570
  function getProjectionRotation(id) {
9103
9571
  if (id == 'calm') {
9104
- // Exact D3 Euler equivalent of MapDesigner's published CALM aspect:
9105
- // latitude 77°, longitude 143°, central meridian -163°.
9572
+ // Kunimune's published CALM aspect (77°N, 143°E, central meridian
9573
+ // 163°W), transformed into this implementation's tetrahedral symmetry.
9106
9574
  return [
9107
- 19.57956907750618,
9108
- -12.42267097553935,
9109
- 3.8615690020132067
9575
+ 64.7261399569101,
9576
+ -39.84470062116125,
9577
+ -119.02303523591051
9110
9578
  ];
9111
9579
  }
9112
9580
  // Markley's singularities are at ±35.26439°, in the oceans.
9113
9581
  return [115, MARKLEY_LATITUDE - 90, 180];
9114
9582
  }
9115
9583
 
9116
- function createLeeTetrahedralEngine(rotation) {
9584
+ function createLeeTetrahedralEngine(rotation, layoutPhase) {
9117
9585
  var base = createPolyhedralProjection({
9118
9586
  faces: TETRAHEDRON_FACES,
9119
9587
  parents: [-1, 0, 0, 0],
@@ -9124,10 +9592,11 @@
9124
9592
  var normalization = getBaseNormalization(base.outline);
9125
9593
  var baseTopology = base.getTopology(0);
9126
9594
  var sourceRegions = createSourceRegions(rotation);
9127
- var pieces = createLayoutPieces(baseTopology.regions, normalization);
9128
- var pieceIndex = new Map(pieces.map(function(piece) {
9129
- return [getPieceKey(piece.face, piece.copy), piece.id];
9130
- }));
9595
+ var rasterPieces = createLayoutPieces(
9596
+ baseTopology.regions, normalization, layoutPhase);
9597
+ var rasterPieceIndex = indexPiecesByFaceAndCopy(rasterPieces);
9598
+ var rasterPieceIds = rasterPieces.map(function(piece) { return piece.id; });
9599
+ var seamCache = new Map();
9131
9600
  var outline = [[
9132
9601
  centerOutputPoint([RECT_XMIN, RECT_YMIN]),
9133
9602
  centerOutputPoint([RECT_XMAX, RECT_YMIN]),
@@ -9138,6 +9607,7 @@
9138
9607
 
9139
9608
  return {
9140
9609
  forward: forward,
9610
+ inverse: inverse,
9141
9611
  outline: outline,
9142
9612
  getTopology: getTopology
9143
9613
  };
@@ -9145,24 +9615,48 @@
9145
9615
  function forward(lam, phi) {
9146
9616
  var p = normalizeBasePoint(base.forward(lam, phi), normalization);
9147
9617
  var copy = getLayoutCopy(p);
9148
- return centerOutputPoint(applyMatrix(copy.matrix, p));
9618
+ var q = applyMatrix(copy.matrix, p);
9619
+ q[0] = wrapLayoutX(q[0] + layoutPhase);
9620
+ q[0] = clamp$2(q[0], RECT_XMIN, RECT_XMAX);
9621
+ q[1] = clamp$2(q[1], RECT_YMIN, RECT_YMAX);
9622
+ return centerOutputPoint(q);
9623
+ }
9624
+
9625
+ // Internal inverse used to trace the geographic cut corresponding to the
9626
+ // rectangular frame. The public projection remains forward-only.
9627
+ //
9628
+ // Layout pieces can overlap along their shared edges, so a candidate is only
9629
+ // accepted once it round-trips through forward().
9630
+ function inverse(x, y) {
9631
+ var q = uncenterOutputPoint([x, y]);
9632
+ for (var i = 0; i < rasterPieces.length; i++) {
9633
+ var piece = rasterPieces[i];
9634
+ var bounds = piece.bounds;
9635
+ if (x < bounds[0] || x > bounds[2] || y < bounds[1] || y > bounds[3]) {
9636
+ continue;
9637
+ }
9638
+ if (!pointInRing$2([x, y], piece.boundary)) continue;
9639
+ var p = applyMatrix(piece.inverse_matrix, q);
9640
+ if (getLayoutCopy(p).id != piece.copy) continue;
9641
+ p = denormalizeBasePoint(p, normalization);
9642
+ var spherical = base.invertFace(p[0], p[1], piece.face);
9643
+ if (!spherical) continue;
9644
+ var check = forward(spherical[0], spherical[1]);
9645
+ if (Math.hypot(check[0] - x, check[1] - y) < 1e-6) {
9646
+ return spherical;
9647
+ }
9648
+ }
9649
+ return null;
9149
9650
  }
9150
9651
 
9151
9652
  function getTopology(lon0) {
9152
- var topology = base.getTopology(lon0);
9153
- return {
9154
- regions: pieces.map(copyProjectedPiece),
9155
- seams: topology.seams.map(function(seam) {
9156
- return {
9157
- type: 'cut',
9158
- faces: seam.faces,
9159
- paths: seam.paths
9160
- };
9161
- }),
9653
+ var result = {
9654
+ interrupted: true,
9655
+ regions: rasterPieces.map(copyProjectedPiece),
9162
9656
  findRegion: findRegion,
9163
9657
  findTransitionRegion: findRegion,
9164
9658
  projectRegion: projectRegion,
9165
- raster_regions: pieces.map(copyRasterPiece),
9659
+ raster_regions: rasterPieces.map(copyRasterPiece),
9166
9660
  raster_source_regions: sourceRegions.map(function(region) {
9167
9661
  return {
9168
9662
  id: region.id,
@@ -9178,14 +9672,47 @@
9178
9672
  raster_cell_halo: false,
9179
9673
  fill_raster_mask: true,
9180
9674
  fill_raster_coverage: true,
9181
- findRasterRegion: findRegion,
9182
- projectRasterRegion: projectRegion,
9675
+ findRasterRegion: findRasterRegion,
9676
+ projectRasterRegion: projectRasterRegion,
9183
9677
  projectRasterSourceRegion: projectRasterSourceRegion,
9184
9678
  clipRasterRegionPolygon: clipRasterRegionPolygon,
9679
+ frame_bounds: [
9680
+ outline[0][0][0],
9681
+ outline[0][0][1],
9682
+ outline[0][2][0],
9683
+ outline[0][2][1]
9684
+ ],
9185
9685
  outline: outline.map(function(ring) {
9186
9686
  return ring.map(function(p) { return p.concat(); });
9187
9687
  })
9188
9688
  };
9689
+ // Rasters use the region mesh but never need geographic seam paths.
9690
+ // Tracing the rectangular cut is deferred until vector clipping requests it.
9691
+ Object.defineProperty(result, 'seams', {
9692
+ enumerable: true,
9693
+ get: function() {
9694
+ if (!seamCache.has(lon0)) {
9695
+ var topology = base.getTopology(lon0);
9696
+ var seams = topology.seams.map(function(seam) {
9697
+ return {
9698
+ type: 'attached',
9699
+ faces: seam.faces,
9700
+ paths: seam.paths
9701
+ };
9702
+ });
9703
+ var frameCut = createFrameCutPaths(inverse, lon0);
9704
+ seams.push({
9705
+ type: 'cut',
9706
+ paths: frameCut.paths,
9707
+ // How much of the frame the trace resolved; see FRAME_INSET.
9708
+ trace_stats: frameCut.stats
9709
+ });
9710
+ seamCache.set(lon0, seams);
9711
+ }
9712
+ return seamCache.get(lon0);
9713
+ }
9714
+ });
9715
+ return result;
9189
9716
 
9190
9717
  function findRegion(lon, lat) {
9191
9718
  var lam = normalizeLongitude$1(lon - lon0) * D2R$1;
@@ -9193,11 +9720,32 @@
9193
9720
  var face = base.findFace(lam, phi);
9194
9721
  var p = normalizeBasePoint(base.forwardFace(lam, phi, face), normalization);
9195
9722
  var copy = getLayoutCopy(p);
9196
- return pieceIndex.get(getPieceKey(face, copy.id));
9723
+ var q = applyMatrix(copy.matrix, p);
9724
+ q[0] = wrapLayoutX(q[0] + layoutPhase);
9725
+ q[0] = clamp$2(q[0], RECT_XMIN, RECT_XMAX);
9726
+ q[1] = clamp$2(q[1], RECT_YMIN, RECT_YMAX);
9727
+ q = centerOutputPoint(q);
9728
+ var ids = rasterPieceIndex.get(getPieceKey(face, copy.id)) || [];
9729
+ var region = findPieceContainingPoint(rasterPieces, ids, q);
9730
+ if (region === undefined) {
9731
+ region = findPieceContainingPoint(rasterPieces, rasterPieceIds, q);
9732
+ }
9733
+ return region;
9197
9734
  }
9198
9735
 
9199
9736
  function projectRegion(lon, lat, regionId) {
9200
- var piece = pieces[regionId];
9737
+ return projectPiece(lon, lat, rasterPieces[regionId]);
9738
+ }
9739
+
9740
+ function findRasterRegion(lon, lat) {
9741
+ return findRegion(lon, lat);
9742
+ }
9743
+
9744
+ function projectRasterRegion(lon, lat, regionId) {
9745
+ return projectPiece(lon, lat, rasterPieces[regionId]);
9746
+ }
9747
+
9748
+ function projectPiece(lon, lat, piece) {
9201
9749
  var lam = normalizeLongitude$1(lon - lon0) * D2R$1;
9202
9750
  var p = normalizeBasePoint(
9203
9751
  base.forwardFace(lam, lat * D2R$1, piece.face),
@@ -9215,7 +9763,7 @@
9215
9763
  }
9216
9764
 
9217
9765
  function clipRasterRegionPolygon(vertices, regionId) {
9218
- var piece = pieces[regionId];
9766
+ var piece = rasterPieces[regionId];
9219
9767
  var polygon = vertices.map(function(vertex) {
9220
9768
  var p = applyMatrix(piece.matrix, [vertex.x, vertex.y]);
9221
9769
  return copyRasterVertex(vertex, p[0], p[1]);
@@ -9246,39 +9794,66 @@
9246
9794
  });
9247
9795
  }
9248
9796
 
9249
- function createLayoutPieces(regions, normalization) {
9797
+ function createLayoutPieces(regions, normalization, layoutPhase) {
9250
9798
  var pieces = [];
9251
9799
  regions.forEach(function(region) {
9252
9800
  var boundary = region.projected_boundary.map(function(p) {
9253
9801
  return normalizeBasePoint(p, normalization);
9254
9802
  });
9255
9803
  LAYOUT_COPIES.forEach(function(copy) {
9256
- var polygon = boundary.map(function(p) {
9257
- return applyMatrix(copy.matrix, p);
9258
- });
9259
- polygon = clipPolygonAxis(polygon, 0, RECT_XMIN, true);
9260
- polygon = clipPolygonAxis(polygon, 0, RECT_XMAX, false);
9261
- polygon = clipPolygonAxis(polygon, 1, RECT_YMIN, true);
9262
- polygon = clipPolygonAxis(polygon, 1, RECT_YMAX, false);
9263
- if (polygon.length < 3 || Math.abs(getRingArea$2(polygon)) < EPS) return;
9264
- var id = pieces.length;
9265
- pieces.push({
9266
- id: id,
9267
- face: region.id,
9268
- source_region: region.id,
9269
- copy: copy.id,
9270
- matrix: copy.matrix,
9271
- boundary: polygon.map(centerOutputPoint)
9804
+ var polygon = clipLayoutCopyDomain(boundary, copy.id);
9805
+ if (polygon.length < 3 ||
9806
+ Math.abs(getRingArea$2(polygon)) < MIN_PIECE_AREA) return;
9807
+ [-LAYOUT_PERIOD, 0, LAYOUT_PERIOD].forEach(function(wrap) {
9808
+ var matrix = shiftMatrixX(copy.matrix, layoutPhase + wrap);
9809
+ var projected = polygon.map(function(p) {
9810
+ return applyMatrix(matrix, p);
9811
+ });
9812
+ projected = clipPolygonAxis(projected, 0, RECT_XMIN, true);
9813
+ projected = clipPolygonAxis(projected, 0, RECT_XMAX, false);
9814
+ projected = clipPolygonAxis(projected, 1, RECT_YMIN, true);
9815
+ projected = clipPolygonAxis(projected, 1, RECT_YMAX, false);
9816
+ if (projected.length < 3 ||
9817
+ Math.abs(getRingArea$2(projected)) < MIN_PIECE_AREA) return;
9818
+ var boundary = projected.map(centerOutputPoint);
9819
+ pieces.push({
9820
+ id: pieces.length,
9821
+ face: region.id,
9822
+ source_region: region.id,
9823
+ copy: copy.id,
9824
+ matrix: matrix,
9825
+ inverse_matrix: invertMatrix(matrix),
9826
+ bounds: getRingBounds(boundary),
9827
+ boundary: boundary
9828
+ });
9272
9829
  });
9273
9830
  });
9274
9831
  });
9275
9832
  return pieces;
9276
9833
  }
9277
9834
 
9835
+ function clipLayoutCopyDomain(polygon, copyId) {
9836
+ if (copyId == 0) { // green
9837
+ polygon = clipPolygonAxis(polygon, 1, 0, true);
9838
+ polygon = clipPolygonAxis(polygon, 0, 3, false);
9839
+ } else if (copyId == 2) { // blue
9840
+ polygon = clipPolygonAxis(polygon, 1, 0, true);
9841
+ polygon = clipPolygonAxis(polygon, 0, 3, true);
9842
+ } else if (copyId == 1) { // red
9843
+ polygon = clipPolygonAxis(polygon, 1, 0, false);
9844
+ polygon = clipPolygonAxis(polygon, 0, 1, false);
9845
+ } else { // orange
9846
+ polygon = clipPolygonAxis(polygon, 1, 0, false);
9847
+ polygon = clipPolygonAxis(polygon, 0, 1, true);
9848
+ }
9849
+ return polygon;
9850
+ }
9851
+
9278
9852
  function copyProjectedPiece(piece) {
9279
9853
  return {
9280
9854
  id: piece.id,
9281
9855
  source_region: piece.source_region,
9856
+ copy: piece.copy,
9282
9857
  projected_boundary: closeRing$1(piece.boundary)
9283
9858
  };
9284
9859
  }
@@ -9291,9 +9866,87 @@
9291
9866
  };
9292
9867
  }
9293
9868
 
9869
+ // Trace the geographic curves that the rectangular frame corresponds to. The
9870
+ // repeated tetrahedral net is continuous inside the frame, so its only
9871
+ // geographic cuts are the three independent sides of the periodic rectangle.
9872
+ //
9873
+ // Sampling is uniform in frame coordinates rather than adaptive. The traced
9874
+ // curve becomes the centreline of the seam mask, so the polygon edges left
9875
+ // behind by the cut inherit this spacing where they run along the frame; the
9876
+ // even resolution is what lets splitPolygonFrameChords recognize a run along a
9877
+ // frame side. Sampling by geographic curvature instead concentrates vertices
9878
+ // where the map curves and leaves the frame edges coarse, which reintroduces
9879
+ // the long chords along the frame that the cut is meant to remove.
9880
+ function createFrameCutPaths(inverse, lon0) {
9881
+ profileStart('lee.traceFrameCuts');
9882
+ var e = FRAME_INSET;
9883
+ var xmin = centerOutputPoint([RECT_XMIN, 0])[0];
9884
+ var xmax = centerOutputPoint([RECT_XMAX, 0])[0];
9885
+ var ymin = centerOutputPoint([0, RECT_YMIN])[1];
9886
+ var ymax = centerOutputPoint([0, RECT_YMAX])[1];
9887
+ var edges = [
9888
+ [[xmin + e, ymin], [xmin + e, ymax]],
9889
+ [[xmin, ymax - e], [xmax, ymax - e]],
9890
+ [[xmin, ymin + e], [xmax, ymin + e]]
9891
+ ];
9892
+ var stats = {sample_count: 0, failed_count: 0};
9893
+ var paths = edges.reduce(function(memo, edge) {
9894
+ var path = [];
9895
+ for (var i = 0; i <= FRAME_SAMPLES; i++) {
9896
+ var t = i / FRAME_SAMPLES;
9897
+ var p = inverse(
9898
+ edge[0][0] + (edge[1][0] - edge[0][0]) * t,
9899
+ edge[0][1] + (edge[1][1] - edge[0][1]) * t
9900
+ );
9901
+ stats.sample_count++;
9902
+ // Samples without an inverse occur only at the facet vertices, which
9903
+ // both neighbours converge on, so the chord across the gap is short.
9904
+ if (!p) {
9905
+ stats.failed_count++;
9906
+ continue;
9907
+ }
9908
+ path.push([normalizeLongitude$1(p[0] * R2D$1 + lon0), p[1] * R2D$1]);
9909
+ }
9910
+ return memo.concat(splitPathAtAntimeridian$1(path));
9911
+ }, []).map(function(path) {
9912
+ path.mask_width = 4e-5;
9913
+ return path;
9914
+ });
9915
+ profileEnd('lee.traceFrameCuts');
9916
+ if (stats.failed_count > stats.sample_count * MAX_FAILED_SAMPLE_RATIO) {
9917
+ error('Unable to trace the projection frame: ' + stats.failed_count +
9918
+ ' of ' + stats.sample_count +
9919
+ ' frame samples had no inverse (see FRAME_INSET).');
9920
+ }
9921
+ return {paths: paths, stats: stats};
9922
+ }
9923
+
9924
+ function splitPathAtAntimeridian$1(path) {
9925
+ if (path.length < 2) return [];
9926
+ var paths = [];
9927
+ var part = [path[0]];
9928
+ for (var i = 1; i < path.length; i++) {
9929
+ var a = path[i - 1];
9930
+ var b = path[i];
9931
+ if (Math.abs(a[0] - b[0]) > 180) {
9932
+ var adjusted = b[0] + (b[0] < a[0] ? 360 : -360);
9933
+ var edge = a[0] < 0 ? -180 : 180;
9934
+ var t = (edge - a[0]) / (adjusted - a[0]);
9935
+ var lat = a[1] + (b[1] - a[1]) * t;
9936
+ part.push([edge, lat]);
9937
+ paths.push(part);
9938
+ part = [[-edge, lat], b];
9939
+ } else {
9940
+ part.push(b);
9941
+ }
9942
+ }
9943
+ if (part.length > 1) paths.push(part);
9944
+ return paths;
9945
+ }
9946
+
9294
9947
  function closeRing$1(ring) {
9295
9948
  var copy = ring.map(function(p) { return p.concat(); });
9296
- if (copy.length && !pointsEqual$1(copy[0], copy[copy.length - 1])) {
9949
+ if (copy.length && !pointsEqual$2(copy[0], copy[copy.length - 1])) {
9297
9950
  copy.push(copy[0].concat());
9298
9951
  }
9299
9952
  return copy;
@@ -9313,6 +9966,13 @@
9313
9966
  ];
9314
9967
  }
9315
9968
 
9969
+ function uncenterOutputPoint(p) {
9970
+ return [
9971
+ p[0] + (RECT_XMIN + RECT_XMAX) / 2,
9972
+ p[1] + (RECT_YMIN + RECT_YMAX) / 2
9973
+ ];
9974
+ }
9975
+
9316
9976
  function getBaseNormalization(outline) {
9317
9977
  var xmin = Infinity;
9318
9978
  var ymin = Infinity;
@@ -9340,72 +10000,142 @@
9340
10000
  ];
9341
10001
  }
9342
10002
 
10003
+ function denormalizeBasePoint(p, normalization) {
10004
+ return [
10005
+ p[0] / normalization.scale + normalization.cx,
10006
+ -p[1] / normalization.scale + normalization.cy
10007
+ ];
10008
+ }
10009
+
9343
10010
  function createLeeFaceProjector(face) {
9344
10011
  var c = face.centroid;
9345
10012
  var rotation = Math.abs(c[1]) == 90 ?
9346
10013
  [0, -c[1], -30] :
9347
10014
  [-c[0], -c[1], 30];
9348
- return function(lam, phi) {
10015
+ function project(lam, phi) {
9349
10016
  var p = rotateSphericalRadians(lam, phi, rotation);
9350
10017
  var q = leeRaw(p[0], p[1]);
9351
10018
  return [q[0], -q[1]];
10019
+ }
10020
+ project.invert = function(x, y) {
10021
+ var p = invertLeeRaw(x, -y);
10022
+ return p && rotateSphericalRadians(p[0], p[1], rotation, true);
9352
10023
  };
10024
+ return project;
9353
10025
  }
9354
10026
 
10027
+ // leeRaw runs tens of times per Newton step in invertLeeRaw, so its constant
10028
+ // tables and the three cube roots of unity are built once here rather than on
10029
+ // every call, and the Horner loops below accumulate into scalars instead of
10030
+ // allocating a complex pair per term. The arithmetic is unchanged.
10031
+ var LEE_W1 = 1.4021821053254548;
10032
+ var LEE_COEFFICIENTS = [
10033
+ 1.15470053837925, 0.192450089729875, 0.0481125224324687,
10034
+ 0.010309826235529, 3.34114739114366e-4, -0.00150351632601465,
10035
+ -0.0012304417796231, -675190201960282e-18,
10036
+ -284084537293856e-18, -821205120500051e-19,
10037
+ -159257630018706e-20, 1.91691805888369e-5,
10038
+ 1.73095888028726e-5, 1.03865580818367e-5,
10039
+ 4.70614523937179e-6, 1.4413500104181e-6,
10040
+ 1.92757960170179e-8, -3.82869799649063e-7,
10041
+ -3.57526015225576e-7, -2.2175964844211e-7
10042
+ ];
10043
+ var LEE_H0 = [1, 1 / 8, 3 / 56, 1 / 32, 35 / 1664, 63 / 4096, 231 / 19456];
10044
+ var LEE_ROTATIONS = [0, 1, 2].map(function(i) {
10045
+ return complexPower([-0.5, SQRT3 / 2], [i, 0]);
10046
+ });
10047
+
9355
10048
  function leeRaw(lam, phi) {
9356
- var w = [-0.5, SQRT3 / 2];
9357
- var z = complexMultiply(stereographicRaw(lam, phi), [SQRT2, 0]);
9358
- var powers = [0, 1, 2].map(function(i) {
9359
- return complexPower(w, [i, 0]);
9360
- });
10049
+ var s = stereographicRaw(lam, phi);
10050
+ var z = [s[0] * SQRT2, s[1] * SQRT2];
10051
+ var i, j;
10052
+ // Pick the sector whose rotation maximizes the real part of rot * z.
9361
10053
  var sector = 0;
9362
- for (var i = 1; i < powers.length; i++) {
9363
- if (complexMultiply(z, powers[i])[0] >
9364
- complexMultiply(z, powers[sector])[0]) sector = i;
10054
+ var best = z[0] * LEE_ROTATIONS[0][0] - z[1] * LEE_ROTATIONS[0][1];
10055
+ for (i = 1; i < LEE_ROTATIONS.length; i++) {
10056
+ var re = z[0] * LEE_ROTATIONS[i][0] - z[1] * LEE_ROTATIONS[i][1];
10057
+ if (re > best) {
10058
+ best = re;
10059
+ sector = i;
10060
+ }
9365
10061
  }
9366
- var rot = powers[sector];
10062
+ var rot = LEE_ROTATIONS[sector];
9367
10063
  var n = complexNorm(z);
9368
10064
  var h = [0, 0];
9369
10065
  var k = [0, 0];
9370
10066
 
9371
10067
  if (n > 0.3) {
9372
10068
  var y = complexSubtract([1, 0], complexMultiply(rot, z));
9373
- var w1 = 1.4021821053254548;
9374
- var coefficients = [
9375
- 1.15470053837925, 0.192450089729875, 0.0481125224324687,
9376
- 0.010309826235529, 3.34114739114366e-4, -0.00150351632601465,
9377
- -0.0012304417796231, -675190201960282e-18,
9378
- -284084537293856e-18, -821205120500051e-19,
9379
- -159257630018706e-20, 1.91691805888369e-5,
9380
- 1.73095888028726e-5, 1.03865580818367e-5,
9381
- 4.70614523937179e-6, 1.4413500104181e-6,
9382
- 1.92757960170179e-8, -3.82869799649063e-7,
9383
- -3.57526015225576e-7, -2.2175964844211e-7
9384
- ];
9385
- var g = [0, 0];
9386
- for (i = coefficients.length - 1; i >= 0; i--) {
9387
- g = complexAdd([coefficients[i], 0], complexMultiply(g, y));
9388
- }
9389
- k = complexSubtract([w1, 0], complexMultiply(complexPower(y, 0.5), g));
10069
+ var g0 = 0;
10070
+ var g1 = 0;
10071
+ for (i = LEE_COEFFICIENTS.length - 1; i >= 0; i--) {
10072
+ j = g1 * y[0] + g0 * y[1];
10073
+ g0 = LEE_COEFFICIENTS[i] + (g0 * y[0] - g1 * y[1]);
10074
+ g1 = j;
10075
+ }
10076
+ k = complexSubtract(
10077
+ [LEE_W1, 0],
10078
+ complexMultiply(complexPower(y, 0.5), [g0, g1]));
9390
10079
  k = complexMultiply(complexMultiply(k, rot), rot);
9391
10080
  }
9392
10081
 
9393
10082
  if (n < 0.5) {
9394
- var h0 = [1, 1 / 8, 3 / 56, 1 / 32, 35 / 1664, 63 / 4096, 231 / 19456];
9395
10083
  var z3 = complexPower(z, [3, 0]);
9396
- for (i = h0.length - 1; i >= 0; i--) {
9397
- h = complexAdd([h0[i], 0], complexMultiply(h, z3));
10084
+ var h0 = 0;
10085
+ var h1 = 0;
10086
+ for (i = LEE_H0.length - 1; i >= 0; i--) {
10087
+ j = h1 * z3[0] + h0 * z3[1];
10088
+ h0 = LEE_H0[i] + (h0 * z3[0] - h1 * z3[1]);
10089
+ h1 = j;
9398
10090
  }
9399
- h = complexMultiply(h, z);
10091
+ h = complexMultiply([h0, h1], z);
9400
10092
  }
9401
10093
 
9402
10094
  if (n < 0.3) return h;
9403
10095
  if (n > 0.5) return k;
9404
10096
  var t = (n - 0.3) / 0.2;
9405
- return complexAdd(
9406
- complexMultiply(k, [t, 0]),
9407
- complexMultiply(h, [1 - t, 0])
9408
- );
10097
+ return [k[0] * t + h[0] * (1 - t), k[1] * t + h[1] * (1 - t)];
10098
+ }
10099
+
10100
+ function invertLeeRaw(x, y) {
10101
+ var lam = x;
10102
+ var phi = y * 0.5;
10103
+ var da = 0;
10104
+ var db = 0;
10105
+ var err2 = Infinity;
10106
+ var eps = 1e-12;
10107
+ for (var i = 0; i < 40; i++) {
10108
+ var q = leeRaw(lam, phi);
10109
+ var tx = q[0] - x;
10110
+ var ty = q[1] - y;
10111
+ if (Math.abs(tx) < eps && Math.abs(ty) < eps) break;
10112
+ var error = tx * tx + ty * ty;
10113
+ if (error > err2) {
10114
+ lam -= da /= 2;
10115
+ phi -= db /= 2;
10116
+ continue;
10117
+ }
10118
+ err2 = error;
10119
+ var ea = (lam > 0 ? -1 : 1) * eps;
10120
+ var eb = (phi > 0 ? -1 : 1) * eps;
10121
+ var qa = leeRaw(lam + ea, phi);
10122
+ var qb = leeRaw(lam, phi + eb);
10123
+ var dxa = (qa[0] - q[0]) / ea;
10124
+ var dya = (qa[1] - q[1]) / ea;
10125
+ var dxb = (qb[0] - q[0]) / eb;
10126
+ var dyb = (qb[1] - q[1]) / eb;
10127
+ var det = dyb * dxa - dya * dxb;
10128
+ if (Math.abs(det) < 1e-14) break;
10129
+ var scale = (Math.abs(det) < 0.5 ? 0.5 : 1) / det;
10130
+ da = (ty * dxb - tx * dyb) * scale;
10131
+ db = (tx * dya - ty * dxa) * scale;
10132
+ lam += da;
10133
+ phi += db;
10134
+ if (Math.abs(da) < eps && Math.abs(db) < eps) break;
10135
+ }
10136
+ var check = leeRaw(lam, phi);
10137
+ return Math.hypot(check[0] - x, check[1] - y) < 1e-8 ?
10138
+ [lam, phi] : null;
9409
10139
  }
9410
10140
 
9411
10141
  function stereographicRaw(lam, phi) {
@@ -9414,10 +10144,6 @@
9414
10144
  return [k * cosPhi * Math.sin(lam), k * Math.sin(phi)];
9415
10145
  }
9416
10146
 
9417
- function complexAdd(a, b) {
9418
- return [a[0] + b[0], a[1] + b[1]];
9419
- }
9420
-
9421
10147
  function complexSubtract(a, b) {
9422
10148
  return [a[0] - b[0], a[1] - b[1]];
9423
10149
  }
@@ -9445,79 +10171,27 @@
9445
10171
  return [magnitude * Math.cos(angle), magnitude * Math.sin(angle)];
9446
10172
  }
9447
10173
 
9448
- function applyMatrix(m, p) {
9449
- return [
9450
- m[0] * p[0] + m[1] * p[1] + m[2],
9451
- m[3] * p[0] + m[4] * p[1] + m[5]
9452
- ];
10174
+ function shiftMatrixX(matrix, offset) {
10175
+ var shifted = matrix.concat();
10176
+ shifted[2] += offset;
10177
+ return shifted;
9453
10178
  }
9454
10179
 
9455
- function clipPolygonAxis(polygon, axis, value, keepGreater) {
9456
- var output = [];
9457
- if (!polygon.length) return output;
9458
- var a = polygon[polygon.length - 1];
9459
- var aInside = keepGreater ? a[axis] >= value - EPS : a[axis] <= value + EPS;
9460
- polygon.forEach(function(b) {
9461
- var bInside = keepGreater ? b[axis] >= value - EPS : b[axis] <= value + EPS;
9462
- if (aInside != bInside) {
9463
- var t = (value - a[axis]) / (b[axis] - a[axis]);
9464
- var p = [
9465
- a[0] + (b[0] - a[0]) * t,
9466
- a[1] + (b[1] - a[1]) * t
9467
- ];
9468
- p[axis] = value;
9469
- output.push(p);
9470
- }
9471
- if (bInside) output.push(b.concat());
9472
- a = b;
9473
- aInside = bInside;
9474
- });
9475
- return output;
10180
+ function wrapLayoutX(x) {
10181
+ while (x < RECT_XMIN) x += LAYOUT_PERIOD;
10182
+ while (x > RECT_XMAX) x -= LAYOUT_PERIOD;
10183
+ return x;
9476
10184
  }
9477
10185
 
9478
- function clipRasterPolygonAxis(polygon, axis, value, keepGreater) {
9479
- var output = [];
9480
- if (!polygon.length) return output;
9481
- var a = polygon[polygon.length - 1];
9482
- var aInside = keepGreater ? a[axis] >= value - EPS : a[axis] <= value + EPS;
9483
- polygon.forEach(function(b) {
9484
- var bInside = keepGreater ? b[axis] >= value - EPS : b[axis] <= value + EPS;
9485
- if (aInside != bInside) {
9486
- var t = (value - a[axis]) / (b[axis] - a[axis]);
9487
- var vertex = {
9488
- x: a.x + (b.x - a.x) * t,
9489
- y: a.y + (b.y - a.y) * t,
9490
- sx: a.sx + (b.sx - a.sx) * t,
9491
- sy: a.sy + (b.sy - a.sy) * t,
9492
- lon: interpolateLongitude(a.lon, b.lon, t),
9493
- lat: a.lat + (b.lat - a.lat) * t
9494
- };
9495
- vertex[axis] = value;
9496
- output.push(vertex);
9497
- }
9498
- if (bInside) output.push(copyRasterVertex(b, b.x, b.y));
9499
- a = b;
9500
- aInside = bInside;
10186
+ function getRingBounds(ring) {
10187
+ var bounds = [Infinity, Infinity, -Infinity, -Infinity];
10188
+ ring.forEach(function(p) {
10189
+ if (p[0] < bounds[0]) bounds[0] = p[0];
10190
+ if (p[1] < bounds[1]) bounds[1] = p[1];
10191
+ if (p[0] > bounds[2]) bounds[2] = p[0];
10192
+ if (p[1] > bounds[3]) bounds[3] = p[1];
9501
10193
  });
9502
- return output;
9503
- }
9504
-
9505
- function copyRasterVertex(vertex, x, y) {
9506
- return {
9507
- x: x,
9508
- y: y,
9509
- sx: vertex.sx,
9510
- sy: vertex.sy,
9511
- lon: vertex.lon,
9512
- lat: vertex.lat
9513
- };
9514
- }
9515
-
9516
- function interpolateLongitude(a, b, t) {
9517
- var delta = b - a;
9518
- if (delta > 180) delta -= 360;
9519
- if (delta < -180) delta += 360;
9520
- return normalizeLongitude$1(a + delta * t);
10194
+ return bounds;
9521
10195
  }
9522
10196
 
9523
10197
  function getRingArea$2(ring) {
@@ -9528,20 +10202,51 @@
9528
10202
  return area / 2;
9529
10203
  }
9530
10204
 
10205
+ function indexPiecesByFaceAndCopy(pieces) {
10206
+ var index = new Map();
10207
+ pieces.forEach(function(piece) {
10208
+ var key = getPieceKey(piece.face, piece.copy);
10209
+ if (!index.has(key)) index.set(key, []);
10210
+ index.get(key).push(piece.id);
10211
+ });
10212
+ return index;
10213
+ }
10214
+
10215
+ function findPieceContainingPoint(pieces, ids, point) {
10216
+ for (var i = 0; i < ids.length; i++) {
10217
+ if (pointInRing$2(point, pieces[ids[i]].boundary)) return ids[i];
10218
+ }
10219
+ }
10220
+
10221
+ function pointInRing$2(point, ring) {
10222
+ var inside = false;
10223
+ for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
10224
+ var a = ring[j];
10225
+ var b = ring[i];
10226
+ var cross = (point[0] - a[0]) * (b[1] - a[1]) -
10227
+ (point[1] - a[1]) * (b[0] - a[0]);
10228
+ if (Math.abs(cross) < 1e-8 &&
10229
+ point[0] >= Math.min(a[0], b[0]) - 1e-8 &&
10230
+ point[0] <= Math.max(a[0], b[0]) + 1e-8 &&
10231
+ point[1] >= Math.min(a[1], b[1]) - 1e-8 &&
10232
+ point[1] <= Math.max(a[1], b[1]) + 1e-8) return true;
10233
+ if ((a[1] > point[1]) != (b[1] > point[1]) &&
10234
+ point[0] < (b[0] - a[0]) * (point[1] - a[1]) /
10235
+ (b[1] - a[1]) + a[0]) {
10236
+ inside = !inside;
10237
+ }
10238
+ }
10239
+ return inside;
10240
+ }
10241
+
9531
10242
  function getPieceKey(face, copy) {
9532
10243
  return face + ':' + copy;
9533
10244
  }
9534
10245
 
9535
- function pointsEqual$1(a, b) {
10246
+ function pointsEqual$2(a, b) {
9536
10247
  return Math.abs(a[0] - b[0]) < EPS && Math.abs(a[1] - b[1]) < EPS;
9537
10248
  }
9538
10249
 
9539
- function normalizeLongitude$1(lon) {
9540
- lon = (lon + 180) % 360;
9541
- if (lon < 0) lon += 360;
9542
- return lon - 180;
9543
- }
9544
-
9545
10250
  var mproj$1 = require$1('mproj');
9546
10251
  registerDymaxionProjections(mproj$1);
9547
10252
  registerButterflyProjections(mproj$1);
@@ -11859,190 +12564,6 @@
11859
12564
  };
11860
12565
  }
11861
12566
 
11862
- // Lightweight hierarchical profiler.
11863
- // Intended for ad-hoc performance work on hot pipelines (e.g. addIntersectionCuts).
11864
- //
11865
- // Usage:
11866
- // import { profileStart, profileEnd, profileWrap, ... } from './utils/mapshaper-profile';
11867
- // profileStart('phase'); doWork(); profileEnd('phase');
11868
- // var result = profileWrap('phase', () => doWork());
11869
- //
11870
- // When disabled (the default) every call short-circuits in ~one comparison; safe
11871
- // to leave in hot code paths. Enable from the CLI with the `-profile` command,
11872
- // from JS with enableProfiling(), or by setting the MAPSHAPER_PROFILE env var.
11873
- //
11874
- // The profiler tracks a stack of currently-open phases so calls can nest. Each
11875
- // unique stack path accumulates ms-elapsed and a call count. profileReport()
11876
- // returns a flat array; formatProfileReport() pretty-prints a tree.
11877
-
11878
- var ENABLED = false;
11879
- var ROOT = makeNode('<root>');
11880
- var STACK = [ROOT];
11881
- var WALL_START = 0;
11882
-
11883
- function makeNode(label) {
11884
- return {
11885
- label: label,
11886
- totalMs: 0,
11887
- selfMs: 0,
11888
- calls: 0,
11889
- childMsAccum: 0,
11890
- children: Object.create(null)
11891
- };
11892
- }
11893
-
11894
- function nowMs() {
11895
- if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {
11896
- // bigint -> number division: precision down to 1 microsecond is fine for our needs
11897
- return Number(process.hrtime.bigint()) / 1e6;
11898
- }
11899
- if (typeof performance !== 'undefined' && performance.now) {
11900
- return performance.now();
11901
- }
11902
- return Date.now();
11903
- }
11904
-
11905
- function enableProfiling() {
11906
- ENABLED = true;
11907
- WALL_START = nowMs();
11908
- }
11909
-
11910
- function disableProfiling() {
11911
- ENABLED = false;
11912
- }
11913
-
11914
- function profileEnabled() {
11915
- return ENABLED;
11916
- }
11917
-
11918
- function profileReset() {
11919
- ROOT = makeNode('<root>');
11920
- STACK = [ROOT];
11921
- WALL_START = ENABLED ? nowMs() : 0;
11922
- }
11923
-
11924
- // Open a new phase. Cheap (~one branch) when disabled.
11925
- function profileStart(label) {
11926
- if (!ENABLED) return;
11927
- var parent = STACK[STACK.length - 1];
11928
- var node = parent.children[label];
11929
- if (!node) {
11930
- node = makeNode(label);
11931
- parent.children[label] = node;
11932
- }
11933
- node._t0 = nowMs();
11934
- STACK.push(node);
11935
- }
11936
-
11937
- function profileEnd(label) {
11938
- if (!ENABLED) return;
11939
- var node = STACK[STACK.length - 1];
11940
- if (label && node.label !== label) {
11941
- // Mismatched labels usually mean an early return forgot to call profileEnd().
11942
- // Walk up the stack until we find a match, closing the intervening frames.
11943
- while (STACK.length > 1 && STACK[STACK.length - 1].label !== label) {
11944
- profileEnd(STACK[STACK.length - 1].label);
11945
- }
11946
- node = STACK[STACK.length - 1];
11947
- if (node.label !== label) return; // give up rather than throw
11948
- }
11949
- var elapsed = nowMs() - node._t0;
11950
- node._t0 = 0;
11951
- node.totalMs += elapsed;
11952
- node.calls += 1;
11953
- STACK.pop();
11954
- var parent = STACK[STACK.length - 1];
11955
- parent.childMsAccum += elapsed;
11956
- }
11957
-
11958
- // Wrap a function call. Re-throws if the callback throws but still closes the
11959
- // phase so the stack stays consistent.
11960
- function profileWrap(label, fn) {
11961
- if (!ENABLED) return fn();
11962
- profileStart(label);
11963
- try {
11964
- return fn();
11965
- } finally {
11966
- profileEnd(label);
11967
- }
11968
- }
11969
-
11970
- // Build a flat tree report: array of {depth, label, totalMs, selfMs, calls}.
11971
- function profileReport() {
11972
- var rows = [];
11973
- function visit(node, depth) {
11974
- if (depth > 0) {
11975
- rows.push({
11976
- depth: depth - 1,
11977
- label: node.label,
11978
- totalMs: node.totalMs,
11979
- selfMs: Math.max(0, node.totalMs - node.childMsAccum),
11980
- calls: node.calls
11981
- });
11982
- }
11983
- var keys = Object.keys(node.children);
11984
- keys.sort(function(a, b) {
11985
- return node.children[b].totalMs - node.children[a].totalMs;
11986
- });
11987
- for (var i = 0; i < keys.length; i++) {
11988
- visit(node.children[keys[i]], depth + 1);
11989
- }
11990
- }
11991
- visit(ROOT, 0);
11992
- return rows;
11993
- }
11994
-
11995
- function profileWallElapsedMs() {
11996
- return ENABLED && WALL_START ? nowMs() - WALL_START : 0;
11997
- }
11998
-
11999
- // Pretty-print the current report as a column-aligned tree.
12000
- function formatProfileReport(opts) {
12001
- var rows = profileReport();
12002
- if (rows.length === 0) return '(profile is empty)';
12003
- opts = opts || {};
12004
- var indent = ' ';
12005
- var lines = [];
12006
- // header
12007
- lines.push(['phase', 'total ms', 'self ms', 'calls'].join('\t'));
12008
- for (var i = 0; i < rows.length; i++) {
12009
- var r = rows[i];
12010
- var label = '';
12011
- for (var j = 0; j < r.depth; j++) label += indent;
12012
- label += r.label;
12013
- lines.push([
12014
- label,
12015
- r.totalMs.toFixed(2),
12016
- r.selfMs.toFixed(2),
12017
- String(r.calls)
12018
- ].join('\t'));
12019
- }
12020
- if (opts.includeWall) {
12021
- lines.push('');
12022
- lines.push('wall elapsed ms: ' + profileWallElapsedMs().toFixed(2));
12023
- }
12024
- return lines.join('\n');
12025
- }
12026
-
12027
- // Honour an env var so users (and CI harnesses) can opt in without code changes.
12028
- if (typeof process !== 'undefined' && process.env && process.env.MAPSHAPER_PROFILE) {
12029
- enableProfiling();
12030
- }
12031
-
12032
- var Profile = /*#__PURE__*/Object.freeze({
12033
- __proto__: null,
12034
- disableProfiling: disableProfiling,
12035
- enableProfiling: enableProfiling,
12036
- formatProfileReport: formatProfileReport,
12037
- profileEnabled: profileEnabled,
12038
- profileEnd: profileEnd,
12039
- profileReport: profileReport,
12040
- profileReset: profileReset,
12041
- profileStart: profileStart,
12042
- profileWallElapsedMs: profileWallElapsedMs,
12043
- profileWrap: profileWrap
12044
- });
12045
-
12046
12567
  // Dissolve arcs that can be merged without affecting topology of layers
12047
12568
  // remove arcs that are not referenced by any layer; remap arc ids
12048
12569
  // in layers. (dataset.arcs is replaced).
@@ -17759,6 +18280,15 @@
17759
18280
  };
17760
18281
  }
17761
18282
 
18283
+ var ColorUtils = /*#__PURE__*/Object.freeze({
18284
+ __proto__: null,
18285
+ formatColor: formatColor,
18286
+ parseColor: parseColor,
18287
+ parseHexColor: parseHexColor,
18288
+ parseRGBA: parseRGBA,
18289
+ validateColor: validateColor
18290
+ });
18291
+
17762
18292
  function blend(a, b) {
17763
18293
  var colors, weights, args;
17764
18294
  if (Array.isArray(a)) {
@@ -21869,9 +22399,12 @@
21869
22399
  // Assumes that input layers are members of the same dataset (and therefore
21870
22400
  // share the same ArcCollection, if layers have paths).
21871
22401
  cmd.mergeLayers = function(layersArg, opts) {
21872
- var layers = layersArg.filter(getFeatureCount); // ignore empty layers
21873
- var merged = {};
22402
+ var layers, merged = {};
21874
22403
  opts = opts || {};
22404
+ layersArg.forEach(function(lyr) {
22405
+ requireNotRasterLayer(lyr, 'Raster layers cannot be merged');
22406
+ });
22407
+ layers = layersArg.filter(getFeatureCount); // ignore empty layers
21875
22408
  if (!layers.length && layersArg.length > 1) {
21876
22409
  layers = layersArg; // merge all-empty layer sets
21877
22410
  }
@@ -28277,6 +28810,9 @@ ${svg}
28277
28810
  if (raster.grid.samples) {
28278
28811
  copy.grid.samples = typedArrayToBuffer(raster.grid.samples);
28279
28812
  }
28813
+ if (raster.grid.coverage) {
28814
+ copy.grid.coverage = typedArrayToBuffer(raster.grid.coverage);
28815
+ }
28280
28816
  }
28281
28817
  if (raster.view) {
28282
28818
  copy.view = Object.assign({}, raster.view);
@@ -33522,8 +34058,9 @@ ${svg}
33522
34058
  error('precision= option should be a positive number');
33523
34059
  }
33524
34060
 
33525
- if (o.raster_type && o.raster_type != 'image' && o.raster_type != 'categorical') {
33526
- error('Unsupported raster-type:', o.raster_type);
34061
+ if (o.interpretation && o.interpretation != 'image' &&
34062
+ o.interpretation != 'categorical' && o.interpretation != 'continuous') {
34063
+ error('Unsupported raster-type:', o.interpretation);
33527
34064
  }
33528
34065
 
33529
34066
  if (o.encoding) {
@@ -34836,8 +35373,15 @@ ${svg}
34836
35373
  .option('percentile-range', {
34837
35374
  describe: '[raster] input percentile range for percentile scaling, e.g. 2,98'
34838
35375
  })
35376
+ // The user-facing spelling is raster-type=; the parsed option is named
35377
+ // interpretation to keep it distinct from layer.raster_type, which is a
35378
+ // storage tag, not a semantic one.
35379
+ .option('interpretation', {
35380
+ label: 'raster-type=',
35381
+ describe: '[raster] image, categorical or continuous (default is image)'
35382
+ })
34839
35383
  .option('raster-type', {
34840
- describe: '[raster] image or categorical (default is image)'
35384
+ alias_to: 'interpretation'
34841
35385
  })
34842
35386
  .option('rendition', {
34843
35387
  describe: '[GeoTIFF] import a GeoTIFF rendition: full,overview-1,etc.'
@@ -35284,9 +35828,11 @@ ${svg}
35284
35828
  .option('no-replace', noReplaceOpt);
35285
35829
 
35286
35830
  parser.command('blur')
35287
- // .describe('apply a Gaussian-like blur to projected raster layers')
35831
+ .describe('apply a Gaussian-like blur to a projected raster layer')
35288
35832
  .option('radius', {
35289
- describe: '[raster] blur amount in pixels, corresponding to 2 * sigma (e.g. 10 or 10px)'
35833
+ DEFAULT: true,
35834
+ type: 'distance',
35835
+ describe: 'blur amount as pixels or a distance (e.g. 5px, 20m)'
35290
35836
  })
35291
35837
  .option('target', targetOpt);
35292
35838
 
@@ -35507,6 +36053,35 @@ ${svg}
35507
36053
  type: 'number'
35508
36054
  });
35509
36055
 
36056
+ parser.command('contours')
36057
+ .describe('convert a raster layer to contour lines')
36058
+ .option('interval', {
36059
+ describe: 'spacing between contour levels (default is a round interval)',
36060
+ type: 'number'
36061
+ })
36062
+ .option('levels', {
36063
+ describe: 'explicit contour levels, instead of interval= (e.g. 0,100,500)',
36064
+ type: 'numbers'
36065
+ })
36066
+ .option('base', {
36067
+ describe: 'value to align interval= to (default is 0)',
36068
+ type: 'number'
36069
+ })
36070
+ .option('band', {
36071
+ describe: '[raster] band to read values from (default is 0)',
36072
+ type: 'number'
36073
+ })
36074
+ .option('field', {
36075
+ describe: 'name of field to hold contour values (default is "value")'
36076
+ })
36077
+ .option('no-smoothing', {
36078
+ describe: 'skip smoothing away the one-pixel contour staircase',
36079
+ type: 'flag'
36080
+ })
36081
+ .option('name', nameOpt)
36082
+ .option('target', targetOpt)
36083
+ .option('no-replace', noReplaceOpt);
36084
+
35510
36085
  parser.command('dashlines')
35511
36086
  .describe('split lines into sections, with or without a gap')
35512
36087
  .oldAlias('split-lines')
@@ -40908,7 +41483,7 @@ ${svg}
40908
41483
  }
40909
41484
 
40910
41485
  function getRasterInterpretation$1(opts) {
40911
- return opts.raster_type || opts.rasterType || 'image';
41486
+ return opts.interpretation || 'image';
40912
41487
  }
40913
41488
 
40914
41489
  async function selectGeoTIFFImportImage(tiff, sourceImage, opts) {
@@ -41336,7 +41911,7 @@ ${svg}
41336
41911
  }
41337
41912
 
41338
41913
  function getRasterInterpretation(opts) {
41339
- return opts.raster_type || opts.rasterType || 'image';
41914
+ return opts.interpretation || 'image';
41340
41915
  }
41341
41916
 
41342
41917
  async function decodeImage(input, imageType) {
@@ -41966,10 +42541,10 @@ ${svg}
41966
42541
  var coords = path.coords;
41967
42542
  if (coords.length < 2) return;
41968
42543
  if (forcePolylinePaths) {
41969
- lines.push(path.closed || pointsEqual(coords[0], coords[coords.length - 1]) ? closeRing(coords) : coords);
42544
+ lines.push(path.closed || pointsEqual$1(coords[0], coords[coords.length - 1]) ? closeRing(coords) : coords);
41970
42545
  return;
41971
42546
  }
41972
- if (path.closed || pointsEqual(coords[0], coords[coords.length - 1])) {
42547
+ if (path.closed || pointsEqual$1(coords[0], coords[coords.length - 1])) {
41973
42548
  if (coords.length >= 3) {
41974
42549
  rings.push(closeRing(coords));
41975
42550
  }
@@ -42192,7 +42767,7 @@ ${svg}
42192
42767
  for (var i = 0; i < subpaths.length; i++) {
42193
42768
  var coords = subpaths[i].coords;
42194
42769
  if (coords.length < 2) continue;
42195
- if (!subpaths[i].closed && !pointsEqual(coords[0], coords[coords.length - 1])) {
42770
+ if (!subpaths[i].closed && !pointsEqual$1(coords[0], coords[coords.length - 1])) {
42196
42771
  return true;
42197
42772
  }
42198
42773
  }
@@ -42277,7 +42852,7 @@ ${svg}
42277
42852
 
42278
42853
  function closeRing(coords) {
42279
42854
  var ring = coords.map(function(p) { return [p[0], p[1]]; });
42280
- if (!pointsEqual(ring[0], ring[ring.length - 1])) {
42855
+ if (!pointsEqual$1(ring[0], ring[ring.length - 1])) {
42281
42856
  ring.push([ring[0][0], ring[0][1]]);
42282
42857
  }
42283
42858
  return ring;
@@ -42332,7 +42907,7 @@ ${svg}
42332
42907
  return /^[a-z]$/i.test(token);
42333
42908
  }
42334
42909
 
42335
- function pointsEqual(a, b) {
42910
+ function pointsEqual$1(a, b) {
42336
42911
  return a && b && a[0] == b[0] && a[1] == b[1];
42337
42912
  }
42338
42913
 
@@ -42681,6 +43256,9 @@ ${svg}
42681
43256
  if (raster.grid.samples) {
42682
43257
  copy.grid.samples = restoreRasterSamples(raster.grid.samples, raster.grid.pixelType);
42683
43258
  }
43259
+ if (raster.grid.coverage) {
43260
+ copy.grid.coverage = new Uint8Array(BinArray.copyToArrayBuffer(raster.grid.coverage));
43261
+ }
42684
43262
  }
42685
43263
  if (raster.view) {
42686
43264
  copy.view = Object.assign({}, raster.view);
@@ -55898,6 +56476,9 @@ ${svg}
55898
56476
 
55899
56477
  function isInterruptedProjection(P) {
55900
56478
  if (P && P.__projection_topology) {
56479
+ if (P.__projection_topology.interrupted !== undefined) {
56480
+ return P.__projection_topology.interrupted;
56481
+ }
55901
56482
  return P.__projection_topology.seams.some(function(o) {
55902
56483
  return o.type == 'cut';
55903
56484
  });
@@ -56756,6 +57337,16 @@ ${svg}
56756
57337
  derivation.type == 'categorical';
56757
57338
  }
56758
57339
 
57340
+ // Whether uncovered output pixels should get the source's numeric nodata
57341
+ // value instead of a fill color. This is a separate question from the
57342
+ // resampling method: elevation data (raster-type=continuous) wants numeric
57343
+ // nodata AND bilinear resampling, a combination that a single
57344
+ // image/categorical switch could not express.
57345
+ function rasterUsesNumericNodataFill(raster) {
57346
+ return !!raster && raster.interpretation == 'continuous' ||
57347
+ rasterAppearsCategorical(raster);
57348
+ }
57349
+
56759
57350
  function getProjectedRasterGridBBox(raster, srcCRS, destCRS, optsArg) {
56760
57351
  var opts = optsArg || {};
56761
57352
  var grid = getRasterGrid(raster);
@@ -57434,9 +58025,8 @@ ${svg}
57434
58025
  }
57435
58026
 
57436
58027
  function validateRasterGridForProjection(grid) {
57437
- var t = grid && grid.transform;
57438
58028
  if (!grid || !grid.samples || !grid.bbox) stop$1('Raster layer is missing required projection data');
57439
- if (t && (t[1] !== 0 || t[3] !== 0)) {
58029
+ if (rasterGridIsRotated(grid)) {
57440
58030
  stop$1('Raster reprojection does not support rotated or skewed rasters');
57441
58031
  }
57442
58032
  }
@@ -57568,18 +58158,35 @@ ${svg}
57568
58158
  var color = getNoDataColor(raster, opts);
57569
58159
  var noData = grid.nodata;
57570
58160
  if (color) {
58161
+ warnIfPaintingOverContinuousData(samples, grid, raster, opts);
57571
58162
  fillProjectedRasterColor(samples, bands, color);
57572
58163
  return;
57573
58164
  }
57574
- if (noData === null || noData === undefined || !isFinite(noData)) return;
58165
+ if (noData === null || noData === undefined || !isFinite(noData)) {
58166
+ if (raster && raster.interpretation == 'continuous') {
58167
+ warnOnce('This raster has no nodata value, so uncovered pixels will be 0');
58168
+ }
58169
+ return;
58170
+ }
57575
58171
  samples.fill(noData);
57576
58172
  }
57577
58173
 
58174
+ // Painting a color into a float grid means writing a color value into what
58175
+ // are probably measurements. Flag it, since raster-type=continuous is
58176
+ // usually what the user wanted.
58177
+ function warnIfPaintingOverContinuousData(samples, grid, raster, opts) {
58178
+ if (opts.nodata_color || opts.nodataColor) return; // explicit user choice
58179
+ if (!rasterSamplesAreFloat(samples)) return;
58180
+ if (grid.nodata === null || grid.nodata === undefined) return;
58181
+ warnOnce('Filling uncovered pixels of a floating-point raster with a color. ' +
58182
+ 'Use -i raster-type=continuous to preserve its nodata value instead.');
58183
+ }
58184
+
57578
58185
  function getNoDataColor(raster, opts) {
57579
58186
  var arg = opts.nodata_color || opts.nodataColor;
57580
58187
  var color;
57581
58188
  if (arg == null || arg === '') {
57582
- return rasterAppearsCategorical(raster) ? null : {r: 255, g: 255, b: 255, a: 1};
58189
+ return rasterUsesNumericNodataFill(raster) ? null : {r: 255, g: 255, b: 255, a: 1};
57583
58190
  }
57584
58191
  if (String(arg).toLowerCase() == 'transparent') {
57585
58192
  return {r: 0, g: 0, b: 0, a: 0};
@@ -57604,7 +58211,8 @@ ${svg}
57604
58211
  }
57605
58212
  }
57606
58213
 
57607
- function rasterizeProjectedMesh(srcGrid, destGrid, mesh, sampleMethod) {
58214
+ function rasterizeProjectedMesh(srcGrid, destGrid, mesh, sampleMethodArg) {
58215
+ var sampleMethod = getRasterSamplerContext(srcGrid, destGrid, sampleMethodArg);
57608
58216
  var regionData = mesh.projectedRegions ?
57609
58217
  createProjectedRegionMask(
57610
58218
  destGrid, mesh.projectedRegions, mesh.fillRegionMask) :
@@ -57801,9 +58409,24 @@ ${svg}
57801
58409
  ];
57802
58410
  }
57803
58411
 
57804
- function copyRasterSample(srcGrid, destGrid, sx, sy, dx, dy, sampleMethod, wrapX) {
57805
- if (sampleMethod == 'bilinear') {
57806
- copyBilinearRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX);
58412
+ // Per-projection sampling context, built once so the inner rasterizing loop
58413
+ // does not repeat type or metadata tests for every output pixel.
58414
+ function getRasterSamplerContext(srcGrid, destGrid, method) {
58415
+ return {
58416
+ bilinear: method == 'bilinear',
58417
+ // Integer sample arrays truncate on assignment, so interpolated values
58418
+ // have to be rounded explicitly. Float arrays must NOT be rounded, or
58419
+ // elevation data loses its sub-unit precision.
58420
+ round: !rasterSamplesAreFloat(destGrid.samples),
58421
+ // Resolved once for the whole source grid; null means every pixel is
58422
+ // valid and the sampler can skip validity testing altogether.
58423
+ validity: getRasterValidityMask(srcGrid)
58424
+ };
58425
+ }
58426
+
58427
+ function copyRasterSample(srcGrid, destGrid, sx, sy, dx, dy, sampler, wrapX) {
58428
+ if (sampler.bilinear) {
58429
+ copyBilinearRasterSample(srcGrid, destGrid, sx, sy, dx, dy, sampler, wrapX);
57807
58430
  } else {
57808
58431
  copyNearestRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX);
57809
58432
  }
@@ -57816,7 +58439,9 @@ ${svg}
57816
58439
  var srcY = clamp$1(Math.floor(sy), 0, srcGrid.height - 1);
57817
58440
  var src = (srcY * srcGrid.width + srcX) * srcGrid.bands;
57818
58441
  var dest = (dy * destGrid.width + dx) * destGrid.bands;
57819
- if (!rasterSourcePixelIsCovered(srcGrid, srcX, srcY)) return;
58442
+ // Nearest sampling copies a nodata sample through unchanged, rather than
58443
+ // skipping it, so nodata areas survive a round trip.
58444
+ if (!rasterPixelIsCovered(srcGrid, srcY * srcGrid.width + srcX)) return;
57820
58445
  for (var band = 0; band < srcGrid.bands; band++) {
57821
58446
  destGrid.samples[dest + band] = srcGrid.samples[src + band];
57822
58447
  }
@@ -57824,7 +58449,7 @@ ${svg}
57824
58449
  if (destGrid.coverage) destGrid.coverage[dy * destGrid.width + dx] = 1;
57825
58450
  }
57826
58451
 
57827
- function copyBilinearRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX) {
58452
+ function copyBilinearRasterSample(srcGrid, destGrid, sx, sy, dx, dy, sampler, wrapX) {
57828
58453
  if (wrapX) sx = modulo(sx, srcGrid.width);
57829
58454
  var srcX = wrapX ?
57830
58455
  modulo(Math.floor(sx - 0.5), srcGrid.width) :
@@ -57840,79 +58465,81 @@ ${svg}
57840
58465
  var src01 = (srcY2 * srcGrid.width + srcX) * srcGrid.bands;
57841
58466
  var src11 = (srcY2 * srcGrid.width + srcX2) * srcGrid.bands;
57842
58467
  var dest = (dy * destGrid.width + dx) * destGrid.bands;
57843
- if (!srcGrid.coverage) {
57844
- copyBilinearRasterSampleFast(srcGrid, destGrid, src00, src10, src01, src11, dest, tx, ty);
58468
+ if (!sampler.validity) {
58469
+ copyBilinearRasterSampleFast(srcGrid, destGrid, src00, src10, src01, src11, dest, tx, ty, sampler.round);
57845
58470
  if (destGrid.coverage) destGrid.coverage[dy * destGrid.width + dx] = 1;
57846
58471
  return;
57847
58472
  }
57848
- if (copyBilinearRasterSampleWithCoverage(srcGrid, destGrid, srcX, srcY, srcX2, srcY2, src00, src10, src01, src11, dest, tx, ty)) {
58473
+ if (copyBilinearRasterSampleChecked(srcGrid, destGrid, sampler.validity, srcX, srcY, srcX2, srcY2, src00, src10, src01, src11, dest, tx, ty, sampler.round)) {
57849
58474
  if (destGrid.coverage) destGrid.coverage[dy * destGrid.width + dx] = 1;
57850
58475
  }
57851
58476
  }
57852
58477
 
57853
- function copyBilinearRasterSampleFast(srcGrid, destGrid, src00, src10, src01, src11, dest, tx, ty) {
58478
+ function copyBilinearRasterSampleFast(srcGrid, destGrid, src00, src10, src01, src11, dest, tx, ty, round) {
57854
58479
  if (srcGrid.bands == 3) {
57855
- copyBilinearRgbSample(srcGrid.samples, destGrid.samples, src00, src10, src01, src11, dest, tx, ty);
58480
+ copyBilinearRgbSample(srcGrid.samples, destGrid.samples, src00, src10, src01, src11, dest, tx, ty, round);
57856
58481
  } else if (srcGrid.bands == 4) {
57857
- copyBilinearRgbaSample(srcGrid.samples, destGrid.samples, src00, src10, src01, src11, dest, tx, ty);
58482
+ copyBilinearRgbaSample(srcGrid.samples, destGrid.samples, src00, src10, src01, src11, dest, tx, ty, round);
57858
58483
  } else {
57859
- copyBilinearGenericSample(srcGrid.samples, destGrid.samples, srcGrid.bands, src00, src10, src01, src11, dest, tx, ty);
58484
+ copyBilinearGenericSample(srcGrid.samples, destGrid.samples, srcGrid.bands, src00, src10, src01, src11, dest, tx, ty, round);
57860
58485
  }
57861
58486
  if (destGrid.bands > srcGrid.bands) destGrid.samples[dest + 3] = 255;
57862
58487
  }
57863
58488
 
57864
- function copyBilinearRgbSample(src, destArr, src00, src10, src01, src11, dest, tx, ty) {
57865
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 0, tx, ty);
57866
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 1, tx, ty);
57867
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 2, tx, ty);
58489
+ function copyBilinearRgbSample(src, destArr, src00, src10, src01, src11, dest, tx, ty, round) {
58490
+ copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 0, tx, ty, round);
58491
+ copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 1, tx, ty, round);
58492
+ copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 2, tx, ty, round);
57868
58493
  }
57869
58494
 
57870
- function copyBilinearRgbaSample(src, destArr, src00, src10, src01, src11, dest, tx, ty) {
57871
- copyBilinearRgbSample(src, destArr, src00, src10, src01, src11, dest, tx, ty);
57872
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 3, tx, ty);
58495
+ function copyBilinearRgbaSample(src, destArr, src00, src10, src01, src11, dest, tx, ty, round) {
58496
+ copyBilinearRgbSample(src, destArr, src00, src10, src01, src11, dest, tx, ty, round);
58497
+ copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 3, tx, ty, round);
57873
58498
  }
57874
58499
 
57875
- function copyBilinearGenericSample(src, destArr, bands, src00, src10, src01, src11, dest, tx, ty) {
58500
+ function copyBilinearGenericSample(src, destArr, bands, src00, src10, src01, src11, dest, tx, ty, round) {
57876
58501
  for (var band = 0; band < bands; band++) {
57877
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, band, tx, ty);
58502
+ copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, band, tx, ty, round);
57878
58503
  }
57879
58504
  }
57880
58505
 
57881
- function copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, band, tx, ty) {
58506
+ function copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, band, tx, ty, round) {
57882
58507
  var a = src[src00 + band] * (1 - tx) + src[src10 + band] * tx;
57883
58508
  var b = src[src01 + band] * (1 - tx) + src[src11 + band] * tx;
57884
- destArr[dest + band] = Math.round(a * (1 - ty) + b * ty);
57885
- }
57886
-
57887
- function copyBilinearRasterSampleWithCoverage(srcGrid, destGrid, srcX, srcY, srcX2, srcY2, src00, src10, src01, src11, dest, tx, ty) {
57888
- var coords = [
57889
- [srcX, srcY, src00, (1 - tx) * (1 - ty)],
57890
- [srcX2, srcY, src10, tx * (1 - ty)],
57891
- [srcX, srcY2, src01, (1 - tx) * ty],
57892
- [srcX2, srcY2, src11, tx * ty]
57893
- ];
57894
- var total = 0;
57895
- var val, item;
58509
+ var val = a * (1 - ty) + b * ty;
58510
+ destArr[dest + band] = round ? Math.round(val) : val;
58511
+ }
58512
+
58513
+ // Bilinear sampling that skips invalid source pixels and renormalizes the
58514
+ // remaining weights, so nodata is never blended into a real value. Validity
58515
+ // is resolved once per output pixel rather than once per band.
58516
+ function copyBilinearRasterSampleChecked(srcGrid, destGrid, validity, srcX, srcY, srcX2, srcY2, src00, src10, src01, src11, dest, tx, ty, round) {
58517
+ var w00 = (1 - tx) * (1 - ty);
58518
+ var w10 = tx * (1 - ty);
58519
+ var w01 = (1 - tx) * ty;
58520
+ var w11 = tx * ty;
58521
+ var width = srcGrid.width;
58522
+ var samples = srcGrid.samples;
58523
+ var ok00 = w00 > 0 && validity[srcY * width + srcX] > 0;
58524
+ var ok10 = w10 > 0 && validity[srcY * width + srcX2] > 0;
58525
+ var ok01 = w01 > 0 && validity[srcY2 * width + srcX] > 0;
58526
+ var ok11 = w11 > 0 && validity[srcY2 * width + srcX2] > 0;
58527
+ var total = (ok00 ? w00 : 0) + (ok10 ? w10 : 0) + (ok01 ? w01 : 0) + (ok11 ? w11 : 0);
58528
+ var val;
58529
+ if (total <= 0) return false;
57896
58530
  for (var band = 0; band < srcGrid.bands; band++) {
57897
58531
  val = 0;
57898
- total = 0;
57899
- for (var i = 0; i < coords.length; i++) {
57900
- item = coords[i];
57901
- if (item[3] <= 0 || !rasterSourcePixelIsCovered(srcGrid, item[0], item[1])) continue;
57902
- val += srcGrid.samples[item[2] + band] * item[3];
57903
- total += item[3];
57904
- }
57905
- if (total <= 0) return false;
57906
- destGrid.samples[dest + band] = Math.round(val / total);
58532
+ if (ok00) val += samples[src00 + band] * w00;
58533
+ if (ok10) val += samples[src10 + band] * w10;
58534
+ if (ok01) val += samples[src01 + band] * w01;
58535
+ if (ok11) val += samples[src11 + band] * w11;
58536
+ val /= total;
58537
+ destGrid.samples[dest + band] = round ? Math.round(val) : val;
57907
58538
  }
57908
58539
  if (destGrid.bands > srcGrid.bands) destGrid.samples[dest + 3] = 255;
57909
58540
  return true;
57910
58541
  }
57911
58542
 
57912
- function rasterSourcePixelIsCovered(grid, x, y) {
57913
- return !grid.coverage || grid.coverage[y * grid.width + x] > 0;
57914
- }
57915
-
57916
58543
  function timeStart(timing, name) {
57917
58544
  if (!timing) return;
57918
58545
  timing[name + 'Start'] = getTimer();
@@ -57956,6 +58583,147 @@ ${svg}
57956
58583
  projectRasterGridForward: projectRasterGridForward
57957
58584
  });
57958
58585
 
58586
+ /*
58587
+ * Post-projection repair for projections whose layout wraps at a rectangular
58588
+ * frame (currently the Lee tetrahedral pair, the only ones that publish
58589
+ * frame_bounds).
58590
+ *
58591
+ * Why this is needed: such a projection is cut along a curved seam traced in
58592
+ * geographic space, and the seam mask splits any path that crosses it. But
58593
+ * without -densify, a path segment is a straight chord in lon/lat, and a chord
58594
+ * between two vertices either side of the seam can pass on the wrong side of
58595
+ * the curve entirely. The projected path then leaves one edge of the frame and
58596
+ * re-enters at the opposite edge, so the ring closes with a long run along a
58597
+ * frame side, joining two halves that belong on opposite sides of the map.
58598
+ * Densified input curves with the seam and is cut normally, which is why the
58599
+ * artifact only appears without that option.
58600
+ *
58601
+ * The repair looks for a ring that meets the same frame side in two or more
58602
+ * separate runs and splits it there. A ring that legitimately touches a frame
58603
+ * side does so once.
58604
+ *
58605
+ * Note that the jump threshold in getFrameSide is coupled to the resolution of
58606
+ * the seam trace: it recognizes a spurious connection as a single long segment
58607
+ * lying on a frame side. Coarsening the trace breaks the connection into a
58608
+ * chain of shorter segments that fall below the threshold and are missed, so
58609
+ * the two need to be changed together.
58610
+ */
58611
+
58612
+
58613
+ function splitPolygonFrameChords(dataset, bounds) {
58614
+ var editor = new DatasetEditor(dataset);
58615
+ dataset.layers.forEach(function(lyr) {
58616
+ if (lyr.geometry_type == 'polygon') {
58617
+ editor.editLayer(lyr, function(path) {
58618
+ return splitRingAtFrameChords(path, bounds);
58619
+ });
58620
+ } else {
58621
+ editor.editLayer(lyr, function(geometry) {
58622
+ return lyr.geometry_type == 'point' ? geometry : [geometry];
58623
+ });
58624
+ }
58625
+ });
58626
+ editor.done();
58627
+ }
58628
+
58629
+ function splitRingAtFrameChords(path, bounds, depth) {
58630
+ depth = depth || 0;
58631
+ var closed = pointsEqual(path[0], path[path.length - 1]);
58632
+ var n = path.length - (closed ? 1 : 0);
58633
+ if (n < 4) return [path];
58634
+ var width = bounds[2] - bounds[0];
58635
+ var height = bounds[3] - bounds[1];
58636
+ var tolerance = Math.max(width, height) * 1e-3;
58637
+ var edgesBySide = [[], [], [], []];
58638
+ for (var i = 0; i < n; i++) {
58639
+ var side = getFrameSide(
58640
+ path[i], path[(i + 1) % n], bounds, tolerance);
58641
+ if (side >= 0) edgesBySide[side].push({index: i, side: side});
58642
+ }
58643
+ for (var frameSide = 0; frameSide < edgesBySide.length; frameSide++) {
58644
+ var runs = groupEdgeRuns(edgesBySide[frameSide], n);
58645
+ if (runs.length > 1) {
58646
+ var parts = splitRingAtRuns(path, n, runs, frameSide, bounds);
58647
+ if (depth >= 3) return parts;
58648
+ return parts.reduce(function(memo, part) {
58649
+ return memo.concat(splitRingAtFrameChords(part, bounds, depth + 1));
58650
+ }, []);
58651
+ }
58652
+ }
58653
+ return [path];
58654
+ }
58655
+
58656
+ function splitRingAtRuns(path, n, runs, side, bounds) {
58657
+ return runs.map(function(run, i) {
58658
+ var next = runs[(i + 1) % runs.length];
58659
+ var start = (run.end + 1) % n;
58660
+ var end = next.start;
58661
+ var part = [];
58662
+ var j = start;
58663
+ while (true) {
58664
+ part.push(path[j].concat());
58665
+ if (j == end) break;
58666
+ j = (j + 1) % n;
58667
+ }
58668
+ if (side == 0 || side == 2) {
58669
+ var y = side == 0 ? bounds[1] : bounds[3];
58670
+ part[0][1] = y;
58671
+ part[part.length - 1][1] = y;
58672
+ } else {
58673
+ var x = side == 1 ? bounds[2] : bounds[0];
58674
+ part[0][0] = x;
58675
+ part[part.length - 1][0] = x;
58676
+ }
58677
+ part.push(part[0].concat());
58678
+ return part;
58679
+ }).filter(function(part) {
58680
+ return part.length > 3;
58681
+ });
58682
+ }
58683
+
58684
+ function getFrameSide(a, b, bounds, tolerance) {
58685
+ var dx = Math.abs(a[0] - b[0]);
58686
+ var dy = Math.abs(a[1] - b[1]);
58687
+ var width = bounds[2] - bounds[0];
58688
+ var height = bounds[3] - bounds[1];
58689
+ if (dx > width * 0.2) {
58690
+ if (Math.abs(a[1] - bounds[1]) < tolerance &&
58691
+ Math.abs(b[1] - bounds[1]) < tolerance) return 0;
58692
+ if (Math.abs(a[1] - bounds[3]) < tolerance &&
58693
+ Math.abs(b[1] - bounds[3]) < tolerance) return 2;
58694
+ }
58695
+ if (dy > height * 0.2) {
58696
+ if (Math.abs(a[0] - bounds[2]) < tolerance &&
58697
+ Math.abs(b[0] - bounds[2]) < tolerance) return 1;
58698
+ if (Math.abs(a[0] - bounds[0]) < tolerance &&
58699
+ Math.abs(b[0] - bounds[0]) < tolerance) return 3;
58700
+ }
58701
+ return -1;
58702
+ }
58703
+
58704
+ function groupEdgeRuns(edges, n) {
58705
+ var runs = [];
58706
+ edges.forEach(function(edge) {
58707
+ var run = runs[runs.length - 1];
58708
+ if (run && edge.side == run.side && edge.index == run.end + 1) {
58709
+ run.end = edge.index;
58710
+ } else {
58711
+ runs.push({start: edge.index, end: edge.index, side: edge.side});
58712
+ }
58713
+ });
58714
+ if (runs.length > 1 && runs[0].start == 0 &&
58715
+ runs[runs.length - 1].end == n - 1 &&
58716
+ runs[0].side == runs[runs.length - 1].side) {
58717
+ runs[0].start = runs[runs.length - 1].start;
58718
+ runs.pop();
58719
+ }
58720
+ return runs;
58721
+ }
58722
+
58723
+ function pointsEqual(a, b) {
58724
+ return a && b && a[0] == b[0] && a[1] == b[1];
58725
+ }
58726
+
57959
58727
  cmd.proj = function(dataset, catalog, opts, targetLayers) {
57960
58728
  var srcInfo, destInfo, destStr;
57961
58729
  var implicitlyProjectedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasGeometry);
@@ -58123,6 +58891,12 @@ ${svg}
58123
58891
  }
58124
58892
  }
58125
58893
 
58894
+ var frameBounds = dest.__projection_topology &&
58895
+ dest.__projection_topology.frame_bounds;
58896
+ if (clipped && dataset.arcs && frameBounds) {
58897
+ splitPolygonFrameChords(dataset, scaleProjectionFrameBounds(frameBounds, dest));
58898
+ }
58899
+
58126
58900
  if (clipped) {
58127
58901
  // TODO: could more selective in cleaning clipped layers
58128
58902
  // (probably only needed when clipped area crosses the antimeridian or includes a pole)
@@ -58138,6 +58912,18 @@ ${svg}
58138
58912
  dataset.info.crs = dest;
58139
58913
  }
58140
58914
 
58915
+ function scaleProjectionFrameBounds(bounds, crs) {
58916
+ var scale = crs.a * crs.fr_meter;
58917
+ var dx = crs.x0 * crs.fr_meter;
58918
+ var dy = crs.y0 * crs.fr_meter;
58919
+ return [
58920
+ bounds[0] * scale + dx,
58921
+ bounds[1] * scale + dy,
58922
+ bounds[2] * scale + dx,
58923
+ bounds[3] * scale + dy
58924
+ ];
58925
+ }
58926
+
58141
58927
  function projectRasterLayer(lyr, src, dest, opts) {
58142
58928
  var raster = lyr.raster;
58143
58929
  raster.grid = projectRasterGridForward(raster, src, dest, opts || {});
@@ -61579,6 +62365,529 @@ ${svg}
61579
62365
  // message('[comment]', opts.message);
61580
62366
  }; // no-op, so -comment doesn't trigger a parsing error
61581
62367
 
62368
+ // Trace contour lines (isolines) from a raster layer's working samples using
62369
+ // marching squares.
62370
+ //
62371
+ // Samples are treated as point measurements at pixel centers, so a W x H grid
62372
+ // contours over a lattice of W x H values and (W-1) x (H-1) cells. Contour
62373
+ // vertices therefore span from the first pixel center to the last, half a
62374
+ // pixel inside the grid bbox. This matches gdal_contour.
62375
+
62376
+
62377
+ var MAX_CONTOUR_LEVELS = 2000;
62378
+ var TARGET_LEVEL_COUNT = 15;
62379
+
62380
+ // Marching squares traces a staircase with treads one pixel wide, so smoothing
62381
+ // at one pixel is what removes the stepping. Measured against an analytic cone
62382
+ // quantized to whole meters (as elevation models usually are), one pixel gives
62383
+ // the lowest deviation from the true contour: 0.16 px, against 0.29 px
62384
+ // unsmoothed and 0.18 px at 1.5 px. On unquantized float data, where there is
62385
+ // no staircase to remove, the cost is under 0.1 px.
62386
+ var SMOOTHING_PIXELS = 1;
62387
+
62388
+ // A degree of longitude vanishes at the poles; keep the implied pixel width
62389
+ // from collapsing to zero there.
62390
+ var MIN_LATITUDE_SCALE = 0.05;
62391
+
62392
+ // Cell corners, and the edges between them:
62393
+ //
62394
+ // edge 0
62395
+ // c0 --------- c1
62396
+ // | |
62397
+ // | edge 3 | edge 1
62398
+ // | |
62399
+ // c3 --------- c2
62400
+ // edge 2
62401
+ //
62402
+ // Corner bits: 1 = c0, 2 = c1, 4 = c2, 8 = c3. A corner is set when its value
62403
+ // is >= the contour level.
62404
+ //
62405
+ // Each entry is the pair of edges a contour segment crosses, directed so that
62406
+ // the above-level side is always on the same hand. Consistent orientation is
62407
+ // what lets segments be stitched together by edge id alone: a crossing is the
62408
+ // exit of one cell and the entry of its neighbor.
62409
+ var CONTOUR_CASES = [
62410
+ null, // 0: none above
62411
+ [3, 0], // 1: c0
62412
+ [0, 1], // 2: c1
62413
+ [3, 1], // 3: c0 c1
62414
+ [1, 2], // 4: c2
62415
+ null, // 5: c0 c2 -- saddle
62416
+ [0, 2], // 6: c1 c2
62417
+ [3, 2], // 7: c0 c1 c2
62418
+ [2, 3], // 8: c3
62419
+ [2, 0], // 9: c0 c3
62420
+ null, // 10: c1 c3 -- saddle
62421
+ [2, 1], // 11: c0 c1 c3
62422
+ [1, 3], // 12: c2 c3
62423
+ [1, 0], // 13: c0 c2 c3
62424
+ [0, 3], // 14: c1 c2 c3
62425
+ null // 15: all above
62426
+ ];
62427
+
62428
+ // A saddle cell has crossings on all four edges, which can be paired two ways.
62429
+ // The cell-center average breaks the tie: when the center is above the level,
62430
+ // the two above-level corners are joined through the middle of the cell and
62431
+ // the below-level corners are cut off separately.
62432
+ var SADDLE_C0_C2_JOINED = [[1, 0], [3, 2]];
62433
+ var SADDLE_C0_C2_SPLIT = [[3, 0], [1, 2]];
62434
+ var SADDLE_C1_C3_JOINED = [[0, 3], [2, 1]];
62435
+ var SADDLE_C1_C3_SPLIT = [[0, 1], [2, 3]];
62436
+
62437
+ function getRasterContourLines(raster, opts) {
62438
+ var grid = getRasterGrid(raster);
62439
+ var band, levels;
62440
+ validateRasterGridForContours(grid);
62441
+ band = getContourBand(grid, opts);
62442
+ levels = getContourLevels(grid, band, opts);
62443
+ return {
62444
+ levels: levels,
62445
+ lines: traceRasterContours(grid, band, levels)
62446
+ };
62447
+ }
62448
+
62449
+ // Auto-selected smoothing interval for traced contours, returned the way
62450
+ // -smooth reads a plain distance= number: as meters when the CRS is known, and
62451
+ // as raw coordinate units when it is not.
62452
+ function getContourSmoothingDistance(grid, crs) {
62453
+ var bbox = grid.bbox;
62454
+ var dx = Math.abs(bbox[2] - bbox[0]) / grid.width;
62455
+ var dy = Math.abs(bbox[3] - bbox[1]) / grid.height;
62456
+ var lat;
62457
+ if (crs && crs.is_latlong) {
62458
+ lat = (bbox[1] + bbox[3]) / 2;
62459
+ dx = degreesToMeters(dx) * Math.max(Math.cos(lat * D2R$8), MIN_LATITUDE_SCALE);
62460
+ dy = degreesToMeters(dy);
62461
+ } else if (crs && crs.to_meter > 0) {
62462
+ dx *= crs.to_meter;
62463
+ dy *= crs.to_meter;
62464
+ }
62465
+ // Geometric mean, so a grid with non-square pixels gets an interval between
62466
+ // its two resolutions rather than one axis winning.
62467
+ return Math.sqrt(dx * dy) * SMOOTHING_PIXELS;
62468
+ }
62469
+
62470
+ function validateRasterGridForContours(grid) {
62471
+ if (!grid || !grid.samples || !grid.bbox) {
62472
+ stop$1('Raster layer is missing required grid data');
62473
+ }
62474
+ if (rasterGridIsRotated(grid)) {
62475
+ stop$1('Tracing contours from a rotated or skewed raster is not supported');
62476
+ }
62477
+ if (grid.width < 2 || grid.height < 2) {
62478
+ stop$1('Raster layer is too small to contour (needs to be at least 2x2 pixels)');
62479
+ }
62480
+ }
62481
+
62482
+ function getContourBand(grid, opts) {
62483
+ var band = opts && 'band' in opts ? opts.band : 0;
62484
+ if (!(band >= 0) || band !== Math.floor(band) || band >= grid.bands) {
62485
+ stop$1('Invalid band=' + band + '; this raster has ' + grid.bands +
62486
+ (grid.bands == 1 ? ' band' : ' bands'));
62487
+ }
62488
+ return band;
62489
+ }
62490
+
62491
+ function getContourLevels(grid, band, opts) {
62492
+ var levels = opts && opts.levels ?
62493
+ getExplicitLevels(opts.levels) :
62494
+ getIntervalLevels(getRasterSampleRange(grid, band), opts || {});
62495
+ if (levels.length > MAX_CONTOUR_LEVELS) {
62496
+ stop$1('Contouring would generate ' + levels.length + ' levels; the limit is ' +
62497
+ MAX_CONTOUR_LEVELS + '. Use a larger interval= or an explicit levels= list.');
62498
+ }
62499
+ return levels;
62500
+ }
62501
+
62502
+ function getExplicitLevels(arg) {
62503
+ var levels = [];
62504
+ arg.forEach(function(val) {
62505
+ if (typeof val != 'number' || !isFinite(val)) {
62506
+ stop$1('Invalid contour level:', val);
62507
+ }
62508
+ if (levels.indexOf(val) == -1) levels.push(val);
62509
+ });
62510
+ return levels.sort(ascending);
62511
+ }
62512
+
62513
+ function getIntervalLevels(range, opts) {
62514
+ var base = 'base' in opts ? opts.base : 0;
62515
+ var levels = [];
62516
+ var interval, k, level;
62517
+ // Only fall back to an automatic interval when none was given: a bad
62518
+ // interval= should be reported, not quietly replaced.
62519
+ if (opts.interval === null || opts.interval === undefined) {
62520
+ interval = getAutoContourInterval(range);
62521
+ } else {
62522
+ interval = opts.interval;
62523
+ if (!(interval > 0) || !isFinite(interval)) {
62524
+ stop$1('Contour interval= must be a positive number');
62525
+ }
62526
+ }
62527
+ if (!isFinite(base)) {
62528
+ stop$1('Contour base= must be a number');
62529
+ }
62530
+ // A level at exactly the data minimum has no cell to cross, because every
62531
+ // corner counts as above it.
62532
+ if (!(range.max > range.min)) return levels;
62533
+ k = Math.ceil((range.min - base) / interval);
62534
+ while (levels.length <= MAX_CONTOUR_LEVELS) {
62535
+ level = cleanLevelValue(base + k * interval);
62536
+ if (level > range.max) break;
62537
+ if (level > range.min) levels.push(level);
62538
+ k++;
62539
+ }
62540
+ return levels;
62541
+ }
62542
+
62543
+ // Choose a round interval that divides the data range into roughly
62544
+ // TARGET_LEVEL_COUNT steps.
62545
+ function getAutoContourInterval(range) {
62546
+ var span = range.max - range.min;
62547
+ var raw, magnitude, normalized;
62548
+ if (!(span > 0) || !isFinite(span)) return 1;
62549
+ raw = span / TARGET_LEVEL_COUNT;
62550
+ magnitude = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10));
62551
+ normalized = raw / magnitude;
62552
+ return (normalized >= 5 ? 5 : normalized >= 2 ? 2 : 1) * magnitude;
62553
+ }
62554
+
62555
+ // Repeated addition of a fractional interval accumulates error that shows up
62556
+ // in output field values, e.g. 0.30000000000000004.
62557
+ function cleanLevelValue(value) {
62558
+ return value === 0 ? 0 : Number(value.toPrecision(12));
62559
+ }
62560
+
62561
+ function getRasterSampleRange(grid, band) {
62562
+ var samples = grid.samples;
62563
+ var bands = grid.bands;
62564
+ var valid = getRasterValidityMask(grid);
62565
+ var n = grid.width * grid.height;
62566
+ var min = Infinity;
62567
+ var max = -Infinity;
62568
+ var v, i;
62569
+ for (i = 0; i < n; i++) {
62570
+ if (valid && !valid[i]) continue;
62571
+ v = samples[i * bands + band];
62572
+ if (v !== v) continue; // NaN, used as a nodata value by some float rasters
62573
+ if (v < min) min = v;
62574
+ if (v > max) max = v;
62575
+ }
62576
+ return {min: min, max: max};
62577
+ }
62578
+
62579
+ function traceRasterContours(grid, band, levels) {
62580
+ var segments = collectContourSegments(grid, band, levels);
62581
+ var toMapXY = getLatticeToMapTransform(grid);
62582
+ var hCount = (grid.width - 1) * grid.height;
62583
+ var lines = [];
62584
+ segments.forEach(function(levelSegments, i) {
62585
+ stitchContourSegments(levelSegments).forEach(function(path) {
62586
+ var coords = getPathCoords(path, grid, band, levels[i], hCount, toMapXY);
62587
+ if (coords.length > 1) {
62588
+ lines.push({value: levels[i], coords: coords});
62589
+ }
62590
+ });
62591
+ });
62592
+ return lines;
62593
+ }
62594
+
62595
+ // Visit each cell once and march it for every level that falls inside its
62596
+ // value range, rather than scanning the whole grid once per level.
62597
+ function collectContourSegments(grid, band, levels) {
62598
+ var W = grid.width;
62599
+ var H = grid.height;
62600
+ var bands = grid.bands;
62601
+ var samples = grid.samples;
62602
+ var hCount = (W - 1) * H;
62603
+ var valid = getRasterValidityMask(grid);
62604
+ var rowStride = W * bands;
62605
+ var out = levels.map(function() { return {from: [], to: []}; });
62606
+ var cx, cy, pixelId, off, v0, v1, v2, v3, min, max, i;
62607
+ if (levels.length === 0) return out;
62608
+ for (cy = 0; cy < H - 1; cy++) {
62609
+ for (cx = 0; cx < W - 1; cx++) {
62610
+ pixelId = cy * W + cx;
62611
+ if (valid && (!valid[pixelId] || !valid[pixelId + 1] ||
62612
+ !valid[pixelId + W] || !valid[pixelId + W + 1])) continue;
62613
+ off = pixelId * bands + band;
62614
+ v0 = samples[off];
62615
+ v1 = samples[off + bands];
62616
+ v3 = samples[off + rowStride];
62617
+ v2 = samples[off + rowStride + bands];
62618
+ if (v0 !== v0 || v1 !== v1 || v2 !== v2 || v3 !== v3) continue; // NaN
62619
+ min = v0 < v1 ? v0 : v1;
62620
+ if (v2 < min) min = v2;
62621
+ if (v3 < min) min = v3;
62622
+ max = v0 > v1 ? v0 : v1;
62623
+ if (v2 > max) max = v2;
62624
+ if (v3 > max) max = v3;
62625
+ // A level crosses the cell when min < level <= max, which matches the
62626
+ // >= test that classifies each corner.
62627
+ for (i = firstLevelAbove(levels, min); i < levels.length && levels[i] <= max; i++) {
62628
+ addCellSegments(out[i], levels[i], v0, v1, v2, v3, cx, cy, W, hCount);
62629
+ }
62630
+ }
62631
+ }
62632
+ return out;
62633
+ }
62634
+
62635
+ function firstLevelAbove(levels, value) {
62636
+ var lo = 0;
62637
+ var hi = levels.length;
62638
+ var mid;
62639
+ while (lo < hi) {
62640
+ mid = (lo + hi) >> 1;
62641
+ if (levels[mid] > value) hi = mid;
62642
+ else lo = mid + 1;
62643
+ }
62644
+ return lo;
62645
+ }
62646
+
62647
+ function addCellSegments(dest, level, v0, v1, v2, v3, cx, cy, W, hCount) {
62648
+ var caseId = (v0 >= level ? 1 : 0) | (v1 >= level ? 2 : 0) |
62649
+ (v2 >= level ? 4 : 0) | (v3 >= level ? 8 : 0);
62650
+ var pairs, pair;
62651
+ if (caseId === 5 || caseId === 10) {
62652
+ pairs = getSaddlePairs(caseId, level, v0, v1, v2, v3);
62653
+ addSegment(dest, pairs[0], cx, cy, W, hCount);
62654
+ addSegment(dest, pairs[1], cx, cy, W, hCount);
62655
+ return;
62656
+ }
62657
+ pair = CONTOUR_CASES[caseId];
62658
+ if (pair) addSegment(dest, pair, cx, cy, W, hCount);
62659
+ }
62660
+
62661
+ function getSaddlePairs(caseId, level, v0, v1, v2, v3) {
62662
+ var centerAbove = (v0 + v1 + v2 + v3) / 4 >= level;
62663
+ if (caseId === 5) {
62664
+ return centerAbove ? SADDLE_C0_C2_JOINED : SADDLE_C0_C2_SPLIT;
62665
+ }
62666
+ return centerAbove ? SADDLE_C1_C3_JOINED : SADDLE_C1_C3_SPLIT;
62667
+ }
62668
+
62669
+ function addSegment(dest, pair, cx, cy, W, hCount) {
62670
+ dest.from.push(getCellEdgeId(pair[0], cx, cy, W, hCount));
62671
+ dest.to.push(getCellEdgeId(pair[1], cx, cy, W, hCount));
62672
+ }
62673
+
62674
+ // Edges are numbered so that neighboring cells agree on the id of the edge
62675
+ // they share: horizontal edges run left to right along a lattice row,
62676
+ // vertical edges run top to bottom down a lattice column.
62677
+ function getCellEdgeId(edge, cx, cy, W, hCount) {
62678
+ if (edge === 0) return cy * (W - 1) + cx; // top
62679
+ if (edge === 1) return hCount + cy * W + cx + 1; // right
62680
+ if (edge === 2) return (cy + 1) * (W - 1) + cx; // bottom
62681
+ return hCount + cy * W + cx; // left
62682
+ }
62683
+
62684
+ // Join directed segments end to end. Because every segment is oriented the
62685
+ // same way relative to the contour, each edge is the start of at most one
62686
+ // segment and the end of at most one, so following the chain is unambiguous.
62687
+ function stitchContourSegments(segments) {
62688
+ var from = segments.from;
62689
+ var to = segments.to;
62690
+ var next = new Map();
62691
+ var hasIncoming = new Set();
62692
+ var visited = new Set();
62693
+ var paths = [];
62694
+ var i, ring;
62695
+ for (i = 0; i < from.length; i++) {
62696
+ next.set(from[i], to[i]);
62697
+ hasIncoming.add(to[i]);
62698
+ }
62699
+ // Trace open contours first, starting from the edges nothing leads into, so
62700
+ // that a contour running between two grid boundaries is not entered part
62701
+ // way along and split in two.
62702
+ for (i = 0; i < from.length; i++) {
62703
+ if (hasIncoming.has(from[i]) || visited.has(from[i])) continue;
62704
+ paths.push(followContour(from[i], next, visited));
62705
+ }
62706
+ // Anything left over forms a closed ring.
62707
+ for (i = 0; i < from.length; i++) {
62708
+ if (visited.has(from[i])) continue;
62709
+ ring = followContour(from[i], next, visited);
62710
+ ring.push(ring[0]);
62711
+ paths.push(ring);
62712
+ }
62713
+ return paths;
62714
+ }
62715
+
62716
+ function followContour(startEdge, next, visited) {
62717
+ var path = [];
62718
+ var edge = startEdge;
62719
+ while (edge !== undefined && !visited.has(edge)) {
62720
+ visited.add(edge);
62721
+ path.push(edge);
62722
+ edge = next.get(edge);
62723
+ }
62724
+ return path;
62725
+ }
62726
+
62727
+ function getPathCoords(path, grid, band, level, hCount, toMapXY) {
62728
+ var coords = [];
62729
+ var prev = null;
62730
+ var point, xy;
62731
+ for (var i = 0; i < path.length; i++) {
62732
+ point = getEdgeCrossing(grid, band, level, path[i], hCount);
62733
+ xy = toMapXY(point[0], point[1]);
62734
+ // A corner value exactly equal to the level puts two crossings in the
62735
+ // same place; keeping both would emit zero-length segments.
62736
+ if (prev && xy[0] === prev[0] && xy[1] === prev[1]) continue;
62737
+ coords.push(xy);
62738
+ prev = xy;
62739
+ }
62740
+ return coords;
62741
+ }
62742
+
62743
+ // Returns the crossing point in lattice coordinates. The result depends only
62744
+ // on the edge and the level, so both cells sharing an edge compute the same
62745
+ // point, down to the last bit.
62746
+ function getEdgeCrossing(grid, band, level, edgeId, hCount) {
62747
+ var W = grid.width;
62748
+ var bands = grid.bands;
62749
+ var samples = grid.samples;
62750
+ var x, y, v0, v1;
62751
+ if (edgeId < hCount) {
62752
+ y = Math.floor(edgeId / (W - 1));
62753
+ x = edgeId - y * (W - 1);
62754
+ v0 = samples[(y * W + x) * bands + band];
62755
+ v1 = samples[(y * W + x + 1) * bands + band];
62756
+ return [x + interpolateCrossing(level, v0, v1), y];
62757
+ }
62758
+ edgeId -= hCount;
62759
+ y = Math.floor(edgeId / W);
62760
+ x = edgeId - y * W;
62761
+ v0 = samples[(y * W + x) * bands + band];
62762
+ v1 = samples[((y + 1) * W + x) * bands + band];
62763
+ return [x, y + interpolateCrossing(level, v0, v1)];
62764
+ }
62765
+
62766
+ function interpolateCrossing(level, v0, v1) {
62767
+ var t;
62768
+ if (v1 === v0) return 0.5;
62769
+ t = (level - v0) / (v1 - v0);
62770
+ return t < 0 ? 0 : t > 1 ? 1 : t;
62771
+ }
62772
+
62773
+ // Lattice coordinates are indexed from the center of the first pixel, not the
62774
+ // corner of the grid.
62775
+ function getLatticeToMapTransform(grid) {
62776
+ var bbox = grid.bbox;
62777
+ var dx = (bbox[2] - bbox[0]) / grid.width;
62778
+ var dy = (bbox[3] - bbox[1]) / grid.height;
62779
+ var x0 = bbox[0] + dx / 2;
62780
+ var y0 = bbox[3] - dy / 2;
62781
+ return function(col, row) {
62782
+ return [x0 + col * dx, y0 - row * dy];
62783
+ };
62784
+ }
62785
+
62786
+ function ascending(a, b) {
62787
+ return a - b;
62788
+ }
62789
+
62790
+ var RasterContours = /*#__PURE__*/Object.freeze({
62791
+ __proto__: null,
62792
+ getContourBand: getContourBand,
62793
+ getContourLevels: getContourLevels,
62794
+ getContourSmoothingDistance: getContourSmoothingDistance,
62795
+ getRasterContourLines: getRasterContourLines,
62796
+ getRasterSampleRange: getRasterSampleRange,
62797
+ traceRasterContours: traceRasterContours,
62798
+ validateRasterGridForContours: validateRasterGridForContours
62799
+ });
62800
+
62801
+ var DEFAULT_CONTOUR_FIELD = 'value';
62802
+
62803
+ cmd.contours = function(targetLyr, targetDataset, opts) {
62804
+ var field = opts.field || DEFAULT_CONTOUR_FIELD;
62805
+ var contours, dataset, outputLayers;
62806
+ if (!layerHasRaster(targetLyr)) {
62807
+ stop$1('Command requires a raster layer');
62808
+ }
62809
+ // Contours come from the layer's working samples, so they reflect any
62810
+ // earlier edits (e.g. -blur, -clip) rather than the original source pixels.
62811
+ contours = getRasterContourLines(targetLyr.raster, opts);
62812
+ message(getContoursMessage(contours));
62813
+ if (contours.lines.length === 0) {
62814
+ return [createEmptyContourLayer(targetLyr, opts)];
62815
+ }
62816
+ dataset = importGeoJSON(getContoursGeoJSON(contours.lines, field), {});
62817
+ setDatasetCrsInfo(dataset, getDatasetCrsInfo(targetDataset));
62818
+ // Smooth before merging, so that only the contour arcs are affected. Merging
62819
+ // first would put them in the same ArcCollection as any vector layer already
62820
+ // in the target dataset, and -smooth rewrites every arc it is given.
62821
+ if (!opts.no_smoothing) {
62822
+ smoothContourDataset(dataset, getRasterGrid(targetLyr.raster));
62823
+ }
62824
+ outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
62825
+ setOutputLayerName(outputLayers[0], targetLyr, 'contours', opts);
62826
+ return outputLayers;
62827
+ };
62828
+
62829
+ function smoothContourDataset(dataset, grid) {
62830
+ var crs = getDatasetCRS(dataset);
62831
+ var distance = getContourSmoothingDistance(grid, crs);
62832
+ if (!(distance > 0)) {
62833
+ message('Skipped contour smoothing: unable to determine the pixel size');
62834
+ return;
62835
+ }
62836
+ message('Smoothing contours with an auto-selected interval of ' +
62837
+ formatSmoothingDistance(distance, crs) + ' (one pixel)');
62838
+ cmd.smooth(dataset, {
62839
+ distance: distance,
62840
+ // The contour staircase is an artifact, so there are no real corners to
62841
+ // preserve and no sub-pixel detail worth prefiltering out.
62842
+ no_corners: true,
62843
+ no_prefilter: true
62844
+ }, dataset.layers);
62845
+ }
62846
+
62847
+ function formatSmoothingDistance(distance, crs) {
62848
+ var rounded = Number(distance.toPrecision(3));
62849
+ return crs ? rounded + 'm' : String(rounded);
62850
+ }
62851
+
62852
+ function getContoursGeoJSON(lines, field) {
62853
+ return {
62854
+ type: 'FeatureCollection',
62855
+ features: lines.map(function(line) {
62856
+ var properties = {};
62857
+ properties[field] = line.value;
62858
+ return {
62859
+ type: 'Feature',
62860
+ properties: properties,
62861
+ geometry: {
62862
+ type: 'LineString',
62863
+ coordinates: line.coords
62864
+ }
62865
+ };
62866
+ })
62867
+ };
62868
+ }
62869
+
62870
+ function createEmptyContourLayer(targetLyr, opts) {
62871
+ var lyr = {
62872
+ geometry_type: 'polyline',
62873
+ shapes: [],
62874
+ data: new DataTable([])
62875
+ };
62876
+ setOutputLayerName(lyr, targetLyr, 'contours', opts);
62877
+ return lyr;
62878
+ }
62879
+
62880
+ function getContoursMessage(contours) {
62881
+ var levels = contours.levels;
62882
+ if (levels.length === 0) {
62883
+ return 'No contour levels fall inside the range of this raster';
62884
+ }
62885
+ return 'Traced ' + contours.lines.length + ' contour ' +
62886
+ (contours.lines.length == 1 ? 'line' : 'lines') + ' at ' + levels.length +
62887
+ (levels.length == 1 ? ' level' : ' levels') +
62888
+ ' (' + levels[0] + ' to ' + levels[levels.length - 1] + ')';
62889
+ }
62890
+
61582
62891
  cmd.dashlines = function(lyr, dataset, opts) {
61583
62892
  var crs = getDatasetCRS(dataset);
61584
62893
  var defs = getStashedVar('defs');
@@ -63127,11 +64436,19 @@ ${svg}
63127
64436
  //
63128
64437
  cmd.dissolve = function(arg1, arg2, opts) {
63129
64438
  if (Array.isArray(arg1)) {
64439
+ arg1.forEach(requireDissolvableLayer);
63130
64440
  return dissolveLayers(arg1, arg2, opts);
63131
64441
  }
64442
+ requireDissolvableLayer(arg1);
63132
64443
  return dissolveSingleLayer(arg1, arg2, opts);
63133
64444
  };
63134
64445
 
64446
+ // Without this guard a raster layer takes the tabular branch of
64447
+ // dissolveOneLayer() and is replaced by a geometry-less aggregate.
64448
+ function requireDissolvableLayer(lyr) {
64449
+ requireNotRasterLayer(lyr, 'Raster layers cannot be dissolved');
64450
+ }
64451
+
63135
64452
  function dissolveLayers(layers, dataset, optsArg) {
63136
64453
  var opts = utils.extend({}, optsArg);
63137
64454
  if (opts.field) opts.fields = [opts.field]; // support old "field" parameter
@@ -66237,7 +67554,10 @@ ${svg}
66237
67554
  var src = parseCrsString$1('wgs84');
66238
67555
  var outline = getOutlineDataset(src, dest, {});
66239
67556
  var graticule = importGeoJSON(createGraticule(dest, !!outline, opts));
66240
- projectDataset(graticule, src, dest, {no_clip: false}); // TODO: densify?
67557
+ projectDataset(graticule, src, dest, {
67558
+ no_clip: false,
67559
+ densify: isInterruptedProjection(dest)
67560
+ });
66241
67561
  if (outline) {
66242
67562
  graticule = addOutlineToGraticule(graticule, outline);
66243
67563
  }
@@ -72202,7 +73522,7 @@ ${svg}
72202
73522
  }
72203
73523
  return [
72204
73524
  'affine', 'alpha-shapes', 'blur', 'buffer', 'calc', 'check-geometry',
72205
- 'classify', 'clean', 'clip', 'cluster', 'dashlines', 'data-fill',
73525
+ 'classify', 'clean', 'clip', 'cluster', 'contours', 'dashlines', 'data-fill',
72206
73526
  'densify', 'dissolve', 'dissolve2', 'divide', 'dots', 'each', 'erase',
72207
73527
  'explode', 'filter',
72208
73528
  'filter-detail', 'filter-fields', 'filter-geom', 'filter-islands',
@@ -72379,6 +73699,9 @@ ${svg}
72379
73699
  // } else if (name == 'comment') {
72380
73700
  // // no-op
72381
73701
 
73702
+ } else if (name == 'contours') {
73703
+ outputLayers = applyCommandToEachLayer(cmd.contours, targetLayers, targetDataset, opts);
73704
+
72382
73705
  } else if (name == 'dashlines') {
72383
73706
  applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
72384
73707
 
@@ -72863,7 +74186,7 @@ ${svg}
72863
74186
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
72864
74187
  }
72865
74188
 
72866
- var version = "0.7.47";
74189
+ var version = "0.7.49";
72867
74190
 
72868
74191
  // Parse command line args into commands and run them
72869
74192
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -73527,10 +74850,10 @@ ${svg}
73527
74850
 
73528
74851
  var BOX_BLUR_PASSES = 3;
73529
74852
 
73530
- function blurRasterGrid(raster, optsArg) {
74853
+ function blurRasterGrid(raster, optsArg, crs) {
73531
74854
  var opts = optsArg || {};
73532
74855
  var grid = getRasterGrid(raster);
73533
- var radius = getBlurRadius(opts);
74856
+ var radius = getBlurRadius(grid, opts, crs);
73534
74857
  var sigma = radius / 2;
73535
74858
  var boxes = getGaussianBoxWidths(sigma, BOX_BLUR_PASSES);
73536
74859
  var samples = new grid.samples.constructor(grid.samples.length);
@@ -73554,20 +74877,48 @@ ${svg}
73554
74877
  });
73555
74878
  }
73556
74879
 
73557
- function getBlurRadius(opts) {
74880
+ // The blur kernel works in pixels, but the radius may be given either in pixels
74881
+ // (a bare number or a px suffix) or as a real-world distance (e.g. 20m), which
74882
+ // is converted using the size of a pixel on the ground.
74883
+ function getBlurRadius(grid, opts, crs) {
73558
74884
  var arg = opts.radius;
73559
- var radius;
73560
- if (arg == null || arg === '') stop$1('Missing blur radius');
73561
- if (typeof arg == 'string') {
73562
- arg = arg.trim().replace(/px$/i, '');
74885
+ var measure, radius;
74886
+ if (arg === null || arg === undefined || arg === '') {
74887
+ stop$1('Missing blur radius');
74888
+ }
74889
+ if (typeof arg == 'string' && /px$/i.test(arg.trim())) {
74890
+ radius = Number(arg.trim().replace(/px$/i, ''));
74891
+ } else {
74892
+ measure = parseMeasure(arg);
74893
+ radius = measure.units ?
74894
+ convertDistanceToPixels(arg, grid, crs) :
74895
+ measure.value;
73563
74896
  }
73564
- radius = Number(arg);
73565
74897
  if (!(radius > 0 && isFinite(radius))) {
73566
- stop$1('Expected radius= to be a positive pixel value');
74898
+ stop$1('Expected a positive blur radius, received:', arg);
73567
74899
  }
73568
74900
  return radius;
73569
74901
  }
73570
74902
 
74903
+ function convertDistanceToPixels(arg, grid, crs) {
74904
+ // convertIntervalParam() gives the distance in the coordinate units of the
74905
+ // dataset, which is what the grid's bbox is measured in.
74906
+ return convertIntervalParam(arg, crs) / getPixelSize(grid);
74907
+ }
74908
+
74909
+ // Blurring is isotropic in pixel space, so a distance has to map to a single
74910
+ // pixel count; use the geometric mean when the pixels are not square.
74911
+ function getPixelSize(grid) {
74912
+ var bbox = grid.bbox;
74913
+ var dx, dy;
74914
+ if (!bbox) {
74915
+ stop$1('Raster layer is missing the georeferencing needed to blur by a distance');
74916
+ }
74917
+ dx = Math.abs(bbox[2] - bbox[0]) / grid.width;
74918
+ dy = Math.abs(bbox[3] - bbox[1]) / grid.height;
74919
+ return Math.sqrt(dx * dy);
74920
+ }
74921
+
73571
74922
  function validateBlurGrid(grid) {
73572
74923
  if (!grid || !grid.samples || !grid.width || !grid.height || !grid.bands) {
73573
74924
  stop$1('Expected a raster grid');
@@ -73716,8 +75067,8 @@ ${svg}
73716
75067
 
73717
75068
  function copyUnpremultipliedChannelToBand(channel, alpha, samples, grid, band) {
73718
75069
  var bands = grid.bands;
73719
- var isFloat = samples instanceof Float32Array || samples instanceof Float64Array;
73720
- var range = isFloat ? null : getTypedArrayRange(samples);
75070
+ var isFloat = rasterSamplesAreFloat(samples);
75071
+ var range = isFloat ? null : getSampleArrayRange(samples);
73721
75072
  var maxAlpha = getAlphaMaxValue(samples);
73722
75073
  var val, a;
73723
75074
  for (var i = 0, j = band; i < channel.length; i++, j += bands) {
@@ -73729,8 +75080,8 @@ ${svg}
73729
75080
 
73730
75081
  function copyChannelToBand(channel, samples, grid, band) {
73731
75082
  var bands = grid.bands;
73732
- var isFloat = samples instanceof Float32Array || samples instanceof Float64Array;
73733
- var range = isFloat ? null : getTypedArrayRange(samples);
75083
+ var isFloat = rasterSamplesAreFloat(samples);
75084
+ var range = isFloat ? null : getSampleArrayRange(samples);
73734
75085
  var val;
73735
75086
  for (var i = 0, j = band; i < channel.length; i++, j += bands) {
73736
75087
  val = channel[i];
@@ -73739,62 +75090,47 @@ ${svg}
73739
75090
  }
73740
75091
 
73741
75092
  function getRasterBlurWeights(grid) {
73742
- if (!grid.coverage && grid.nodata == null) return null;
75093
+ if (!rasterGridHasInvalidPixels(grid)) return null;
73743
75094
  var weights = new Float32Array(grid.width * grid.height);
73744
75095
  for (var i = 0; i < weights.length; i++) {
73745
- weights[i] = rasterBlurPixelIsValid(grid, i) ? 1 : 0;
75096
+ weights[i] = rasterPixelIsValid(grid, i) ? 1 : 0;
73746
75097
  }
73747
75098
  return weights;
73748
75099
  }
73749
75100
 
73750
- function rasterBlurPixelIsValid(grid, pixelId) {
73751
- var off, n;
73752
- if (grid.coverage && grid.coverage[pixelId] === 0) return false;
73753
- if (grid.nodata == null) return true;
73754
- off = pixelId * grid.bands;
73755
- n = Math.min(grid.bands, 3);
73756
- for (var i = 0; i < n; i++) {
73757
- if (grid.samples[off + i] != grid.nodata) return true;
73758
- }
73759
- return false;
73760
- }
73761
-
73762
75101
  function getAlphaMaxValue(samples) {
73763
- if (samples instanceof Float32Array || samples instanceof Float64Array) return 1;
73764
- return getTypedArrayRange(samples).max;
73765
- }
73766
-
73767
- function getTypedArrayRange(arr) {
73768
- if (arr instanceof Uint8Array || arr instanceof Uint8ClampedArray) return {min: 0, max: 255};
73769
- if (arr instanceof Int8Array) return {min: -128, max: 127};
73770
- if (arr instanceof Uint16Array) return {min: 0, max: 65535};
73771
- if (arr instanceof Int16Array) return {min: -32768, max: 32767};
73772
- if (arr instanceof Uint32Array) return {min: 0, max: 4294967295};
73773
- if (arr instanceof Int32Array) return {min: -2147483648, max: 2147483647};
73774
- return {min: -Infinity, max: Infinity};
75102
+ if (rasterSamplesAreFloat(samples)) return 1;
75103
+ return getSampleArrayRange(samples).max;
73775
75104
  }
73776
75105
 
73777
75106
  function clamp(val, min, max) {
73778
75107
  return val < min ? min : val > max ? max : val;
73779
75108
  }
73780
75109
 
75110
+ var RasterBlur = /*#__PURE__*/Object.freeze({
75111
+ __proto__: null,
75112
+ blurRasterGrid: blurRasterGrid,
75113
+ getBlurRadius: getBlurRadius
75114
+ });
75115
+
73781
75116
  cmd.blur = blurRasterLayers;
73782
75117
 
73783
75118
  function blurRasterLayers(layers, dataset, optsArg) {
73784
75119
  var opts = optsArg || {};
75120
+ var crs = getDatasetCRS(dataset);
73785
75121
  requireProjectedDataset(dataset);
73786
75122
  layers.forEach(function(lyr) {
73787
75123
  if (!layerHasRaster(lyr)) {
73788
75124
  stop$1('Command requires a raster layer');
73789
75125
  }
73790
- blurRasterLayer(lyr, opts);
75126
+ blurRasterLayer(lyr, opts, crs);
73791
75127
  });
73792
75128
  }
73793
75129
 
73794
- function blurRasterLayer(lyr, opts) {
75130
+ function blurRasterLayer(lyr, opts, crs) {
73795
75131
  var raster = lyr.raster;
73796
75132
  noteLayerWillChange(lyr, {operation: 'blurRasterLayer', unit: 'raster'});
73797
- raster.grid = blurRasterGrid(raster, opts);
75133
+ raster.grid = blurRasterGrid(raster, opts, crs);
73798
75134
  raster.view = raster.view || {};
73799
75135
  delete raster.view.scalingStats;
73800
75136
  if (runningInBrowser()) {
@@ -74698,6 +76034,7 @@ ${svg}
74698
76034
  Catalog$1,
74699
76035
  ClipErase,
74700
76036
  ClipPoints,
76037
+ ColorUtils,
74701
76038
  Colorizer,
74702
76039
  CustomProjections,
74703
76040
  Dymaxion,
@@ -74745,6 +76082,9 @@ ${svg}
74745
76082
  Lines,
74746
76083
  Logging,
74747
76084
  Profile,
76085
+ RasterBlur,
76086
+ RasterContours,
76087
+ RasterGrid,
74748
76088
  RasterReprojection,
74749
76089
  RasterUtils,
74750
76090
  Merging,