mapshaper 0.7.48 → 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$5(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$5(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$5, 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;
@@ -6378,7 +6685,7 @@
6378
6685
 
6379
6686
  function projectFacePoint(face, lam, phi) {
6380
6687
  var p = face.project(lam, phi);
6381
- p = applyMatrix$1(face.transform, p);
6688
+ p = applyMatrix(face.transform, p);
6382
6689
  p = transformOutputPoint(p);
6383
6690
  p[0] -= centerX;
6384
6691
  p[1] -= centerY;
@@ -6390,7 +6697,7 @@
6390
6697
  var p, rotated;
6391
6698
  if (!face || !face.project.invert) return null;
6392
6699
  p = inverseTransformOutputPoint([x + centerX, y + centerY]);
6393
- p = applyMatrix$1(invertMatrix$1(face.transform), p);
6700
+ p = applyMatrix(invertMatrix(face.transform), p);
6394
6701
  rotated = face.project.invert(p[0], p[1]);
6395
6702
  if (!rotated) return null;
6396
6703
  return rotateRadians(
@@ -6483,7 +6790,7 @@
6483
6790
  config.rotation[2] * D2R$7,
6484
6791
  true
6485
6792
  );
6486
- 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];
6487
6794
  });
6488
6795
  var paths = splitPathAtAntimeridian$3(
6489
6796
  interpolateGreatCircle(endpoints[0], endpoints[1], 0.05)
@@ -6513,15 +6820,15 @@
6513
6820
  }),
6514
6821
  seams: seams,
6515
6822
  findRegion: function(lon, lat) {
6516
- var lam = normalizeLongitude$3(lon - lon0) * D2R$7;
6823
+ var lam = normalizeLongitude$1(lon - lon0) * D2R$7;
6517
6824
  return findFace(lam, lat * D2R$7);
6518
6825
  },
6519
6826
  findTransitionRegion: function(lon, lat) {
6520
- var lam = normalizeLongitude$3(lon - lon0) * D2R$7;
6827
+ var lam = normalizeLongitude$1(lon - lon0) * D2R$7;
6521
6828
  return findTransitionRegion(lam, lat * D2R$7);
6522
6829
  },
6523
6830
  projectRegion: function(lon, lat, regionId) {
6524
- var lam = normalizeLongitude$3(lon - lon0) * D2R$7;
6831
+ var lam = normalizeLongitude$1(lon - lon0) * D2R$7;
6525
6832
  return forwardFace(lam, lat * D2R$7, regionId);
6526
6833
  },
6527
6834
  regionsAreAttached: function(a, b) {
@@ -6629,8 +6936,8 @@
6629
6936
  var adjacent = findAdjacentFace(faces, face.id, a, b);
6630
6937
  if (adjacent >= 0 && attachedPairs.has(pairKey(face.id, adjacent))) continue;
6631
6938
  edges.push([
6632
- applyMatrix$1(face.transform, face.project(a[0] * D2R$7, a[1] * D2R$7)),
6633
- 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))
6634
6941
  ]);
6635
6942
  }
6636
6943
  });
@@ -6747,25 +7054,6 @@
6747
7054
  ];
6748
7055
  }
6749
7056
 
6750
- function applyMatrix$1(m, p) {
6751
- return [
6752
- m[0] * p[0] + m[1] * p[1] + m[2],
6753
- m[3] * p[0] + m[4] * p[1] + m[5]
6754
- ];
6755
- }
6756
-
6757
- function invertMatrix$1(m) {
6758
- var det = m[0] * m[4] - m[1] * m[3];
6759
- return [
6760
- m[4] / det,
6761
- -m[1] / det,
6762
- (m[1] * m[5] - m[4] * m[2]) / det,
6763
- -m[3] / det,
6764
- m[0] / det,
6765
- (m[3] * m[2] - m[0] * m[5]) / det
6766
- ];
6767
- }
6768
-
6769
7057
  function rotateRadians(lam, phi, deltaLam, deltaPhi, deltaGamma, invert) {
6770
7058
  if (invert) {
6771
7059
  var inv = rotatePhiGamma$1(lam, phi, deltaPhi, deltaGamma, true);
@@ -6791,21 +7079,21 @@
6791
7079
  return [
6792
7080
  Math.atan2(y * cosDeltaGamma + z * sinDeltaGamma,
6793
7081
  x * cosDeltaPhi + k * sinDeltaPhi),
6794
- Math.asin(clamp$4(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
7082
+ Math.asin(clamp$2(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
6795
7083
  ];
6796
7084
  }
6797
7085
  k = z * cosDeltaPhi + x * sinDeltaPhi;
6798
7086
  return [
6799
7087
  Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma,
6800
7088
  x * cosDeltaPhi - z * sinDeltaPhi),
6801
- Math.asin(clamp$4(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
7089
+ Math.asin(clamp$2(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
6802
7090
  ];
6803
7091
  }
6804
7092
 
6805
7093
  function interpolateGreatCircle(a, b, interval) {
6806
7094
  var av = degreesToVector$2(a[0], a[1]);
6807
7095
  var bv = degreesToVector$2(b[0], b[1]);
6808
- var angle = Math.acos(clamp$4(dot$2(av, bv), -1, 1));
7096
+ var angle = Math.acos(clamp$2(dot$2(av, bv), -1, 1));
6809
7097
  var n = Math.max(1, Math.ceil(angle * R2D$6 / interval));
6810
7098
  var sinAngle = Math.sin(angle);
6811
7099
  var points = [];
@@ -6864,7 +7152,7 @@
6864
7152
  }
6865
7153
 
6866
7154
  function angularDistance(a, b) {
6867
- return Math.acos(clamp$4(dot$2(degreesToVector$2(a[0], a[1]),
7155
+ return Math.acos(clamp$2(dot$2(degreesToVector$2(a[0], a[1]),
6868
7156
  degreesToVector$2(b[0], b[1])), -1, 1));
6869
7157
  }
6870
7158
 
@@ -6880,7 +7168,7 @@
6880
7168
  function vectorToDegrees$2(p) {
6881
7169
  return [
6882
7170
  Math.atan2(p[1], p[0]) * R2D$6,
6883
- Math.asin(clamp$4(p[2], -1, 1)) * R2D$6
7171
+ Math.asin(clamp$2(p[2], -1, 1)) * R2D$6
6884
7172
  ];
6885
7173
  }
6886
7174
 
@@ -6935,16 +7223,6 @@
6935
7223
  return lam;
6936
7224
  }
6937
7225
 
6938
- function normalizeLongitude$3(lon) {
6939
- while (lon > 180) lon -= 360;
6940
- while (lon < -180) lon += 360;
6941
- return lon;
6942
- }
6943
-
6944
- function clamp$4(val, min, max) {
6945
- return Math.max(min, Math.min(max, val));
6946
- }
6947
-
6948
7226
  /*
6949
7227
  * Buckminster Fuller's Airocean arrangement of the icosahedron.
6950
7228
  *
@@ -8150,6 +8428,7 @@
8150
8428
  * equations, not Imago's power-law approximation.
8151
8429
  */
8152
8430
 
8431
+
8153
8432
  var D2R$2 = Math.PI / 180;
8154
8433
  var R2D$2 = 180 / Math.PI;
8155
8434
  var HALF_PI = Math.PI / 2;
@@ -8298,7 +8577,7 @@
8298
8577
  boundary: sector.sphericalBoundary.map(function(p) {
8299
8578
  var q = fromCanonical(p[0], p[1], orientation);
8300
8579
  return [
8301
- normalizeLongitude$2(q[0] * R2D$2 + lon0),
8580
+ normalizeLongitude$1(q[0] * R2D$2 + lon0),
8302
8581
  q[1] * R2D$2
8303
8582
  ];
8304
8583
  })
@@ -8378,7 +8657,7 @@
8378
8657
  return applyOobTransform(p, facet, oobKey);
8379
8658
  });
8380
8659
  [0, 1].forEach(function(folded) {
8381
- var foldedPolygon = clipPolygonAxis$1(oobPolygon, 0, 0, folded == 0);
8660
+ var foldedPolygon = clipPolygonAxis(oobPolygon, 0, 0, folded == 0);
8382
8661
  if (foldedPolygon.length < 3) return;
8383
8662
  foldedPolygon = foldedPolygon.map(function(p) {
8384
8663
  return applyFoldTransform(p, folded);
@@ -8507,26 +8786,26 @@
8507
8786
  var polygon = clipRasterOobPolygon(vertices, piece.oobKey);
8508
8787
  polygon = polygon.map(function(vertex) {
8509
8788
  var p = applyOobTransform([vertex.x, vertex.y], facet, piece.oobKey);
8510
- return copyRasterVertex$1(vertex, p[0], p[1]);
8789
+ return copyRasterVertex(vertex, p[0], p[1]);
8511
8790
  });
8512
- polygon = clipRasterPolygonAxis$1(
8791
+ polygon = clipRasterPolygonAxis(
8513
8792
  polygon, 'x', 0, piece.folded == 0);
8514
8793
  polygon = polygon.map(function(vertex) {
8515
8794
  var p = applyFoldTransform([vertex.x, vertex.y], piece.folded);
8516
- return copyRasterVertex$1(vertex, p[0], p[1]);
8795
+ return copyRasterVertex(vertex, p[0], p[1]);
8517
8796
  });
8518
8797
  polygon = polygon.map(function(vertex) {
8519
- return copyRasterVertex$1(
8798
+ return copyRasterVertex(
8520
8799
  vertex,
8521
8800
  vertex.x + LAYOUT_SHIFT +
8522
8801
  piece.wrap * 2 * BLOCK_HEIGHT - BLOCK_HEIGHT,
8523
8802
  vertex.y + 1.5
8524
8803
  );
8525
8804
  });
8526
- polygon = clipRasterPolygonAxis$1(polygon, 'x', XMIN, true);
8527
- polygon = clipRasterPolygonAxis$1(polygon, 'x', XMAX, false);
8528
- polygon = clipRasterPolygonAxis$1(polygon, 'y', YMIN, true);
8529
- 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);
8530
8809
  polygon.forEach(function(vertex) {
8531
8810
  vertex.x *= EDGE_SCALE;
8532
8811
  vertex.y *= EDGE_SCALE;
@@ -8536,62 +8815,22 @@
8536
8815
 
8537
8816
  function clipRasterOobPolygon(polygon, oobKey) {
8538
8817
  if (oobKey == 'xpos') {
8539
- return clipRasterPolygonAxis$1(polygon, 'x', 3, true);
8818
+ return clipRasterPolygonAxis(polygon, 'x', 3, true);
8540
8819
  }
8541
8820
  if (oobKey == 'xneg') {
8542
- return clipRasterPolygonAxis$1(polygon, 'x', -3, false);
8821
+ return clipRasterPolygonAxis(polygon, 'x', -3, false);
8543
8822
  }
8544
8823
  if (oobKey == 'ypos' || oobKey == 'yneg') {
8545
- polygon = clipRasterPolygonAxis$1(polygon, 'x', -3, true);
8546
- polygon = clipRasterPolygonAxis$1(polygon, 'x', 3, false);
8547
- return clipRasterPolygonAxis$1(
8824
+ polygon = clipRasterPolygonAxis(polygon, 'x', -3, true);
8825
+ polygon = clipRasterPolygonAxis(polygon, 'x', 3, false);
8826
+ return clipRasterPolygonAxis(
8548
8827
  polygon, 'y', oobKey == 'ypos' ? SQRT3$1 : -SQRT3$1,
8549
8828
  oobKey == 'ypos');
8550
8829
  }
8551
- polygon = clipRasterPolygonAxis$1(polygon, 'x', -3, true);
8552
- polygon = clipRasterPolygonAxis$1(polygon, 'x', 3, false);
8553
- polygon = clipRasterPolygonAxis$1(polygon, 'y', -SQRT3$1, true);
8554
- return clipRasterPolygonAxis$1(polygon, 'y', SQRT3$1, false);
8555
- }
8556
-
8557
- function clipRasterPolygonAxis$1(polygon, axis, value, keepGreater) {
8558
- var output = [];
8559
- if (polygon.length === 0) return output;
8560
- var a = polygon[polygon.length - 1];
8561
- var aInside = keepGreater ?
8562
- a[axis] >= value - EPS$1 : a[axis] <= value + EPS$1;
8563
- polygon.forEach(function(b) {
8564
- var bInside = keepGreater ?
8565
- b[axis] >= value - EPS$1 : b[axis] <= value + EPS$1;
8566
- if (aInside != bInside) {
8567
- var t = (value - a[axis]) / (b[axis] - a[axis]);
8568
- var vertex = {
8569
- x: a.x + (b.x - a.x) * t,
8570
- y: a.y + (b.y - a.y) * t,
8571
- sx: a.sx + (b.sx - a.sx) * t,
8572
- sy: a.sy + (b.sy - a.sy) * t,
8573
- lon: a.lon + (b.lon - a.lon) * t,
8574
- lat: a.lat + (b.lat - a.lat) * t
8575
- };
8576
- vertex[axis] = value;
8577
- output.push(vertex);
8578
- }
8579
- if (bInside) output.push(copyRasterVertex$1(b, b.x, b.y));
8580
- a = b;
8581
- aInside = bInside;
8582
- });
8583
- return output;
8584
- }
8585
-
8586
- function copyRasterVertex$1(vertex, x, y) {
8587
- return {
8588
- x: x,
8589
- y: y,
8590
- sx: vertex.sx,
8591
- sy: vertex.sy,
8592
- lon: vertex.lon,
8593
- lat: vertex.lat
8594
- };
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);
8595
8834
  }
8596
8835
 
8597
8836
  function applyOobTransform(p, facet, oobKey) {
@@ -8612,57 +8851,34 @@
8612
8851
  }
8613
8852
 
8614
8853
  function clipOobPolygon(polygon, oobKey) {
8615
- if (oobKey == 'xpos') return clipPolygonAxis$1(polygon, 0, 3, true);
8616
- 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);
8617
8856
  if (oobKey == 'ypos') {
8618
- return clipPolygonAxis$1(
8619
- clipPolygonAxis$1(
8620
- clipPolygonAxis$1(polygon, 0, -3, true), 0, 3, false),
8857
+ return clipPolygonAxis(
8858
+ clipPolygonAxis(
8859
+ clipPolygonAxis(polygon, 0, -3, true), 0, 3, false),
8621
8860
  1, SQRT3$1, true);
8622
8861
  }
8623
8862
  if (oobKey == 'yneg') {
8624
- return clipPolygonAxis$1(
8625
- clipPolygonAxis$1(
8626
- clipPolygonAxis$1(polygon, 0, -3, true), 0, 3, false),
8863
+ return clipPolygonAxis(
8864
+ clipPolygonAxis(
8865
+ clipPolygonAxis(polygon, 0, -3, true), 0, 3, false),
8627
8866
  1, -SQRT3$1, false);
8628
8867
  }
8629
8868
  return clipRectangle(polygon, -3, -SQRT3$1, 3, SQRT3$1);
8630
8869
  }
8631
8870
 
8632
8871
  function clipRectangle(polygon, xmin, ymin, xmax, ymax) {
8633
- polygon = clipPolygonAxis$1(polygon, 0, xmin, true);
8634
- polygon = clipPolygonAxis$1(polygon, 0, xmax, false);
8635
- polygon = clipPolygonAxis$1(polygon, 1, ymin, true);
8636
- return clipPolygonAxis$1(polygon, 1, ymax, false);
8637
- }
8638
-
8639
- function clipPolygonAxis$1(polygon, axis, value, keepGreater) {
8640
- var output = [];
8641
- if (polygon.length === 0) return output;
8642
- var a = polygon[polygon.length - 1];
8643
- var aInside = keepGreater ? a[axis] >= value - EPS$1 : a[axis] <= value + EPS$1;
8644
- polygon.forEach(function(b) {
8645
- var bInside = keepGreater ? b[axis] >= value - EPS$1 : b[axis] <= value + EPS$1;
8646
- if (aInside != bInside) {
8647
- var t = (value - a[axis]) / (b[axis] - a[axis]);
8648
- var p = [
8649
- a[0] + (b[0] - a[0]) * t,
8650
- a[1] + (b[1] - a[1]) * t
8651
- ];
8652
- p[axis] = value;
8653
- output.push(p);
8654
- }
8655
- if (bInside) output.push(b.concat());
8656
- a = b;
8657
- aInside = bInside;
8658
- });
8659
- 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);
8660
8876
  }
8661
8877
 
8662
8878
  function interpolateSphericalRadians(a, b, interval) {
8663
8879
  var av = radiansToVector(a[0], a[1]);
8664
8880
  var bv = radiansToVector(b[0], b[1]);
8665
- var angle = Math.acos(clamp$3(dot$1(av, bv), -1, 1));
8881
+ var angle = Math.acos(clamp$2(dot$1(av, bv), -1, 1));
8666
8882
  var count = Math.max(1, Math.ceil(angle / interval));
8667
8883
  var sinAngle = Math.sin(angle);
8668
8884
  var points = [];
@@ -8682,7 +8898,7 @@
8682
8898
  function vectorToRadians(p) {
8683
8899
  return [
8684
8900
  Math.atan2(p[1], p[0]),
8685
- Math.asin(clamp$3(p[2], -1, 1))
8901
+ Math.asin(clamp$2(p[2], -1, 1))
8686
8902
  ];
8687
8903
  }
8688
8904
 
@@ -8756,8 +8972,8 @@
8756
8972
  }
8757
8973
  x = qx - BLOCK_HEIGHT;
8758
8974
  y = qy + 1.5;
8759
- x = clamp$3(x, XMIN, XMAX);
8760
- y = clamp$3(y, YMIN, YMAX);
8975
+ x = clamp$2(x, XMIN, XMAX);
8976
+ y = clamp$2(y, YMIN, YMAX);
8761
8977
  return {
8762
8978
  x: x,
8763
8979
  y: y,
@@ -8867,7 +9083,7 @@
8867
9083
  var v = radiansToVector(lam, phi);
8868
9084
  return [
8869
9085
  Math.atan2(dot$1(v, orientation.y), dot$1(v, orientation.x)),
8870
- Math.asin(clamp$3(dot$1(v, orientation.z), -1, 1))
9086
+ Math.asin(clamp$2(dot$1(v, orientation.z), -1, 1))
8871
9087
  ];
8872
9088
  }
8873
9089
 
@@ -8878,7 +9094,7 @@
8878
9094
  orientation.x[1] * v[0] + orientation.y[1] * v[1] + orientation.z[1] * v[2],
8879
9095
  orientation.x[2] * v[0] + orientation.y[2] * v[1] + orientation.z[2] * v[2]
8880
9096
  ];
8881
- return [Math.atan2(p[1], p[0]), Math.asin(clamp$3(p[2], -1, 1))];
9097
+ return [Math.atan2(p[1], p[0]), Math.asin(clamp$2(p[2], -1, 1))];
8882
9098
  }
8883
9099
 
8884
9100
  // Adapted from Justin Kunimune's Imago implementation.
@@ -8892,7 +9108,7 @@
8892
9108
  lat1 = lat;
8893
9109
  lon1 = lon - lon0;
8894
9110
  } else {
8895
- lat1 = Math.asin(clamp$3(
9111
+ lat1 = Math.asin(clamp$2(
8896
9112
  Math.sin(lat0) * Math.sin(lat) +
8897
9113
  Math.cos(lat0) * Math.cos(lat) * Math.cos(lon0 - lon),
8898
9114
  -1,
@@ -8903,7 +9119,7 @@
8903
9119
  (Math.cos(lat0) * Math.sin(lat) -
8904
9120
  Math.sin(lat0) * Math.cos(lat) * Math.cos(lon0 - lon)) /
8905
9121
  denominator;
8906
- lon1 = Math.acos(clamp$3(value, -1, 1)) - Math.PI;
9122
+ lon1 = Math.acos(clamp$2(value, -1, 1)) - Math.PI;
8907
9123
  if (Math.sin(lon - lon0) > 0) lon1 = -lon1;
8908
9124
  }
8909
9125
  return [lat1, normalizeRadians(lon1 - theta0)];
@@ -8913,7 +9129,7 @@
8913
9129
  var lat0 = pole.lat;
8914
9130
  var lon0 = pole.lon;
8915
9131
  lon += pole.meridian;
8916
- var latOut = Math.asin(clamp$3(
9132
+ var latOut = Math.asin(clamp$2(
8917
9133
  Math.sin(lat0) * Math.sin(lat) -
8918
9134
  Math.cos(lat0) * Math.cos(lon) * Math.cos(lat),
8919
9135
  -1,
@@ -8925,8 +9141,8 @@
8925
9141
  } else {
8926
9142
  var value = Math.sin(lat) / Math.cos(lat0) / Math.cos(latOut) -
8927
9143
  Math.tan(lat0) * Math.tan(latOut);
8928
- if (Math.sin(lon) > 0) lonOut = lon0 + Math.acos(clamp$3(value, -1, 1));
8929
- else lonOut = lon0 - Math.acos(clamp$3(value, -1, 1));
9144
+ if (Math.sin(lon) > 0) lonOut = lon0 + Math.acos(clamp$2(value, -1, 1));
9145
+ else lonOut = lon0 - Math.acos(clamp$2(value, -1, 1));
8930
9146
  }
8931
9147
  return [latOut, normalizeRadians(lonOut)];
8932
9148
  }
@@ -8949,7 +9165,7 @@
8949
9165
  var y = edge[0][1] + (edge[1][1] - edge[0][1]) * t;
8950
9166
  var p = inverse(x * EDGE_SCALE, y * EDGE_SCALE);
8951
9167
  path.push([
8952
- normalizeLongitude$2(p[0] * R2D$2 + lon0),
9168
+ normalizeLongitude$1(p[0] * R2D$2 + lon0),
8953
9169
  p[1] * R2D$2
8954
9170
  ]);
8955
9171
  }
@@ -9040,20 +9256,194 @@
9040
9256
  return lam;
9041
9257
  }
9042
9258
 
9043
- function normalizeLongitude$2(lon) {
9044
- while (lon > 180) lon -= 360;
9045
- while (lon < -180) lon += 360;
9046
- 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
+ };
9047
9295
  }
9048
9296
 
9049
- function clamp$3(value, min, max) {
9050
- 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();
9051
9306
  }
9052
9307
 
9053
- 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({
9054
9436
  __proto__: null,
9055
- getNarukawa2022Engine: getNarukawa2022Engine,
9056
- 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
9057
9447
  });
9058
9448
 
9059
9449
  /*
@@ -9086,6 +9476,32 @@
9086
9476
  var RECT_YMAX = 0;
9087
9477
  var EPS = 1e-12;
9088
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
+
9089
9505
  var engines = {};
9090
9506
 
9091
9507
  var TETRAHEDRON_VERTICES = [
@@ -9208,14 +9624,20 @@
9208
9624
 
9209
9625
  // Internal inverse used to trace the geographic cut corresponding to the
9210
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().
9211
9630
  function inverse(x, y) {
9212
9631
  var q = uncenterOutputPoint([x, y]);
9213
9632
  for (var i = 0; i < rasterPieces.length; i++) {
9214
9633
  var piece = rasterPieces[i];
9215
- if (!pointInRing$2(centerOutputPoint(q), piece.boundary)) continue;
9216
- var p = applyMatrix(invertMatrix(piece.matrix), q);
9217
- var projected = getLayoutCopy(p);
9218
- if (projected.id != piece.copy) continue;
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;
9219
9641
  p = denormalizeBasePoint(p, normalization);
9220
9642
  var spherical = base.invertFace(p[0], p[1], piece.face);
9221
9643
  if (!spherical) continue;
@@ -9278,9 +9700,12 @@
9278
9700
  paths: seam.paths
9279
9701
  };
9280
9702
  });
9703
+ var frameCut = createFrameCutPaths(inverse, lon0);
9281
9704
  seams.push({
9282
9705
  type: 'cut',
9283
- paths: createFrameCutPaths(inverse, lon0)
9706
+ paths: frameCut.paths,
9707
+ // How much of the frame the trace resolved; see FRAME_INSET.
9708
+ trace_stats: frameCut.stats
9284
9709
  });
9285
9710
  seamCache.set(lon0, seams);
9286
9711
  }
@@ -9390,13 +9815,16 @@
9390
9815
  projected = clipPolygonAxis(projected, 1, RECT_YMAX, false);
9391
9816
  if (projected.length < 3 ||
9392
9817
  Math.abs(getRingArea$2(projected)) < MIN_PIECE_AREA) return;
9818
+ var boundary = projected.map(centerOutputPoint);
9393
9819
  pieces.push({
9394
9820
  id: pieces.length,
9395
9821
  face: region.id,
9396
9822
  source_region: region.id,
9397
9823
  copy: copy.id,
9398
9824
  matrix: matrix,
9399
- boundary: projected.map(centerOutputPoint)
9825
+ inverse_matrix: invertMatrix(matrix),
9826
+ bounds: getRingBounds(boundary),
9827
+ boundary: boundary
9400
9828
  });
9401
9829
  });
9402
9830
  });
@@ -9438,40 +9866,59 @@
9438
9866
  };
9439
9867
  }
9440
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.
9441
9880
  function createFrameCutPaths(inverse, lon0) {
9442
- // The repeated tetrahedral net is continuous inside the frame. Its only
9443
- // geographic cuts are the three independent sides of the periodic rectangle.
9444
- var n = 6000;
9445
- // Stay far enough inside the frame to avoid browser-specific inverse
9446
- // branches at the exact Lee face boundary.
9447
- var epsilon = 5e-8;
9881
+ profileStart('lee.traceFrameCuts');
9882
+ var e = FRAME_INSET;
9448
9883
  var xmin = centerOutputPoint([RECT_XMIN, 0])[0];
9449
9884
  var xmax = centerOutputPoint([RECT_XMAX, 0])[0];
9450
9885
  var ymin = centerOutputPoint([0, RECT_YMIN])[1];
9451
9886
  var ymax = centerOutputPoint([0, RECT_YMAX])[1];
9452
9887
  var edges = [
9453
- [[xmin + epsilon, ymin], [xmin + epsilon, ymax]],
9454
- [[xmin, ymax - epsilon], [xmax, ymax - epsilon]],
9455
- [[xmin, ymin + epsilon], [xmax, ymin + epsilon]]
9888
+ [[xmin + e, ymin], [xmin + e, ymax]],
9889
+ [[xmin, ymax - e], [xmax, ymax - e]],
9890
+ [[xmin, ymin + e], [xmax, ymin + e]]
9456
9891
  ];
9457
- return edges.reduce(function(memo, edge) {
9892
+ var stats = {sample_count: 0, failed_count: 0};
9893
+ var paths = edges.reduce(function(memo, edge) {
9458
9894
  var path = [];
9459
- for (var i = 0; i <= n; i++) {
9460
- var t = i / n;
9461
- var x = edge[0][0] + (edge[1][0] - edge[0][0]) * t;
9462
- var y = edge[0][1] + (edge[1][1] - edge[0][1]) * t;
9463
- var p = inverse(x, y);
9464
- if (!p) continue;
9465
- path.push([
9466
- normalizeLongitude$1(p[0] * R2D$1 + lon0),
9467
- p[1] * R2D$1
9468
- ]);
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]);
9469
9909
  }
9470
9910
  return memo.concat(splitPathAtAntimeridian$1(path));
9471
9911
  }, []).map(function(path) {
9472
9912
  path.mask_width = 4e-5;
9473
9913
  return path;
9474
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};
9475
9922
  }
9476
9923
 
9477
9924
  function splitPathAtAntimeridian$1(path) {
@@ -9577,60 +10024,77 @@
9577
10024
  return project;
9578
10025
  }
9579
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
+
9580
10048
  function leeRaw(lam, phi) {
9581
- var w = [-0.5, SQRT3 / 2];
9582
- var z = complexMultiply(stereographicRaw(lam, phi), [SQRT2, 0]);
9583
- var powers = [0, 1, 2].map(function(i) {
9584
- return complexPower(w, [i, 0]);
9585
- });
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.
9586
10053
  var sector = 0;
9587
- for (var i = 1; i < powers.length; i++) {
9588
- if (complexMultiply(z, powers[i])[0] >
9589
- 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
+ }
9590
10061
  }
9591
- var rot = powers[sector];
10062
+ var rot = LEE_ROTATIONS[sector];
9592
10063
  var n = complexNorm(z);
9593
10064
  var h = [0, 0];
9594
10065
  var k = [0, 0];
9595
10066
 
9596
10067
  if (n > 0.3) {
9597
10068
  var y = complexSubtract([1, 0], complexMultiply(rot, z));
9598
- var w1 = 1.4021821053254548;
9599
- var coefficients = [
9600
- 1.15470053837925, 0.192450089729875, 0.0481125224324687,
9601
- 0.010309826235529, 3.34114739114366e-4, -0.00150351632601465,
9602
- -0.0012304417796231, -675190201960282e-18,
9603
- -284084537293856e-18, -821205120500051e-19,
9604
- -159257630018706e-20, 1.91691805888369e-5,
9605
- 1.73095888028726e-5, 1.03865580818367e-5,
9606
- 4.70614523937179e-6, 1.4413500104181e-6,
9607
- 1.92757960170179e-8, -3.82869799649063e-7,
9608
- -3.57526015225576e-7, -2.2175964844211e-7
9609
- ];
9610
- var g = [0, 0];
9611
- for (i = coefficients.length - 1; i >= 0; i--) {
9612
- g = complexAdd([coefficients[i], 0], complexMultiply(g, y));
9613
- }
9614
- 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]));
9615
10079
  k = complexMultiply(complexMultiply(k, rot), rot);
9616
10080
  }
9617
10081
 
9618
10082
  if (n < 0.5) {
9619
- var h0 = [1, 1 / 8, 3 / 56, 1 / 32, 35 / 1664, 63 / 4096, 231 / 19456];
9620
10083
  var z3 = complexPower(z, [3, 0]);
9621
- for (i = h0.length - 1; i >= 0; i--) {
9622
- 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;
9623
10090
  }
9624
- h = complexMultiply(h, z);
10091
+ h = complexMultiply([h0, h1], z);
9625
10092
  }
9626
10093
 
9627
10094
  if (n < 0.3) return h;
9628
10095
  if (n > 0.5) return k;
9629
10096
  var t = (n - 0.3) / 0.2;
9630
- return complexAdd(
9631
- complexMultiply(k, [t, 0]),
9632
- complexMultiply(h, [1 - t, 0])
9633
- );
10097
+ return [k[0] * t + h[0] * (1 - t), k[1] * t + h[1] * (1 - t)];
9634
10098
  }
9635
10099
 
9636
10100
  function invertLeeRaw(x, y) {
@@ -9680,10 +10144,6 @@
9680
10144
  return [k * cosPhi * Math.sin(lam), k * Math.sin(phi)];
9681
10145
  }
9682
10146
 
9683
- function complexAdd(a, b) {
9684
- return [a[0] + b[0], a[1] + b[1]];
9685
- }
9686
-
9687
10147
  function complexSubtract(a, b) {
9688
10148
  return [a[0] - b[0], a[1] - b[1]];
9689
10149
  }
@@ -9711,13 +10171,6 @@
9711
10171
  return [magnitude * Math.cos(angle), magnitude * Math.sin(angle)];
9712
10172
  }
9713
10173
 
9714
- function applyMatrix(m, p) {
9715
- return [
9716
- m[0] * p[0] + m[1] * p[1] + m[2],
9717
- m[3] * p[0] + m[4] * p[1] + m[5]
9718
- ];
9719
- }
9720
-
9721
10174
  function shiftMatrixX(matrix, offset) {
9722
10175
  var shifted = matrix.concat();
9723
10176
  shifted[2] += offset;
@@ -9730,84 +10183,15 @@
9730
10183
  return x;
9731
10184
  }
9732
10185
 
9733
- function invertMatrix(m) {
9734
- var det = m[0] * m[4] - m[1] * m[3];
9735
- return [
9736
- m[4] / det,
9737
- -m[1] / det,
9738
- (m[1] * m[5] - m[4] * m[2]) / det,
9739
- -m[3] / det,
9740
- m[0] / det,
9741
- (m[3] * m[2] - m[0] * m[5]) / det
9742
- ];
9743
- }
9744
-
9745
- function clipPolygonAxis(polygon, axis, value, keepGreater) {
9746
- var output = [];
9747
- if (!polygon.length) return output;
9748
- var a = polygon[polygon.length - 1];
9749
- var aInside = keepGreater ? a[axis] >= value - EPS : a[axis] <= value + EPS;
9750
- polygon.forEach(function(b) {
9751
- var bInside = keepGreater ? b[axis] >= value - EPS : b[axis] <= value + EPS;
9752
- if (aInside != bInside) {
9753
- var t = (value - a[axis]) / (b[axis] - a[axis]);
9754
- var p = [
9755
- a[0] + (b[0] - a[0]) * t,
9756
- a[1] + (b[1] - a[1]) * t
9757
- ];
9758
- p[axis] = value;
9759
- output.push(p);
9760
- }
9761
- if (bInside) output.push(b.concat());
9762
- a = b;
9763
- aInside = bInside;
9764
- });
9765
- return output;
9766
- }
9767
-
9768
- function clipRasterPolygonAxis(polygon, axis, value, keepGreater) {
9769
- var output = [];
9770
- if (!polygon.length) return output;
9771
- var a = polygon[polygon.length - 1];
9772
- var aInside = keepGreater ? a[axis] >= value - EPS : a[axis] <= value + EPS;
9773
- polygon.forEach(function(b) {
9774
- var bInside = keepGreater ? b[axis] >= value - EPS : b[axis] <= value + EPS;
9775
- if (aInside != bInside) {
9776
- var t = (value - a[axis]) / (b[axis] - a[axis]);
9777
- var vertex = {
9778
- x: a.x + (b.x - a.x) * t,
9779
- y: a.y + (b.y - a.y) * t,
9780
- sx: a.sx + (b.sx - a.sx) * t,
9781
- sy: a.sy + (b.sy - a.sy) * t,
9782
- lon: interpolateLongitude(a.lon, b.lon, t),
9783
- lat: a.lat + (b.lat - a.lat) * t
9784
- };
9785
- vertex[axis] = value;
9786
- output.push(vertex);
9787
- }
9788
- if (bInside) output.push(copyRasterVertex(b, b.x, b.y));
9789
- a = b;
9790
- 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];
9791
10193
  });
9792
- return output;
9793
- }
9794
-
9795
- function copyRasterVertex(vertex, x, y) {
9796
- return {
9797
- x: x,
9798
- y: y,
9799
- sx: vertex.sx,
9800
- sy: vertex.sy,
9801
- lon: vertex.lon,
9802
- lat: vertex.lat
9803
- };
9804
- }
9805
-
9806
- function interpolateLongitude(a, b, t) {
9807
- var delta = b - a;
9808
- if (delta > 180) delta -= 360;
9809
- if (delta < -180) delta += 360;
9810
- return normalizeLongitude$1(a + delta * t);
10194
+ return bounds;
9811
10195
  }
9812
10196
 
9813
10197
  function getRingArea$2(ring) {
@@ -9863,16 +10247,6 @@
9863
10247
  return Math.abs(a[0] - b[0]) < EPS && Math.abs(a[1] - b[1]) < EPS;
9864
10248
  }
9865
10249
 
9866
- function normalizeLongitude$1(lon) {
9867
- lon = (lon + 180) % 360;
9868
- if (lon < 0) lon += 360;
9869
- return lon - 180;
9870
- }
9871
-
9872
- function clamp$2(value, min, max) {
9873
- return Math.max(min, Math.min(max, value));
9874
- }
9875
-
9876
10250
  var mproj$1 = require$1('mproj');
9877
10251
  registerDymaxionProjections(mproj$1);
9878
10252
  registerButterflyProjections(mproj$1);
@@ -12190,190 +12564,6 @@
12190
12564
  };
12191
12565
  }
12192
12566
 
12193
- // Lightweight hierarchical profiler.
12194
- // Intended for ad-hoc performance work on hot pipelines (e.g. addIntersectionCuts).
12195
- //
12196
- // Usage:
12197
- // import { profileStart, profileEnd, profileWrap, ... } from './utils/mapshaper-profile';
12198
- // profileStart('phase'); doWork(); profileEnd('phase');
12199
- // var result = profileWrap('phase', () => doWork());
12200
- //
12201
- // When disabled (the default) every call short-circuits in ~one comparison; safe
12202
- // to leave in hot code paths. Enable from the CLI with the `-profile` command,
12203
- // from JS with enableProfiling(), or by setting the MAPSHAPER_PROFILE env var.
12204
- //
12205
- // The profiler tracks a stack of currently-open phases so calls can nest. Each
12206
- // unique stack path accumulates ms-elapsed and a call count. profileReport()
12207
- // returns a flat array; formatProfileReport() pretty-prints a tree.
12208
-
12209
- var ENABLED = false;
12210
- var ROOT = makeNode('<root>');
12211
- var STACK = [ROOT];
12212
- var WALL_START = 0;
12213
-
12214
- function makeNode(label) {
12215
- return {
12216
- label: label,
12217
- totalMs: 0,
12218
- selfMs: 0,
12219
- calls: 0,
12220
- childMsAccum: 0,
12221
- children: Object.create(null)
12222
- };
12223
- }
12224
-
12225
- function nowMs() {
12226
- if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {
12227
- // bigint -> number division: precision down to 1 microsecond is fine for our needs
12228
- return Number(process.hrtime.bigint()) / 1e6;
12229
- }
12230
- if (typeof performance !== 'undefined' && performance.now) {
12231
- return performance.now();
12232
- }
12233
- return Date.now();
12234
- }
12235
-
12236
- function enableProfiling() {
12237
- ENABLED = true;
12238
- WALL_START = nowMs();
12239
- }
12240
-
12241
- function disableProfiling() {
12242
- ENABLED = false;
12243
- }
12244
-
12245
- function profileEnabled() {
12246
- return ENABLED;
12247
- }
12248
-
12249
- function profileReset() {
12250
- ROOT = makeNode('<root>');
12251
- STACK = [ROOT];
12252
- WALL_START = ENABLED ? nowMs() : 0;
12253
- }
12254
-
12255
- // Open a new phase. Cheap (~one branch) when disabled.
12256
- function profileStart(label) {
12257
- if (!ENABLED) return;
12258
- var parent = STACK[STACK.length - 1];
12259
- var node = parent.children[label];
12260
- if (!node) {
12261
- node = makeNode(label);
12262
- parent.children[label] = node;
12263
- }
12264
- node._t0 = nowMs();
12265
- STACK.push(node);
12266
- }
12267
-
12268
- function profileEnd(label) {
12269
- if (!ENABLED) return;
12270
- var node = STACK[STACK.length - 1];
12271
- if (label && node.label !== label) {
12272
- // Mismatched labels usually mean an early return forgot to call profileEnd().
12273
- // Walk up the stack until we find a match, closing the intervening frames.
12274
- while (STACK.length > 1 && STACK[STACK.length - 1].label !== label) {
12275
- profileEnd(STACK[STACK.length - 1].label);
12276
- }
12277
- node = STACK[STACK.length - 1];
12278
- if (node.label !== label) return; // give up rather than throw
12279
- }
12280
- var elapsed = nowMs() - node._t0;
12281
- node._t0 = 0;
12282
- node.totalMs += elapsed;
12283
- node.calls += 1;
12284
- STACK.pop();
12285
- var parent = STACK[STACK.length - 1];
12286
- parent.childMsAccum += elapsed;
12287
- }
12288
-
12289
- // Wrap a function call. Re-throws if the callback throws but still closes the
12290
- // phase so the stack stays consistent.
12291
- function profileWrap(label, fn) {
12292
- if (!ENABLED) return fn();
12293
- profileStart(label);
12294
- try {
12295
- return fn();
12296
- } finally {
12297
- profileEnd(label);
12298
- }
12299
- }
12300
-
12301
- // Build a flat tree report: array of {depth, label, totalMs, selfMs, calls}.
12302
- function profileReport() {
12303
- var rows = [];
12304
- function visit(node, depth) {
12305
- if (depth > 0) {
12306
- rows.push({
12307
- depth: depth - 1,
12308
- label: node.label,
12309
- totalMs: node.totalMs,
12310
- selfMs: Math.max(0, node.totalMs - node.childMsAccum),
12311
- calls: node.calls
12312
- });
12313
- }
12314
- var keys = Object.keys(node.children);
12315
- keys.sort(function(a, b) {
12316
- return node.children[b].totalMs - node.children[a].totalMs;
12317
- });
12318
- for (var i = 0; i < keys.length; i++) {
12319
- visit(node.children[keys[i]], depth + 1);
12320
- }
12321
- }
12322
- visit(ROOT, 0);
12323
- return rows;
12324
- }
12325
-
12326
- function profileWallElapsedMs() {
12327
- return ENABLED && WALL_START ? nowMs() - WALL_START : 0;
12328
- }
12329
-
12330
- // Pretty-print the current report as a column-aligned tree.
12331
- function formatProfileReport(opts) {
12332
- var rows = profileReport();
12333
- if (rows.length === 0) return '(profile is empty)';
12334
- opts = opts || {};
12335
- var indent = ' ';
12336
- var lines = [];
12337
- // header
12338
- lines.push(['phase', 'total ms', 'self ms', 'calls'].join('\t'));
12339
- for (var i = 0; i < rows.length; i++) {
12340
- var r = rows[i];
12341
- var label = '';
12342
- for (var j = 0; j < r.depth; j++) label += indent;
12343
- label += r.label;
12344
- lines.push([
12345
- label,
12346
- r.totalMs.toFixed(2),
12347
- r.selfMs.toFixed(2),
12348
- String(r.calls)
12349
- ].join('\t'));
12350
- }
12351
- if (opts.includeWall) {
12352
- lines.push('');
12353
- lines.push('wall elapsed ms: ' + profileWallElapsedMs().toFixed(2));
12354
- }
12355
- return lines.join('\n');
12356
- }
12357
-
12358
- // Honour an env var so users (and CI harnesses) can opt in without code changes.
12359
- if (typeof process !== 'undefined' && process.env && process.env.MAPSHAPER_PROFILE) {
12360
- enableProfiling();
12361
- }
12362
-
12363
- var Profile = /*#__PURE__*/Object.freeze({
12364
- __proto__: null,
12365
- disableProfiling: disableProfiling,
12366
- enableProfiling: enableProfiling,
12367
- formatProfileReport: formatProfileReport,
12368
- profileEnabled: profileEnabled,
12369
- profileEnd: profileEnd,
12370
- profileReport: profileReport,
12371
- profileReset: profileReset,
12372
- profileStart: profileStart,
12373
- profileWallElapsedMs: profileWallElapsedMs,
12374
- profileWrap: profileWrap
12375
- });
12376
-
12377
12567
  // Dissolve arcs that can be merged without affecting topology of layers
12378
12568
  // remove arcs that are not referenced by any layer; remap arc ids
12379
12569
  // in layers. (dataset.arcs is replaced).
@@ -18090,6 +18280,15 @@
18090
18280
  };
18091
18281
  }
18092
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
+
18093
18292
  function blend(a, b) {
18094
18293
  var colors, weights, args;
18095
18294
  if (Array.isArray(a)) {
@@ -22200,9 +22399,12 @@
22200
22399
  // Assumes that input layers are members of the same dataset (and therefore
22201
22400
  // share the same ArcCollection, if layers have paths).
22202
22401
  cmd.mergeLayers = function(layersArg, opts) {
22203
- var layers = layersArg.filter(getFeatureCount); // ignore empty layers
22204
- var merged = {};
22402
+ var layers, merged = {};
22205
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
22206
22408
  if (!layers.length && layersArg.length > 1) {
22207
22409
  layers = layersArg; // merge all-empty layer sets
22208
22410
  }
@@ -28608,6 +28810,9 @@ ${svg}
28608
28810
  if (raster.grid.samples) {
28609
28811
  copy.grid.samples = typedArrayToBuffer(raster.grid.samples);
28610
28812
  }
28813
+ if (raster.grid.coverage) {
28814
+ copy.grid.coverage = typedArrayToBuffer(raster.grid.coverage);
28815
+ }
28611
28816
  }
28612
28817
  if (raster.view) {
28613
28818
  copy.view = Object.assign({}, raster.view);
@@ -33853,8 +34058,9 @@ ${svg}
33853
34058
  error('precision= option should be a positive number');
33854
34059
  }
33855
34060
 
33856
- if (o.raster_type && o.raster_type != 'image' && o.raster_type != 'categorical') {
33857
- 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);
33858
34064
  }
33859
34065
 
33860
34066
  if (o.encoding) {
@@ -35167,8 +35373,15 @@ ${svg}
35167
35373
  .option('percentile-range', {
35168
35374
  describe: '[raster] input percentile range for percentile scaling, e.g. 2,98'
35169
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
+ })
35170
35383
  .option('raster-type', {
35171
- describe: '[raster] image or categorical (default is image)'
35384
+ alias_to: 'interpretation'
35172
35385
  })
35173
35386
  .option('rendition', {
35174
35387
  describe: '[GeoTIFF] import a GeoTIFF rendition: full,overview-1,etc.'
@@ -35615,9 +35828,11 @@ ${svg}
35615
35828
  .option('no-replace', noReplaceOpt);
35616
35829
 
35617
35830
  parser.command('blur')
35618
- // .describe('apply a Gaussian-like blur to projected raster layers')
35831
+ .describe('apply a Gaussian-like blur to a projected raster layer')
35619
35832
  .option('radius', {
35620
- 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)'
35621
35836
  })
35622
35837
  .option('target', targetOpt);
35623
35838
 
@@ -35838,6 +36053,35 @@ ${svg}
35838
36053
  type: 'number'
35839
36054
  });
35840
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
+
35841
36085
  parser.command('dashlines')
35842
36086
  .describe('split lines into sections, with or without a gap')
35843
36087
  .oldAlias('split-lines')
@@ -41239,7 +41483,7 @@ ${svg}
41239
41483
  }
41240
41484
 
41241
41485
  function getRasterInterpretation$1(opts) {
41242
- return opts.raster_type || opts.rasterType || 'image';
41486
+ return opts.interpretation || 'image';
41243
41487
  }
41244
41488
 
41245
41489
  async function selectGeoTIFFImportImage(tiff, sourceImage, opts) {
@@ -41667,7 +41911,7 @@ ${svg}
41667
41911
  }
41668
41912
 
41669
41913
  function getRasterInterpretation(opts) {
41670
- return opts.raster_type || opts.rasterType || 'image';
41914
+ return opts.interpretation || 'image';
41671
41915
  }
41672
41916
 
41673
41917
  async function decodeImage(input, imageType) {
@@ -43012,6 +43256,9 @@ ${svg}
43012
43256
  if (raster.grid.samples) {
43013
43257
  copy.grid.samples = restoreRasterSamples(raster.grid.samples, raster.grid.pixelType);
43014
43258
  }
43259
+ if (raster.grid.coverage) {
43260
+ copy.grid.coverage = new Uint8Array(BinArray.copyToArrayBuffer(raster.grid.coverage));
43261
+ }
43015
43262
  }
43016
43263
  if (raster.view) {
43017
43264
  copy.view = Object.assign({}, raster.view);
@@ -57090,6 +57337,16 @@ ${svg}
57090
57337
  derivation.type == 'categorical';
57091
57338
  }
57092
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
+
57093
57350
  function getProjectedRasterGridBBox(raster, srcCRS, destCRS, optsArg) {
57094
57351
  var opts = optsArg || {};
57095
57352
  var grid = getRasterGrid(raster);
@@ -57768,9 +58025,8 @@ ${svg}
57768
58025
  }
57769
58026
 
57770
58027
  function validateRasterGridForProjection(grid) {
57771
- var t = grid && grid.transform;
57772
58028
  if (!grid || !grid.samples || !grid.bbox) stop$1('Raster layer is missing required projection data');
57773
- if (t && (t[1] !== 0 || t[3] !== 0)) {
58029
+ if (rasterGridIsRotated(grid)) {
57774
58030
  stop$1('Raster reprojection does not support rotated or skewed rasters');
57775
58031
  }
57776
58032
  }
@@ -57902,18 +58158,35 @@ ${svg}
57902
58158
  var color = getNoDataColor(raster, opts);
57903
58159
  var noData = grid.nodata;
57904
58160
  if (color) {
58161
+ warnIfPaintingOverContinuousData(samples, grid, raster, opts);
57905
58162
  fillProjectedRasterColor(samples, bands, color);
57906
58163
  return;
57907
58164
  }
57908
- 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
+ }
57909
58171
  samples.fill(noData);
57910
58172
  }
57911
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
+
57912
58185
  function getNoDataColor(raster, opts) {
57913
58186
  var arg = opts.nodata_color || opts.nodataColor;
57914
58187
  var color;
57915
58188
  if (arg == null || arg === '') {
57916
- 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};
57917
58190
  }
57918
58191
  if (String(arg).toLowerCase() == 'transparent') {
57919
58192
  return {r: 0, g: 0, b: 0, a: 0};
@@ -57938,7 +58211,8 @@ ${svg}
57938
58211
  }
57939
58212
  }
57940
58213
 
57941
- function rasterizeProjectedMesh(srcGrid, destGrid, mesh, sampleMethod) {
58214
+ function rasterizeProjectedMesh(srcGrid, destGrid, mesh, sampleMethodArg) {
58215
+ var sampleMethod = getRasterSamplerContext(srcGrid, destGrid, sampleMethodArg);
57942
58216
  var regionData = mesh.projectedRegions ?
57943
58217
  createProjectedRegionMask(
57944
58218
  destGrid, mesh.projectedRegions, mesh.fillRegionMask) :
@@ -58135,9 +58409,24 @@ ${svg}
58135
58409
  ];
58136
58410
  }
58137
58411
 
58138
- function copyRasterSample(srcGrid, destGrid, sx, sy, dx, dy, sampleMethod, wrapX) {
58139
- if (sampleMethod == 'bilinear') {
58140
- 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);
58141
58430
  } else {
58142
58431
  copyNearestRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX);
58143
58432
  }
@@ -58150,7 +58439,9 @@ ${svg}
58150
58439
  var srcY = clamp$1(Math.floor(sy), 0, srcGrid.height - 1);
58151
58440
  var src = (srcY * srcGrid.width + srcX) * srcGrid.bands;
58152
58441
  var dest = (dy * destGrid.width + dx) * destGrid.bands;
58153
- 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;
58154
58445
  for (var band = 0; band < srcGrid.bands; band++) {
58155
58446
  destGrid.samples[dest + band] = srcGrid.samples[src + band];
58156
58447
  }
@@ -58158,7 +58449,7 @@ ${svg}
58158
58449
  if (destGrid.coverage) destGrid.coverage[dy * destGrid.width + dx] = 1;
58159
58450
  }
58160
58451
 
58161
- function copyBilinearRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX) {
58452
+ function copyBilinearRasterSample(srcGrid, destGrid, sx, sy, dx, dy, sampler, wrapX) {
58162
58453
  if (wrapX) sx = modulo(sx, srcGrid.width);
58163
58454
  var srcX = wrapX ?
58164
58455
  modulo(Math.floor(sx - 0.5), srcGrid.width) :
@@ -58174,79 +58465,81 @@ ${svg}
58174
58465
  var src01 = (srcY2 * srcGrid.width + srcX) * srcGrid.bands;
58175
58466
  var src11 = (srcY2 * srcGrid.width + srcX2) * srcGrid.bands;
58176
58467
  var dest = (dy * destGrid.width + dx) * destGrid.bands;
58177
- if (!srcGrid.coverage) {
58178
- 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);
58179
58470
  if (destGrid.coverage) destGrid.coverage[dy * destGrid.width + dx] = 1;
58180
58471
  return;
58181
58472
  }
58182
- 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)) {
58183
58474
  if (destGrid.coverage) destGrid.coverage[dy * destGrid.width + dx] = 1;
58184
58475
  }
58185
58476
  }
58186
58477
 
58187
- function copyBilinearRasterSampleFast(srcGrid, destGrid, src00, src10, src01, src11, dest, tx, ty) {
58478
+ function copyBilinearRasterSampleFast(srcGrid, destGrid, src00, src10, src01, src11, dest, tx, ty, round) {
58188
58479
  if (srcGrid.bands == 3) {
58189
- 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);
58190
58481
  } else if (srcGrid.bands == 4) {
58191
- 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);
58192
58483
  } else {
58193
- 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);
58194
58485
  }
58195
58486
  if (destGrid.bands > srcGrid.bands) destGrid.samples[dest + 3] = 255;
58196
58487
  }
58197
58488
 
58198
- function copyBilinearRgbSample(src, destArr, src00, src10, src01, src11, dest, tx, ty) {
58199
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 0, tx, ty);
58200
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, 1, tx, ty);
58201
- 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);
58202
58493
  }
58203
58494
 
58204
- function copyBilinearRgbaSample(src, destArr, src00, src10, src01, src11, dest, tx, ty) {
58205
- copyBilinearRgbSample(src, destArr, src00, src10, src01, src11, dest, tx, ty);
58206
- 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);
58207
58498
  }
58208
58499
 
58209
- 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) {
58210
58501
  for (var band = 0; band < bands; band++) {
58211
- copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, band, tx, ty);
58502
+ copyBilinearBand(src, destArr, src00, src10, src01, src11, dest, band, tx, ty, round);
58212
58503
  }
58213
58504
  }
58214
58505
 
58215
- 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) {
58216
58507
  var a = src[src00 + band] * (1 - tx) + src[src10 + band] * tx;
58217
58508
  var b = src[src01 + band] * (1 - tx) + src[src11 + band] * tx;
58218
- destArr[dest + band] = Math.round(a * (1 - ty) + b * ty);
58219
- }
58220
-
58221
- function copyBilinearRasterSampleWithCoverage(srcGrid, destGrid, srcX, srcY, srcX2, srcY2, src00, src10, src01, src11, dest, tx, ty) {
58222
- var coords = [
58223
- [srcX, srcY, src00, (1 - tx) * (1 - ty)],
58224
- [srcX2, srcY, src10, tx * (1 - ty)],
58225
- [srcX, srcY2, src01, (1 - tx) * ty],
58226
- [srcX2, srcY2, src11, tx * ty]
58227
- ];
58228
- var total = 0;
58229
- 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;
58230
58530
  for (var band = 0; band < srcGrid.bands; band++) {
58231
58531
  val = 0;
58232
- total = 0;
58233
- for (var i = 0; i < coords.length; i++) {
58234
- item = coords[i];
58235
- if (item[3] <= 0 || !rasterSourcePixelIsCovered(srcGrid, item[0], item[1])) continue;
58236
- val += srcGrid.samples[item[2] + band] * item[3];
58237
- total += item[3];
58238
- }
58239
- if (total <= 0) return false;
58240
- 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;
58241
58538
  }
58242
58539
  if (destGrid.bands > srcGrid.bands) destGrid.samples[dest + 3] = 255;
58243
58540
  return true;
58244
58541
  }
58245
58542
 
58246
- function rasterSourcePixelIsCovered(grid, x, y) {
58247
- return !grid.coverage || grid.coverage[y * grid.width + x] > 0;
58248
- }
58249
-
58250
58543
  function timeStart(timing, name) {
58251
58544
  if (!timing) return;
58252
58545
  timing[name + 'Start'] = getTimer();
@@ -58290,6 +58583,33 @@ ${svg}
58290
58583
  projectRasterGridForward: projectRasterGridForward
58291
58584
  });
58292
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
+
58293
58613
  function splitPolygonFrameChords(dataset, bounds) {
58294
58614
  var editor = new DatasetEditor(dataset);
58295
58615
  dataset.layers.forEach(function(lyr) {
@@ -62045,6 +62365,529 @@ ${svg}
62045
62365
  // message('[comment]', opts.message);
62046
62366
  }; // no-op, so -comment doesn't trigger a parsing error
62047
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
+
62048
62891
  cmd.dashlines = function(lyr, dataset, opts) {
62049
62892
  var crs = getDatasetCRS(dataset);
62050
62893
  var defs = getStashedVar('defs');
@@ -63593,11 +64436,19 @@ ${svg}
63593
64436
  //
63594
64437
  cmd.dissolve = function(arg1, arg2, opts) {
63595
64438
  if (Array.isArray(arg1)) {
64439
+ arg1.forEach(requireDissolvableLayer);
63596
64440
  return dissolveLayers(arg1, arg2, opts);
63597
64441
  }
64442
+ requireDissolvableLayer(arg1);
63598
64443
  return dissolveSingleLayer(arg1, arg2, opts);
63599
64444
  };
63600
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
+
63601
64452
  function dissolveLayers(layers, dataset, optsArg) {
63602
64453
  var opts = utils.extend({}, optsArg);
63603
64454
  if (opts.field) opts.fields = [opts.field]; // support old "field" parameter
@@ -72671,7 +73522,7 @@ ${svg}
72671
73522
  }
72672
73523
  return [
72673
73524
  'affine', 'alpha-shapes', 'blur', 'buffer', 'calc', 'check-geometry',
72674
- 'classify', 'clean', 'clip', 'cluster', 'dashlines', 'data-fill',
73525
+ 'classify', 'clean', 'clip', 'cluster', 'contours', 'dashlines', 'data-fill',
72675
73526
  'densify', 'dissolve', 'dissolve2', 'divide', 'dots', 'each', 'erase',
72676
73527
  'explode', 'filter',
72677
73528
  'filter-detail', 'filter-fields', 'filter-geom', 'filter-islands',
@@ -72848,6 +73699,9 @@ ${svg}
72848
73699
  // } else if (name == 'comment') {
72849
73700
  // // no-op
72850
73701
 
73702
+ } else if (name == 'contours') {
73703
+ outputLayers = applyCommandToEachLayer(cmd.contours, targetLayers, targetDataset, opts);
73704
+
72851
73705
  } else if (name == 'dashlines') {
72852
73706
  applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
72853
73707
 
@@ -73332,7 +74186,7 @@ ${svg}
73332
74186
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
73333
74187
  }
73334
74188
 
73335
- var version = "0.7.48";
74189
+ var version = "0.7.49";
73336
74190
 
73337
74191
  // Parse command line args into commands and run them
73338
74192
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -73996,10 +74850,10 @@ ${svg}
73996
74850
 
73997
74851
  var BOX_BLUR_PASSES = 3;
73998
74852
 
73999
- function blurRasterGrid(raster, optsArg) {
74853
+ function blurRasterGrid(raster, optsArg, crs) {
74000
74854
  var opts = optsArg || {};
74001
74855
  var grid = getRasterGrid(raster);
74002
- var radius = getBlurRadius(opts);
74856
+ var radius = getBlurRadius(grid, opts, crs);
74003
74857
  var sigma = radius / 2;
74004
74858
  var boxes = getGaussianBoxWidths(sigma, BOX_BLUR_PASSES);
74005
74859
  var samples = new grid.samples.constructor(grid.samples.length);
@@ -74023,20 +74877,48 @@ ${svg}
74023
74877
  });
74024
74878
  }
74025
74879
 
74026
- 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) {
74027
74884
  var arg = opts.radius;
74028
- var radius;
74029
- if (arg == null || arg === '') stop$1('Missing blur radius');
74030
- if (typeof arg == 'string') {
74031
- 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;
74032
74896
  }
74033
- radius = Number(arg);
74034
74897
  if (!(radius > 0 && isFinite(radius))) {
74035
- stop$1('Expected radius= to be a positive pixel value');
74898
+ stop$1('Expected a positive blur radius, received:', arg);
74036
74899
  }
74037
74900
  return radius;
74038
74901
  }
74039
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
+
74040
74922
  function validateBlurGrid(grid) {
74041
74923
  if (!grid || !grid.samples || !grid.width || !grid.height || !grid.bands) {
74042
74924
  stop$1('Expected a raster grid');
@@ -74185,8 +75067,8 @@ ${svg}
74185
75067
 
74186
75068
  function copyUnpremultipliedChannelToBand(channel, alpha, samples, grid, band) {
74187
75069
  var bands = grid.bands;
74188
- var isFloat = samples instanceof Float32Array || samples instanceof Float64Array;
74189
- var range = isFloat ? null : getTypedArrayRange(samples);
75070
+ var isFloat = rasterSamplesAreFloat(samples);
75071
+ var range = isFloat ? null : getSampleArrayRange(samples);
74190
75072
  var maxAlpha = getAlphaMaxValue(samples);
74191
75073
  var val, a;
74192
75074
  for (var i = 0, j = band; i < channel.length; i++, j += bands) {
@@ -74198,8 +75080,8 @@ ${svg}
74198
75080
 
74199
75081
  function copyChannelToBand(channel, samples, grid, band) {
74200
75082
  var bands = grid.bands;
74201
- var isFloat = samples instanceof Float32Array || samples instanceof Float64Array;
74202
- var range = isFloat ? null : getTypedArrayRange(samples);
75083
+ var isFloat = rasterSamplesAreFloat(samples);
75084
+ var range = isFloat ? null : getSampleArrayRange(samples);
74203
75085
  var val;
74204
75086
  for (var i = 0, j = band; i < channel.length; i++, j += bands) {
74205
75087
  val = channel[i];
@@ -74208,62 +75090,47 @@ ${svg}
74208
75090
  }
74209
75091
 
74210
75092
  function getRasterBlurWeights(grid) {
74211
- if (!grid.coverage && grid.nodata == null) return null;
75093
+ if (!rasterGridHasInvalidPixels(grid)) return null;
74212
75094
  var weights = new Float32Array(grid.width * grid.height);
74213
75095
  for (var i = 0; i < weights.length; i++) {
74214
- weights[i] = rasterBlurPixelIsValid(grid, i) ? 1 : 0;
75096
+ weights[i] = rasterPixelIsValid(grid, i) ? 1 : 0;
74215
75097
  }
74216
75098
  return weights;
74217
75099
  }
74218
75100
 
74219
- function rasterBlurPixelIsValid(grid, pixelId) {
74220
- var off, n;
74221
- if (grid.coverage && grid.coverage[pixelId] === 0) return false;
74222
- if (grid.nodata == null) return true;
74223
- off = pixelId * grid.bands;
74224
- n = Math.min(grid.bands, 3);
74225
- for (var i = 0; i < n; i++) {
74226
- if (grid.samples[off + i] != grid.nodata) return true;
74227
- }
74228
- return false;
74229
- }
74230
-
74231
75101
  function getAlphaMaxValue(samples) {
74232
- if (samples instanceof Float32Array || samples instanceof Float64Array) return 1;
74233
- return getTypedArrayRange(samples).max;
74234
- }
74235
-
74236
- function getTypedArrayRange(arr) {
74237
- if (arr instanceof Uint8Array || arr instanceof Uint8ClampedArray) return {min: 0, max: 255};
74238
- if (arr instanceof Int8Array) return {min: -128, max: 127};
74239
- if (arr instanceof Uint16Array) return {min: 0, max: 65535};
74240
- if (arr instanceof Int16Array) return {min: -32768, max: 32767};
74241
- if (arr instanceof Uint32Array) return {min: 0, max: 4294967295};
74242
- if (arr instanceof Int32Array) return {min: -2147483648, max: 2147483647};
74243
- return {min: -Infinity, max: Infinity};
75102
+ if (rasterSamplesAreFloat(samples)) return 1;
75103
+ return getSampleArrayRange(samples).max;
74244
75104
  }
74245
75105
 
74246
75106
  function clamp(val, min, max) {
74247
75107
  return val < min ? min : val > max ? max : val;
74248
75108
  }
74249
75109
 
75110
+ var RasterBlur = /*#__PURE__*/Object.freeze({
75111
+ __proto__: null,
75112
+ blurRasterGrid: blurRasterGrid,
75113
+ getBlurRadius: getBlurRadius
75114
+ });
75115
+
74250
75116
  cmd.blur = blurRasterLayers;
74251
75117
 
74252
75118
  function blurRasterLayers(layers, dataset, optsArg) {
74253
75119
  var opts = optsArg || {};
75120
+ var crs = getDatasetCRS(dataset);
74254
75121
  requireProjectedDataset(dataset);
74255
75122
  layers.forEach(function(lyr) {
74256
75123
  if (!layerHasRaster(lyr)) {
74257
75124
  stop$1('Command requires a raster layer');
74258
75125
  }
74259
- blurRasterLayer(lyr, opts);
75126
+ blurRasterLayer(lyr, opts, crs);
74260
75127
  });
74261
75128
  }
74262
75129
 
74263
- function blurRasterLayer(lyr, opts) {
75130
+ function blurRasterLayer(lyr, opts, crs) {
74264
75131
  var raster = lyr.raster;
74265
75132
  noteLayerWillChange(lyr, {operation: 'blurRasterLayer', unit: 'raster'});
74266
- raster.grid = blurRasterGrid(raster, opts);
75133
+ raster.grid = blurRasterGrid(raster, opts, crs);
74267
75134
  raster.view = raster.view || {};
74268
75135
  delete raster.view.scalingStats;
74269
75136
  if (runningInBrowser()) {
@@ -75167,6 +76034,7 @@ ${svg}
75167
76034
  Catalog$1,
75168
76035
  ClipErase,
75169
76036
  ClipPoints,
76037
+ ColorUtils,
75170
76038
  Colorizer,
75171
76039
  CustomProjections,
75172
76040
  Dymaxion,
@@ -75214,6 +76082,9 @@ ${svg}
75214
76082
  Lines,
75215
76083
  Logging,
75216
76084
  Profile,
76085
+ RasterBlur,
76086
+ RasterContours,
76087
+ RasterGrid,
75217
76088
  RasterReprojection,
75218
76089
  RasterUtils,
75219
76090
  Merging,