mapshaper 0.7.49 → 0.7.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/www/mapshaper.js CHANGED
@@ -336,7 +336,7 @@
336
336
  return arr;
337
337
  }
338
338
 
339
- function repeat(times, func) {
339
+ function repeat$1(times, func) {
340
340
  var values = [],
341
341
  val;
342
342
  for (var i=0; i<times; i++) {
@@ -1115,7 +1115,7 @@
1115
1115
  parseIntlNumber, parseNumber: parseNumber$1, parseString, pickOne, pluck,
1116
1116
  pluralSuffix, promisify,
1117
1117
  quicksort: quicksort$1, quicksortPartition,
1118
- range, regexEscape, reorderArray: reorderArray$1, repeat, repeatString,
1118
+ range, regexEscape, reorderArray: reorderArray$1, repeat: repeat$1, repeatString,
1119
1119
  replaceArray, rpad, rtrim,
1120
1120
  shuffle, some, sortArrayIndex, sortOn, splitLines, sum: sum$1,
1121
1121
  toArray: toArray$1, toBuffer, trim, trimQuotes,
@@ -1409,7 +1409,7 @@
1409
1409
  // path/bound helpers, forming a cycle).
1410
1410
 
1411
1411
 
1412
- function layerHasGeometry(lyr) {
1412
+ function layerHasGeometry$1(lyr) {
1413
1413
  return layerHasPaths(lyr) || layerHasPoints(lyr);
1414
1414
  }
1415
1415
 
@@ -2054,7 +2054,7 @@
2054
2054
  // also consider using ellipsoidal formulas where greater accuracy might be important.
2055
2055
  var R$3 = WGS84.SEMIMAJOR_AXIS;
2056
2056
  var D2R$8 = Math.PI / 180;
2057
- var R2D$7 = 180 / Math.PI;
2057
+ var R2D$8 = 180 / Math.PI;
2058
2058
 
2059
2059
  // Equirectangular projection
2060
2060
  function degreesToMeters(deg) {
@@ -2336,7 +2336,7 @@
2336
2336
  __proto__: null,
2337
2337
  D2R: D2R$8,
2338
2338
  R: R$3,
2339
- R2D: R2D$7,
2339
+ R2D: R2D$8,
2340
2340
  bearing: bearing,
2341
2341
  bearing2D: bearing2D,
2342
2342
  containsBounds: containsBounds,
@@ -6087,7 +6087,7 @@
6087
6087
  insertFieldValues: insertFieldValues,
6088
6088
  isolateLayer: isolateLayer,
6089
6089
  layerHasAttributeData: layerHasAttributeData,
6090
- layerHasGeometry: layerHasGeometry,
6090
+ layerHasGeometry: layerHasGeometry$1,
6091
6091
  layerHasNonNullData: layerHasNonNullData,
6092
6092
  layerHasNonNullShapes: layerHasNonNullShapes,
6093
6093
  layerHasPaths: layerHasPaths,
@@ -6572,7 +6572,7 @@
6572
6572
 
6573
6573
 
6574
6574
  var D2R$7 = Math.PI / 180;
6575
- var R2D$6 = 180 / Math.PI;
6575
+ var R2D$7 = 180 / Math.PI;
6576
6576
  var EPS$3 = 1e-12;
6577
6577
 
6578
6578
  // Build a forward-only spherical polyhedral projection.
@@ -6790,7 +6790,7 @@
6790
6790
  config.rotation[2] * D2R$7,
6791
6791
  true
6792
6792
  );
6793
- return [normalizeLongitude$1(q[0] * R2D$6 + lon0), q[1] * R2D$6];
6793
+ return [normalizeLongitude$1(q[0] * R2D$7 + lon0), q[1] * R2D$7];
6794
6794
  });
6795
6795
  var paths = splitPathAtAntimeridian$3(
6796
6796
  interpolateGreatCircle(endpoints[0], endpoints[1], 0.05)
@@ -7094,7 +7094,7 @@
7094
7094
  var av = degreesToVector$2(a[0], a[1]);
7095
7095
  var bv = degreesToVector$2(b[0], b[1]);
7096
7096
  var angle = Math.acos(clamp$2(dot$2(av, bv), -1, 1));
7097
- var n = Math.max(1, Math.ceil(angle * R2D$6 / interval));
7097
+ var n = Math.max(1, Math.ceil(angle * R2D$7 / interval));
7098
7098
  var sinAngle = Math.sin(angle);
7099
7099
  var points = [];
7100
7100
  for (var i = 0; i <= n; i++) {
@@ -7167,8 +7167,8 @@
7167
7167
 
7168
7168
  function vectorToDegrees$2(p) {
7169
7169
  return [
7170
- Math.atan2(p[1], p[0]) * R2D$6,
7171
- Math.asin(clamp$2(p[2], -1, 1)) * R2D$6
7170
+ Math.atan2(p[1], p[0]) * R2D$7,
7171
+ Math.asin(clamp$2(p[2], -1, 1)) * R2D$7
7172
7172
  ];
7173
7173
  }
7174
7174
 
@@ -7236,7 +7236,7 @@
7236
7236
 
7237
7237
 
7238
7238
  var D2R$6 = Math.PI / 180;
7239
- var R2D$5 = 180 / Math.PI;
7239
+ var R2D$6 = 180 / Math.PI;
7240
7240
  var SQRT3$2 = Math.sqrt(3);
7241
7241
  var GRAY_Z = Math.sqrt(5 + 2 * Math.sqrt(5)) / Math.sqrt(15);
7242
7242
  var GRAY_EL = Math.sqrt(8) / Math.sqrt(5 + Math.sqrt(5));
@@ -7297,12 +7297,12 @@
7297
7297
  xy.y = p[1];
7298
7298
  }
7299
7299
  };
7300
- P.__projection_topology = engine.getTopology(P.lam0 * R2D$5);
7300
+ P.__projection_topology = engine.getTopology(P.lam0 * R2D$6);
7301
7301
  P.__projected_outline = engine.outline;
7302
7302
  }
7303
7303
 
7304
7304
  function createAiroceanFaces() {
7305
- var theta = Math.atan(0.5) * R2D$5;
7305
+ var theta = Math.atan(0.5) * R2D$6;
7306
7306
  var vertices = [[0, 90], [0, -90]];
7307
7307
  var faces;
7308
7308
  var sites;
@@ -7435,8 +7435,8 @@
7435
7435
 
7436
7436
  function vectorToDegrees$1(p) {
7437
7437
  return [
7438
- Math.atan2(p[1], p[0]) * R2D$5,
7439
- Math.asin(Math.max(-1, Math.min(1, p[2]))) * R2D$5
7438
+ Math.atan2(p[1], p[0]) * R2D$6,
7439
+ Math.asin(Math.max(-1, Math.min(1, p[2]))) * R2D$6
7440
7440
  ];
7441
7441
  }
7442
7442
 
@@ -7485,7 +7485,7 @@
7485
7485
  */
7486
7486
 
7487
7487
  var D2R$5 = Math.PI / 180;
7488
- var R2D$4 = 180 / Math.PI;
7488
+ var R2D$5 = 180 / Math.PI;
7489
7489
 
7490
7490
  function createCahillKeyesRaw(mg) {
7491
7491
  return createCahillKeyesTransform(mg, false);
@@ -7497,12 +7497,12 @@
7497
7497
 
7498
7498
  return function(lambda, phi) {
7499
7499
  if (faceOnly) {
7500
- var lon = lambda * R2D$4;
7500
+ var lon = lambda * R2D$5;
7501
7501
  var side = lon < 0 ? -1 : lon > 0 ? 1 : 0;
7502
- var local = mp2xy(Math.abs(lon), Math.abs(phi * R2D$4));
7502
+ var local = mp2xy(Math.abs(lon), Math.abs(phi * R2D$5));
7503
7503
  return [local[0], side * local[1]];
7504
7504
  }
7505
- var res = ll2mp(lambda * R2D$4, phi * R2D$4);
7505
+ var res = ll2mp(lambda * R2D$5, phi * R2D$5);
7506
7506
  var xy = mp2xy(res[0], res[1]);
7507
7507
  var p = [xy[0], res[2] * xy[1]];
7508
7508
  return mj2g(p, res[3]);
@@ -7999,7 +7999,7 @@
7999
7999
 
8000
8000
 
8001
8001
  var D2R$3 = Math.PI / 180;
8002
- var R2D$3 = 180 / Math.PI;
8002
+ var R2D$4 = 180 / Math.PI;
8003
8003
  var RADIAL_BOUNDARY_STRENGTH = 1;
8004
8004
  var OCTAHEDRON = createOctahedron();
8005
8005
  var BUTTERFLY_PARENTS = [-1, 0, 0, 1, 0, 1, 4, 5];
@@ -8054,7 +8054,7 @@
8054
8054
  xy.y = p[1];
8055
8055
  }
8056
8056
  };
8057
- P.__projection_topology = engine.getTopology(P.lam0 * R2D$3);
8057
+ P.__projection_topology = engine.getTopology(P.lam0 * R2D$4);
8058
8058
  if (engine.removeOutlineExtremeConnectors) {
8059
8059
  P.__remove_outline_extreme_connectors = true;
8060
8060
  }
@@ -8374,8 +8374,8 @@
8374
8374
 
8375
8375
  function vectorToDegrees(p) {
8376
8376
  return [
8377
- Math.atan2(p[1], p[0]) * R2D$3,
8378
- Math.asin(Math.max(-1, Math.min(1, p[2]))) * R2D$3
8377
+ Math.atan2(p[1], p[0]) * R2D$4,
8378
+ Math.asin(Math.max(-1, Math.min(1, p[2]))) * R2D$4
8379
8379
  ];
8380
8380
  }
8381
8381
 
@@ -8430,7 +8430,7 @@
8430
8430
 
8431
8431
 
8432
8432
  var D2R$2 = Math.PI / 180;
8433
- var R2D$2 = 180 / Math.PI;
8433
+ var R2D$3 = 180 / Math.PI;
8434
8434
  var HALF_PI = Math.PI / 2;
8435
8435
  var SQRT2$1 = Math.sqrt(2);
8436
8436
  var SQRT3$1 = Math.sqrt(3);
@@ -8487,7 +8487,7 @@
8487
8487
  xy.x = p[0];
8488
8488
  xy.y = p[1];
8489
8489
  };
8490
- P.__projection_topology = engine.getTopology(P.lam0 * R2D$2);
8490
+ P.__projection_topology = engine.getTopology(P.lam0 * R2D$3);
8491
8491
  P.__projected_outline = engine.outline;
8492
8492
  }
8493
8493
 
@@ -8577,8 +8577,8 @@
8577
8577
  boundary: sector.sphericalBoundary.map(function(p) {
8578
8578
  var q = fromCanonical(p[0], p[1], orientation);
8579
8579
  return [
8580
- normalizeLongitude$1(q[0] * R2D$2 + lon0),
8581
- q[1] * R2D$2
8580
+ normalizeLongitude$1(q[0] * R2D$3 + lon0),
8581
+ q[1] * R2D$3
8582
8582
  ];
8583
8583
  })
8584
8584
  };
@@ -9165,8 +9165,8 @@
9165
9165
  var y = edge[0][1] + (edge[1][1] - edge[0][1]) * t;
9166
9166
  var p = inverse(x * EDGE_SCALE, y * EDGE_SCALE);
9167
9167
  path.push([
9168
- normalizeLongitude$1(p[0] * R2D$2 + lon0),
9169
- p[1] * R2D$2
9168
+ normalizeLongitude$1(p[0] * R2D$3 + lon0),
9169
+ p[1] * R2D$3
9170
9170
  ]);
9171
9171
  }
9172
9172
  return memo.concat(splitPathAtAntimeridian$2(path));
@@ -9462,11 +9462,11 @@
9462
9462
 
9463
9463
 
9464
9464
  var D2R$1 = Math.PI / 180;
9465
- var R2D$1 = 180 / Math.PI;
9465
+ var R2D$2 = 180 / Math.PI;
9466
9466
  var SQRT2 = Math.sqrt(2);
9467
9467
  var SQRT3 = Math.sqrt(3);
9468
9468
  var ASIN_ONE_THIRD = Math.asin(1 / 3);
9469
- var MARKLEY_LATITUDE = Math.acos(1 / 3) * 0.5 * R2D$1;
9469
+ var MARKLEY_LATITUDE = Math.acos(1 / 3) * 0.5 * R2D$2;
9470
9470
  var LAYOUT_PERIOD = 8;
9471
9471
  var MARKLEY_LAYOUT_PHASE = -2;
9472
9472
  var CALM_LAYOUT_PHASE = -1.5;
@@ -9506,9 +9506,9 @@
9506
9506
 
9507
9507
  var TETRAHEDRON_VERTICES = [
9508
9508
  [0, 90],
9509
- [-180, -ASIN_ONE_THIRD * R2D$1],
9510
- [-60, -ASIN_ONE_THIRD * R2D$1],
9511
- [60, -ASIN_ONE_THIRD * R2D$1]
9509
+ [-180, -ASIN_ONE_THIRD * R2D$2],
9510
+ [-60, -ASIN_ONE_THIRD * R2D$2],
9511
+ [60, -ASIN_ONE_THIRD * R2D$2]
9512
9512
  ];
9513
9513
 
9514
9514
  var TETRAHEDRON_FACES = [
@@ -9562,7 +9562,7 @@
9562
9562
  xy.x = p[0];
9563
9563
  xy.y = p[1];
9564
9564
  };
9565
- P.__projection_topology = engine.getTopology(P.lam0 * R2D$1);
9565
+ P.__projection_topology = engine.getTopology(P.lam0 * R2D$2);
9566
9566
  P.__projected_outline = engine.outline;
9567
9567
  P.__graticule_precision = 1;
9568
9568
  }
@@ -9787,7 +9787,7 @@
9787
9787
  var boundary = face.map(function(p) {
9788
9788
  var q = rotateSphericalRadians(
9789
9789
  p[0] * D2R$1, p[1] * D2R$1, rotation, true);
9790
- return [normalizeLongitude$1(q[0] * R2D$1), q[1] * R2D$1];
9790
+ return [normalizeLongitude$1(q[0] * R2D$2), q[1] * R2D$2];
9791
9791
  });
9792
9792
  boundary.push(boundary[0].concat());
9793
9793
  return {id: id, boundary: boundary};
@@ -9905,7 +9905,7 @@
9905
9905
  stats.failed_count++;
9906
9906
  continue;
9907
9907
  }
9908
- path.push([normalizeLongitude$1(p[0] * R2D$1 + lon0), p[1] * R2D$1]);
9908
+ path.push([normalizeLongitude$1(p[0] * R2D$2 + lon0), p[1] * R2D$2]);
9909
9909
  }
9910
9910
  return memo.concat(splitPathAtAntimeridian$1(path));
9911
9911
  }, []).map(function(path) {
@@ -10447,6 +10447,40 @@
10447
10447
  };
10448
10448
  }
10449
10449
 
10450
+ // Parse a CRS string that may well be unusable, without the failure reaching
10451
+ // the user: returns null instead of stopping. This is for CRS metadata read out
10452
+ // of a file, where mapshaper is guessing and the caller has a fallback. (In the
10453
+ // web app a stop() shows a modal popup as it throws, which is too much for a
10454
+ // guess that did not come off, and interrupts whatever the user was doing.)
10455
+ function tryParseCrsString(str) {
10456
+ var revertLogging = getLoggingSetter();
10457
+ var crs = null;
10458
+ setLoggingForCLI();
10459
+ try {
10460
+ crs = parseCrsString$1(str);
10461
+ } catch(e) {
10462
+ // an unparseable string is one of the expected outcomes here
10463
+ }
10464
+ revertLogging();
10465
+ return crs;
10466
+ }
10467
+
10468
+ // The WKT counterpart of tryParseCrsString(), for a projection read out of a
10469
+ // .prj or .aux.xml file, in either WKT dialect. Returns the proj4 equivalent of
10470
+ // the WKT, or null.
10471
+ function tryParseWktToProj(str) {
10472
+ var revertLogging = getLoggingSetter();
10473
+ var proj = null;
10474
+ setLoggingForCLI();
10475
+ try {
10476
+ proj = wktToProj(str);
10477
+ } catch(e) {
10478
+ // WKT that cannot be converted is one of the expected outcomes here
10479
+ }
10480
+ revertLogging();
10481
+ return proj || null;
10482
+ }
10483
+
10450
10484
  function parseCrsString$1(str) {
10451
10485
  var defn = getProjDefn(str); // defn is a string or a Proj object
10452
10486
  var P;
@@ -10701,6 +10735,8 @@
10701
10735
  setDatasetCrsInfo: setDatasetCrsInfo,
10702
10736
  setProjectionLoader: setProjectionLoader,
10703
10737
  toLngLat: toLngLat,
10738
+ tryParseCrsString: tryParseCrsString,
10739
+ tryParseWktToProj: tryParseWktToProj,
10704
10740
  wkt1ToProj: wkt1ToProj,
10705
10741
  wkt2ToProj: wkt2ToProj,
10706
10742
  wktToProj: wktToProj
@@ -12867,7 +12903,7 @@
12867
12903
 
12868
12904
  function datasetHasGeometry(dataset) {
12869
12905
  return utils.some(dataset.layers, function(lyr) {
12870
- return layerHasGeometry(lyr);
12906
+ return layerHasGeometry$1(lyr);
12871
12907
  });
12872
12908
  }
12873
12909
 
@@ -13110,6 +13146,67 @@
13110
13146
  toLowerCaseExtension: toLowerCaseExtension
13111
13147
  });
13112
13148
 
13149
+ // GDAL's "persistent auxiliary metadata" sidecar, named after the whole raster
13150
+ // filename rather than replacing its extension (world.tif -> world.tif.aux.xml).
13151
+ // Unlike the .prj file that accompanies a shapefile, this is a sidecar that
13152
+ // GDAL-based software (QGIS included) actually consults for a GeoTIFF's CRS, so
13153
+ // it is where mapshaper puts a projection that GeoTIFF itself cannot describe.
13154
+ var AUX_EXT = '.aux.xml';
13155
+
13156
+ function getAuxFilename(filename) {
13157
+ return filename + AUX_EXT;
13158
+ }
13159
+
13160
+ function isAuxFilename(filename) {
13161
+ return /\.aux\.xml$/i.test(filename || '');
13162
+ }
13163
+
13164
+ // Returns the raster a sidecar belongs to: world.tif.aux.xml -> world.tif
13165
+ function getAuxSourceFilename(filename) {
13166
+ return String(filename).replace(/\.aux\.xml$/i, '');
13167
+ }
13168
+
13169
+ function formatAuxXml(wkt) {
13170
+ return '<PAMDataset>\n <SRS>' + escapeXml(wkt) + '</SRS>\n</PAMDataset>\n';
13171
+ }
13172
+
13173
+ // The CRS a sidecar carries, or null. Everything else GDAL writes to these
13174
+ // files -- statistics, band metadata, overviews -- describes data that
13175
+ // mapshaper reads from the raster itself.
13176
+ function parseAuxCrsString(content) {
13177
+ var xml = typeof content == 'string' ? content :
13178
+ content ? decodeString(content, 'utf8') : '';
13179
+ var match = /<SRS\b[^>]*>([\s\S]*?)<\/SRS>/i.exec(xml);
13180
+ var srs = match ? unescapeXml(match[1]).trim() : '';
13181
+ return srs || null;
13182
+ }
13183
+
13184
+ // GDAL states the SRS in WKT, but a hand-written sidecar may hold an EPSG code
13185
+ // or a proj4 definition instead, which mapshaper can use as they are.
13186
+ function auxCrsIsWkt(str) {
13187
+ return /^\s*[A-Z_]+\[/i.test(str || '');
13188
+ }
13189
+
13190
+ function escapeXml(str) {
13191
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
13192
+ }
13193
+
13194
+ function unescapeXml(str) {
13195
+ return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
13196
+ .replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
13197
+ }
13198
+
13199
+ var GeoTIFFAux = /*#__PURE__*/Object.freeze({
13200
+ __proto__: null,
13201
+ AUX_EXT: AUX_EXT,
13202
+ auxCrsIsWkt: auxCrsIsWkt,
13203
+ formatAuxXml: formatAuxXml,
13204
+ getAuxFilename: getAuxFilename,
13205
+ getAuxSourceFilename: getAuxSourceFilename,
13206
+ isAuxFilename: isAuxFilename,
13207
+ parseAuxCrsString: parseAuxCrsString
13208
+ });
13209
+
13113
13210
  // Constants shared between the .msx (mapshaper snapshot) writer in
13114
13211
  // mapshaper-pack.mjs and the file-type registry in
13115
13212
  // io/mapshaper-file-types.mjs. Living here lets file-types reference the
@@ -13123,7 +13220,10 @@
13123
13220
  function guessInputFileType(file) {
13124
13221
  var ext = getFileExtension(file || '').toLowerCase(),
13125
13222
  type = null;
13126
- if (ext == 'dbf' || ext == 'shp' || ext == 'kml' || ext == 'svg' || ext == 'fgb' || ext == 'gpkg' || ext == 'png') {
13223
+ if (isAuxFilename(file)) {
13224
+ // Matched ahead of the extension, which is the raster's own (.tif.aux.xml).
13225
+ type = 'aux';
13226
+ } else if (ext == 'dbf' || ext == 'shp' || ext == 'kml' || ext == 'svg' || ext == 'fgb' || ext == 'gpkg' || ext == 'png') {
13127
13227
  type = ext;
13128
13228
  } else if (ext == 'jpg' || ext == 'jpeg') {
13129
13229
  type = 'jpeg';
@@ -13147,7 +13247,8 @@
13147
13247
 
13148
13248
  // File types that can be imported but are not convertible to datasets
13149
13249
  function isAuxiliaryInputFileType(type) {
13150
- return type == 'prj' || type == 'shx' || type == 'cpg' || type == 'world';
13250
+ return type == 'prj' || type == 'shx' || type == 'cpg' || type == 'world' ||
13251
+ type == 'aux';
13151
13252
  }
13152
13253
 
13153
13254
  function isRasterImageInputType(type) {
@@ -14082,6 +14183,28 @@
14082
14183
  d: function () { return ~c; }
14083
14184
  };
14084
14185
  };
14186
+ // Adler32
14187
+ var adler = function () {
14188
+ var a = 1, b = 0;
14189
+ return {
14190
+ p: function (d) {
14191
+ // closures have awful performance
14192
+ var n = a, m = b;
14193
+ var l = d.length | 0;
14194
+ for (var i = 0; i != l;) {
14195
+ var e = Math.min(i + 2655, l);
14196
+ for (; i < e; ++i)
14197
+ m += n += d[i];
14198
+ n = (n & 65535) + 15 * (n >> 16), m = (m & 65535) + 15 * (m >> 16);
14199
+ }
14200
+ a = n, b = m;
14201
+ },
14202
+ d: function () {
14203
+ a %= 65521, b %= 65521;
14204
+ return (a & 255) << 24 | (a & 0xFF00) << 8 | (b & 255) << 8 | (b >> 8);
14205
+ }
14206
+ };
14207
+ };
14085
14208
  // deflate with opts
14086
14209
  var dopt = function (dat, opt, pre, post, st) {
14087
14210
  if (!st) {
@@ -14166,7 +14289,7 @@
14166
14289
  };
14167
14290
  // base async inflate fn
14168
14291
  var bInflt = function () { return [u8, u16, i32, fleb, fdeb, clim, fl, fd, flrm, fdrm, rev, ec, hMap, max, bits, bits16, shft, slc, err, inflt, inflateSync, pbf, gopt]; };
14169
- var bDflt = function () { return [u8, u16, i32, fleb, fdeb, clim, revfl, revfd, flm, flt, fdm, fdt, rev, deo, et, hMap, wbits, wbits16, hTree, ln, lc, clen, wfblk, wblk, shft, slc, dflt, dopt, deflateSync, pbf]; };
14292
+ var bDflt = function () { return [u8, u16, i32, fleb, fdeb, clim, revfl, revfd, flm, flt, fdm, fdt, rev, deo, et, hMap, wbits, wbits16, hTree, ln, lc, clen, wfblk, wblk, shft, slc, dflt, dopt, deflateSync$1, pbf]; };
14170
14293
  // gzip extra
14171
14294
  var gze = function () { return [gzh, gzhl, wbytes, crc, crct]; };
14172
14295
  // gunzip extra
@@ -14229,6 +14352,17 @@
14229
14352
  };
14230
14353
  // gzip header length
14231
14354
  var gzhl = function (o) { return 10 + (o.filename ? o.filename.length + 1 : 0); };
14355
+ // zlib header
14356
+ var zlh = function (c, o) {
14357
+ var lv = o.level, fl = lv == 0 ? 0 : lv < 6 ? 1 : lv == 9 ? 3 : 2;
14358
+ c[0] = 120, c[1] = (fl << 6) | (o.dictionary && 32);
14359
+ c[1] |= 31 - ((c[0] << 8) | c[1]) % 31;
14360
+ if (o.dictionary) {
14361
+ var h = adler();
14362
+ h.p(o.dictionary);
14363
+ wbytes(c, 2, h.d());
14364
+ }
14365
+ };
14232
14366
  function deflate(data, opts, cb) {
14233
14367
  if (!cb)
14234
14368
  cb = opts, opts = {};
@@ -14236,7 +14370,7 @@
14236
14370
  err(7);
14237
14371
  return cbify(data, opts, [
14238
14372
  bDflt,
14239
- ], function (ev) { return pbf(deflateSync(ev.data[0], ev.data[1])); }, 0, cb);
14373
+ ], function (ev) { return pbf(deflateSync$1(ev.data[0], ev.data[1])); }, 0, cb);
14240
14374
  }
14241
14375
  /**
14242
14376
  * Compresses data with DEFLATE without any wrapper
@@ -14244,7 +14378,7 @@
14244
14378
  * @param opts The compression options
14245
14379
  * @returns The deflated version of the data
14246
14380
  */
14247
- function deflateSync(data, opts) {
14381
+ function deflateSync$1(data, opts) {
14248
14382
  return dopt(data, opts || {}, 0, 0);
14249
14383
  }
14250
14384
  function inflate(data, opts, cb) {
@@ -14313,6 +14447,20 @@
14313
14447
  err(6, 'invalid gzip data');
14314
14448
  return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary);
14315
14449
  }
14450
+ /**
14451
+ * Compress data with Zlib
14452
+ * @param data The data to compress
14453
+ * @param opts The compression options
14454
+ * @returns The zlib-compressed version of the data
14455
+ */
14456
+ function zlibSync(data, opts) {
14457
+ if (!opts)
14458
+ opts = {};
14459
+ var a = adler();
14460
+ a.p(data);
14461
+ var d = dopt(data, opts, opts.dictionary ? 6 : 2, 4);
14462
+ return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d;
14463
+ }
14316
14464
  // flatten a directory structure
14317
14465
  var fltn = function (d, p, t, o) {
14318
14466
  for (var k in d) {
@@ -14568,7 +14716,7 @@
14568
14716
  cbl(null, file);
14569
14717
  else if (size < 160000) {
14570
14718
  try {
14571
- cbl(null, deflateSync(file, p));
14719
+ cbl(null, deflateSync$1(file, p));
14572
14720
  }
14573
14721
  catch (e) {
14574
14722
  cbl(e, null);
@@ -14606,7 +14754,7 @@
14606
14754
  var exl = exfl(p.extra);
14607
14755
  if (s > 65535)
14608
14756
  err(11);
14609
- var d = compression ? deflateSync(file, p) : file, l = d.length;
14757
+ var d = compression ? deflateSync$1(file, p) : file, l = d.length;
14610
14758
  var c = crc();
14611
14759
  c.p(file);
14612
14760
  files.push(mrg(p, {
@@ -17475,6 +17623,12 @@
17475
17623
  sortIntersections: sortIntersections
17476
17624
  });
17477
17625
 
17626
+ // Reverting an arc can leave it crossing a neighbor that is still modified, so
17627
+ // the check is repeated; each pass reverts more arcs than the last, and the
17628
+ // unmodified geometry is the floor.
17629
+ var MAX_REPAIR_PASSES = 10;
17630
+
17631
+
17478
17632
  // arcs: ArcCollection containing original coordinates
17479
17633
  function getRepairFunction(arcs) {
17480
17634
  var arcsOrig = arcs.getCopy();
@@ -17536,6 +17690,73 @@
17536
17690
  });
17537
17691
  }
17538
17692
 
17693
+ // Remove intersections from a whole-collection edit (such as smoothing) by
17694
+ // putting the arcs that cross back the way they were. Reverting per arc, rather
17695
+ // than per vertex, is what suits an edit that resamples a path: the modified and
17696
+ // original versions of an arc need not share a vertex count.
17697
+ //
17698
+ // arcs: modified arcs, edited in place
17699
+ // arcsOrig: the same arcs, in the same order, before the edit
17700
+ // Returns {reverted: number of arcs put back, remaining: crossings left}
17701
+ function repairCrossedArcs(arcs, arcsOrig) {
17702
+ var intersections = findSegmentIntersections(arcs, {});
17703
+ var reverted = null;
17704
+ var pass = 0;
17705
+ while (intersections.length > 0 && pass++ < MAX_REPAIR_PASSES) {
17706
+ reverted = reverted || new Uint8Array(arcs.size());
17707
+ // Nothing new to try: the arcs that cross are already unmodified.
17708
+ if (markCrossedArcs(intersections, arcs, reverted) === 0) break;
17709
+ revertArcs(arcs, arcsOrig, reverted);
17710
+ intersections = findSegmentIntersections(arcs, {});
17711
+ }
17712
+ return {
17713
+ reverted: reverted ? countMarked(reverted) : 0,
17714
+ remaining: intersections.length
17715
+ };
17716
+ }
17717
+
17718
+ // Flags the arcs taking part in each intersection; returns how many were newly
17719
+ // flagged.
17720
+ function markCrossedArcs(intersections, arcs, marked) {
17721
+ var ii = arcs.getVertexData().ii;
17722
+ var added = 0;
17723
+ intersections.forEach(function(o) {
17724
+ [findArcIdFromVertexId(o.a[0], ii), findArcIdFromVertexId(o.b[0], ii)]
17725
+ .forEach(function(arcId) {
17726
+ if (marked[arcId]) return;
17727
+ marked[arcId] = 1;
17728
+ added++;
17729
+ });
17730
+ });
17731
+ return added;
17732
+ }
17733
+
17734
+ function revertArcs(arcs, arcsOrig, marked) {
17735
+ var mod = arcs.getVertexData();
17736
+ var orig = arcsOrig.getVertexData();
17737
+ var nn = [], xx = [], yy = [];
17738
+ var i, j, src, start, len;
17739
+ for (i = 0; i < marked.length; i++) {
17740
+ src = marked[i] ? orig : mod;
17741
+ start = src.ii[i];
17742
+ len = src.nn[i];
17743
+ nn.push(len);
17744
+ for (j = 0; j < len; j++) {
17745
+ xx.push(src.xx[start + j]);
17746
+ yy.push(src.yy[start + j]);
17747
+ }
17748
+ }
17749
+ arcs.updateVertexData(nn, xx, yy);
17750
+ }
17751
+
17752
+ function countMarked(marked) {
17753
+ var count = 0;
17754
+ for (var i = 0; i < marked.length; i++) {
17755
+ if (marked[i]) count++;
17756
+ }
17757
+ return count;
17758
+ }
17759
+
17539
17760
  // idx: index of an arc endpoint
17540
17761
  function findMatchingEndpoints(idx, data) {
17541
17762
  var ii = data.ii, nn = data.nn, xx = data.xx, yy = data.yy;
@@ -17555,6 +17776,13 @@
17555
17776
  return matches;
17556
17777
  }
17557
17778
 
17779
+ var SegmentIntersectionRepair = /*#__PURE__*/Object.freeze({
17780
+ __proto__: null,
17781
+ getRepairFunction: getRepairFunction,
17782
+ repairCrossedArcs: repairCrossedArcs,
17783
+ repairSegmentIntersections: repairSegmentIntersections
17784
+ });
17785
+
17558
17786
  function roundToSignificantDigits(n, d) {
17559
17787
  return +n.toPrecision(d);
17560
17788
  }
@@ -22299,7 +22527,7 @@
22299
22527
  return;
22300
22528
  }
22301
22529
  layers.forEach(function(lyr) {
22302
- if (!layerHasGeometry(lyr)) return;
22530
+ if (!layerHasGeometry$1(lyr)) return;
22303
22531
  if (lyr.geometry_type == 'polygon' && opts.rewind) {
22304
22532
  profileStart('rewindPolygons');
22305
22533
  rewindPolygons(lyr, dataset.arcs);
@@ -22595,33 +22823,35 @@
22595
22823
  });
22596
22824
  }
22597
22825
 
22598
- // Return an array of Features or Geometries as objects or strings
22826
+ // Returns a cursor for generating a layer's Features (or Geometries) one at a
22827
+ // time. A caller that can consume features incrementally should use this
22828
+ // instead of exportLayerAsGeoJSON(), which holds the entire layer in memory --
22829
+ // the GeoJSON form of a coordinate is many times larger than its binary form,
22830
+ // so materializing a whole layer of detailed geometry is usually the largest
22831
+ // allocation an exporter makes.
22599
22832
  //
22600
- function exportLayerAsGeoJSON(lyr, dataset, opts, asFeatures, ofmt) {
22833
+ // getFeature() returns null for a null geometry when asFeatures is false, so
22834
+ // that such shapes can be left out of a GeometryCollection.
22835
+ function getFeatureCursor(lyr, dataset, opts, asFeatures) {
22601
22836
  var properties = exportProperties(lyr.data, opts),
22602
22837
  shapes = lyr.shapes,
22603
22838
  ids = exportIds(lyr.data, opts),
22604
- stringify;
22605
-
22606
- if (opts.ndjson) {
22607
- stringify = stringifyAsNDJSON;
22608
- } else if (opts.prettify) {
22609
- stringify = getFormattedStringify(['bbox', 'coordinates']);
22610
- } else {
22611
- stringify = JSON.stringify;
22612
- }
22839
+ exporter = GeoJSON.exporters[lyr.geometry_type];
22613
22840
 
22614
22841
  if (properties && shapes && properties.length !== shapes.length) {
22615
22842
  error("Mismatch between number of properties and number of shapes");
22616
22843
  }
22617
22844
 
22618
- return (shapes || properties || []).reduce(function(memo, o, i) {
22619
- var shape = shapes ? shapes[i] : null,
22620
- exporter = GeoJSON.exporters[lyr.geometry_type],
22621
- geom = shape ? exporter(shape, dataset.arcs, opts) : null,
22622
- obj = null;
22623
-
22624
- if (asFeatures) {
22845
+ return {
22846
+ length: (shapes || properties || []).length,
22847
+ properties: properties,
22848
+ getFeature: function(i) {
22849
+ var shape = shapes ? shapes[i] : null,
22850
+ geom = shape ? exporter(shape, dataset.arcs, opts) : null,
22851
+ obj;
22852
+ if (!asFeatures) {
22853
+ return geom || null;
22854
+ }
22625
22855
  obj = composeFeature(geom, properties ? properties[i] : null, opts);
22626
22856
  if (ids) {
22627
22857
  obj.id = ids[i];
@@ -22629,24 +22859,40 @@
22629
22859
  if (opts.no_null_props && !obj.properties) {
22630
22860
  obj.properties = {};
22631
22861
  }
22632
- } else if (!geom) {
22633
- return memo; // don't add null objects to GeometryCollection
22634
- } else {
22635
- obj = geom;
22862
+ return obj;
22636
22863
  }
22864
+ };
22865
+ }
22866
+
22867
+ // Return an array of Features or Geometries as objects or strings
22868
+ //
22869
+ function exportLayerAsGeoJSON(lyr, dataset, opts, asFeatures, ofmt) {
22870
+ var cursor = getFeatureCursor(lyr, dataset, opts, asFeatures),
22871
+ items = [],
22872
+ stringify, obj, i;
22873
+
22874
+ if (opts.ndjson) {
22875
+ stringify = stringifyAsNDJSON;
22876
+ } else if (opts.prettify) {
22877
+ stringify = getFormattedStringify(['bbox', 'coordinates']);
22878
+ } else {
22879
+ stringify = JSON.stringify;
22880
+ }
22881
+
22882
+ for (i = 0; i < cursor.length; i++) {
22883
+ obj = cursor.getFeature(i);
22884
+ if (!asFeatures && !obj) continue; // don't add null objects to GeometryCollection
22637
22885
  if (ofmt) {
22638
22886
  // stringify features as soon as they are generated, to reduce the
22639
22887
  // number of JS objects in memory (so larger files can be exported)
22640
22888
  obj = stringify(obj);
22641
22889
  if (ofmt == 'buffer') {
22642
22890
  obj = encodeString(obj, 'utf8');
22643
- // obj = stringToBuffer(obj);
22644
- // obj = new Buffer(obj, 'utf8');
22645
22891
  }
22646
22892
  }
22647
- memo.push(obj);
22648
- return memo;
22649
- }, []);
22893
+ items.push(obj);
22894
+ }
22895
+ return items;
22650
22896
  }
22651
22897
 
22652
22898
  function composeFeature(geom, properties, opts) {
@@ -22967,6 +23213,7 @@
22967
23213
  exportLayerAsGeoJSON: exportLayerAsGeoJSON,
22968
23214
  exportProperties: exportProperties,
22969
23215
  getDatasetBbox: getDatasetBbox,
23216
+ getFeatureCursor: getFeatureCursor,
22970
23217
  getIdField: getIdField,
22971
23218
  preserveOriginalCRS: preserveOriginalCRS,
22972
23219
  useFeatureCollection: useFeatureCollection,
@@ -28525,6 +28772,19 @@ ${svg}
28525
28772
  return require('zlib').gzipSync(content, opts);
28526
28773
  }
28527
28774
 
28775
+ // Compress to a zlib stream (a deflate stream wrapped in a zlib header and
28776
+ // checksum). This is what TIFF's Deflate compression expects, unlike the gzip
28777
+ // wrapper used for .gz files.
28778
+ function deflateSync(content, opts) {
28779
+ if (typeof content == 'string') {
28780
+ content = strToU8(content);
28781
+ }
28782
+ if (runningInBrowser()) {
28783
+ return zlibSync(content, opts);
28784
+ }
28785
+ return require('zlib').deflateSync(content, opts);
28786
+ }
28787
+
28528
28788
  async function gzipAsync(content, opts) {
28529
28789
  if (typeof content == 'string') {
28530
28790
  content = strToU8(content);
@@ -28559,6 +28819,7 @@ ${svg}
28559
28819
 
28560
28820
  var Gzip = /*#__PURE__*/Object.freeze({
28561
28821
  __proto__: null,
28822
+ deflateSync: deflateSync,
28562
28823
  gunzipAsync: gunzipAsync,
28563
28824
  gunzipSync: gunzipSync,
28564
28825
  gzipAsync: gzipAsync,
@@ -31642,56 +31903,89 @@ ${svg}
31642
31903
  return builder.asUint8Array();
31643
31904
  }
31644
31905
 
31645
- function serializeWithColumns(geojson, columns) {
31646
- var headerMeta = {
31647
- geometryType: inferGeometryType(geojson.features),
31648
- columns: columns,
31649
- featuresCount: geojson.features.length};
31650
- var header = buildHeader(headerMeta);
31651
- var features = geojson.features.map(function(feature) {
31906
+ // source: a feature cursor, {length, getFeature(i)}. Features are pulled and
31907
+ // encoded one at a time rather than as a collection, because the GeoJSON form
31908
+ // of a layer is several times larger than the FlatGeobuf it encodes into.
31909
+ function serializeWithColumns(source, columns) {
31910
+ if (source.length === 0) {
31911
+ throw new Error('Could not infer geometry type for collection of features.');
31912
+ }
31913
+ // The header precedes the features and declares the collection's geometry
31914
+ // type, but that type isn't known until every feature has been seen. Assume
31915
+ // the layer is homogeneous, which nearly all are, and encode again as
31916
+ // Unknown in the rare case that it isn't.
31917
+ return encodeCollection(source, columns, null) ||
31918
+ encodeCollection(source, columns, GeometryType.Unknown);
31919
+ }
31920
+
31921
+ // Returns null if forcedType is null and the features turn out to have more
31922
+ // than one geometry type.
31923
+ function encodeCollection(source, columns, forcedType) {
31924
+ var headerMeta = null;
31925
+ var sink = null;
31926
+ for (var i = 0; i < source.length; i++) {
31927
+ var feature = source.getFeature(i);
31928
+ var type = GeometryType[feature.geometry.type] || GeometryType.Unknown;
31929
+ if (!headerMeta) {
31930
+ headerMeta = {
31931
+ geometryType: forcedType === null ? type : forcedType,
31932
+ columns: columns,
31933
+ envelope: null,
31934
+ featuresCount: source.length,
31935
+ indexNodeSize: 0,
31936
+ crs: null,
31937
+ title: null,
31938
+ description: null,
31939
+ metadata: null
31940
+ };
31941
+ sink = new ByteSink(magicbytes.length + estimateEncodedBytes(source));
31942
+ sink.append(magicbytes);
31943
+ sink.append(buildHeader(headerMeta));
31944
+ } else if (forcedType === null && type != headerMeta.geometryType) {
31945
+ return null;
31946
+ }
31652
31947
  var geometry = feature.geometry.type == 'GeometryCollection' ?
31653
31948
  parseGC(feature.geometry) :
31654
31949
  parseGeometry(feature.geometry);
31655
31950
  omitRedundantGeometryType(geometry, headerMeta.geometryType);
31656
- return buildFeature(geometry, normalizeProperties(feature.properties, columns), headerMeta);
31657
- });
31658
- var featureBytes = features.reduce(function(sum, feature) {
31659
- return sum + feature.length;
31660
- }, 0);
31661
- var output = new Uint8Array(magicbytes.length + header.length + featureBytes);
31662
- var offset = magicbytes.length;
31663
- output.set(header, offset);
31664
- offset += header.length;
31665
- features.forEach(function(feature) {
31666
- output.set(feature, offset);
31667
- offset += feature.length;
31668
- });
31669
- output.set(magicbytes);
31670
- return output;
31951
+ sink.append(buildFeature(geometry, normalizeProperties(feature.properties, columns), headerMeta));
31952
+ }
31953
+ return sink.toBytes();
31954
+ }
31955
+
31956
+ // Encoding the first feature is the only sample available before the buffer
31957
+ // has to be allocated; overshooting costs a resize, which is why it is loose.
31958
+ function estimateEncodedBytes(source) {
31959
+ return Math.max(source.length * 64, 1024);
31671
31960
  }
31672
31961
 
31962
+ function ByteSink(initialSize) {
31963
+ this.bytes = new Uint8Array(initialSize);
31964
+ this.length = 0;
31965
+ }
31966
+
31967
+ ByteSink.prototype.append = function(chunk) {
31968
+ if (this.length + chunk.length > this.bytes.length) {
31969
+ var size = this.bytes.length;
31970
+ while (size < this.length + chunk.length) size *= 2;
31971
+ var grown = new Uint8Array(size);
31972
+ grown.set(this.bytes.subarray(0, this.length));
31973
+ this.bytes = grown;
31974
+ }
31975
+ this.bytes.set(chunk, this.length);
31976
+ this.length += chunk.length;
31977
+ };
31978
+
31979
+ ByteSink.prototype.toBytes = function() {
31980
+ return this.bytes.subarray(0, this.length);
31981
+ };
31982
+
31673
31983
  function omitRedundantGeometryType(geometry, headerType) {
31674
31984
  if (headerType != GeometryType.Unknown && geometry.type == headerType) {
31675
31985
  geometry.type = GeometryType.Unknown;
31676
31986
  }
31677
31987
  }
31678
31988
 
31679
- function inferGeometryType(features) {
31680
- var type;
31681
- features.forEach(function(feature) {
31682
- var next = GeometryType[feature.geometry.type] || GeometryType.Unknown;
31683
- if (type === undefined) {
31684
- type = next;
31685
- } else if (type != next) {
31686
- type = GeometryType.Unknown;
31687
- }
31688
- });
31689
- if (type === undefined) {
31690
- throw new Error('Could not infer geometry type for collection of features.');
31691
- }
31692
- return type;
31693
- }
31694
-
31695
31989
  function normalizeProperties(properties, columns) {
31696
31990
  var copy = {};
31697
31991
  properties = properties || {};
@@ -31751,9 +32045,12 @@ ${svg}
31751
32045
  }
31752
32046
  var crsMeta = resolveOutputCRS(dataset);
31753
32047
  return dataset.layers.map(function(lyr) {
31754
- var geojson = getFeatureCollection(lyr, dataset, opts);
32048
+ var cursor = getFeatureCursor(lyr, dataset, opts, true);
32049
+ if (cursor.length === 0) {
32050
+ stop$1('FlatGeobuf export does not support empty layers');
32051
+ }
31755
32052
  var columns = getFlatGeobufColumns(lyr);
31756
- var content = serializeWithColumns(geojson, columns);
32053
+ var content = serializeWithColumns(cursor, columns);
31757
32054
  var filename = lyr.name + '.' + extension;
31758
32055
  if (crsMeta) {
31759
32056
  content = rewriteHeaderWithCRS(content, crsMeta);
@@ -31767,17 +32064,6 @@ ${svg}
31767
32064
  });
31768
32065
  }
31769
32066
 
31770
- function getFeatureCollection(lyr, dataset, opts) {
31771
- var features = exportLayerAsGeoJSON(lyr, dataset, opts, true, null);
31772
- if (features.length === 0) {
31773
- stop$1('FlatGeobuf export does not support empty layers');
31774
- }
31775
- return {
31776
- type: 'FeatureCollection',
31777
- features: features
31778
- };
31779
- }
31780
-
31781
32067
  function getFlatGeobufColumns(lyr) {
31782
32068
  var records = lyr.data ? lyr.data.getRecords() : [];
31783
32069
  return lyr.data ? lyr.data.getFields().map(function(name) {
@@ -32473,83 +32759,177 @@ ${svg}
32473
32759
  async function exportGeoParquet(dataset, opts, filenameOverride) {
32474
32760
  var writer = await loadGeoParquetWriter();
32475
32761
  var compression = await getGeoParquetCompression(opts);
32762
+ var rowGroupOverride = validateRowGroupSize(opts.rowgroup);
32476
32763
  var extension = opts.extension || 'parquet';
32477
32764
  var files = [];
32478
32765
  if (opts.file) {
32479
32766
  extension = getFileExtension(opts.file) || extension;
32480
32767
  }
32481
32768
  dataset.layers.forEach(function(lyr) {
32482
- var features = exportLayerAsGeoJSON(lyr, dataset, opts, true, null);
32483
- var hasGeometry = features.some(function(feat) {
32484
- return !!feat.geometry;
32485
- });
32486
- var output = buildGeoParquetColumns(features, hasGeometry);
32487
- var writeOptions = {
32488
- columnData: output.columnData,
32489
- codec: compression.codec,
32490
- compressors: compression.compressors,
32491
- pageSize: compression.pageSize
32492
- };
32493
- if (hasGeometry) {
32494
- writeOptions.kvMetadata = [{
32495
- key: 'geo',
32496
- value: JSON.stringify(buildGeoMetadata(features, dataset))
32497
- }];
32498
- } else {
32499
- warn('GeoParquet export: layer has no geometry; writing attribute data only.');
32500
- }
32501
- var content = writer.parquetWriteBuffer(writeOptions);
32502
32769
  files.push({
32503
32770
  filename: filenameOverride || (lyr.name + '.' + extension),
32504
- content: content
32771
+ content: writeLayer(writer, lyr, dataset, opts, compression, rowGroupOverride)
32505
32772
  });
32506
32773
  });
32507
32774
  return files;
32508
32775
  }
32509
32776
 
32510
- function buildGeoParquetColumns(features, includeGeometry) {
32511
- var geometryName = 'geometry';
32512
- var names = getPropertyNames(features);
32513
- var columnData = [];
32514
- if (features.length === 0) {
32777
+ var GEOMETRY_COLUMN = 'geometry';
32778
+
32779
+ // Rows are converted and handed to the writer one row group at a time. The
32780
+ // GeoJSON form of a geometry is several times larger than the WKB the writer
32781
+ // encodes it into, so materializing a whole layer of it was the largest
32782
+ // allocation the export made -- larger than the dataset and the output file
32783
+ // combined. Converting a group at a time makes that cost a function of the
32784
+ // row group size rather than of the layer size.
32785
+ function writeLayer(writer, lyr, dataset, opts, compression, rowGroupOverride) {
32786
+ var cursor = getFeatureCursor(lyr, dataset, opts, true);
32787
+ var rowCount = cursor.length;
32788
+ if (rowCount === 0) {
32515
32789
  stop$1('GeoParquet export requires at least one record');
32516
32790
  }
32517
- if (includeGeometry) {
32518
- columnData.push({
32519
- name: geometryName,
32520
- data: features.map(function(feat) {
32521
- return feat.geometry || null;
32522
- }),
32523
- type: 'GEOMETRY'
32791
+ var hasGeometry = layerHasGeometry(lyr);
32792
+ var fields = getFieldTypes(cursor, opts);
32793
+ if (!hasGeometry && fields.length === 0) {
32794
+ stop$1('GeoParquet export requires geometry or attribute data');
32795
+ }
32796
+ if (!hasGeometry) {
32797
+ warn('GeoParquet export: layer has no geometry; writing attribute data only.');
32798
+ }
32799
+ var crs = hasGeometry ? getGeoMetadataCrs(dataset) : null;
32800
+ var byteWriter = new writer.ByteWriter();
32801
+ var pq = new writer.ParquetWriter({
32802
+ writer: byteWriter,
32803
+ schema: buildSchema(writer, fields, hasGeometry, crs),
32804
+ codec: compression.codec,
32805
+ compressors: compression.compressors
32806
+ });
32807
+ var geometryTypes = {};
32808
+ var plan = getRowGroupSize(cursor, fields, hasGeometry, rowCount, rowGroupOverride);
32809
+ eachRowGroupRange(rowCount, plan, function(start, end) {
32810
+ pq.write({
32811
+ columnData: buildChunkColumns(cursor, fields, hasGeometry, start, end, geometryTypes),
32812
+ rowGroupSize: end - start,
32813
+ pageSize: compression.pageSize
32524
32814
  });
32815
+ });
32816
+ if (hasGeometry) {
32817
+ // Set on the writer rather than passed to the constructor: the geometry
32818
+ // types aren't known until every group has been converted.
32819
+ pq.kvMetadata = [{
32820
+ key: 'geo',
32821
+ value: JSON.stringify(buildGeoMetadata(Object.keys(geometryTypes), crs))
32822
+ }];
32525
32823
  }
32526
- names.forEach(function(name) {
32527
- var values = features.map(function(feat) {
32528
- return feat.properties ? feat.properties[name] : null;
32824
+ avoidIntegerBboxBounds(pq.row_groups);
32825
+ pq.finish();
32826
+ return byteWriter.getBuffer();
32827
+ }
32828
+
32829
+ // hyparquet-writer (0.14) infers each thrift wire type from the runtime value, so
32830
+ // a bounding box bound that lands on a whole number is written as I32 where the
32831
+ // Parquet spec requires a double. Readers that parse geospatial statistics
32832
+ // (pyarrow 21+, GDAL 3.11+) then fail to deserialize the footer and cannot open
32833
+ // the file at all -- which rounded, snapped or integer-grid coordinates make easy
32834
+ // to hit. Widening each bound to the adjacent double forces the double path.
32835
+ // Reported upstream; remove once the writer carries declared field types.
32836
+ function avoidIntegerBboxBounds(rowGroups) {
32837
+ (rowGroups || []).forEach(function(group) {
32838
+ (group.columns || []).forEach(function(col) {
32839
+ var stats = col.meta_data && col.meta_data.geospatial_statistics;
32840
+ if (!stats || !stats.bbox) return;
32841
+ if (!widenIntegerBounds(stats.bbox)) {
32842
+ // Past 2^53 doubles are spaced too widely to step off an integer. Drop
32843
+ // the box rather than write a footer that no reader can parse.
32844
+ delete stats.bbox;
32845
+ }
32529
32846
  });
32530
- columnData.push(buildAttributeColumn(name, values));
32531
32847
  });
32532
- if (columnData.length === 0) {
32533
- stop$1('GeoParquet export requires geometry or attribute data');
32848
+ }
32849
+
32850
+ // Nudges whole-number bounds outward, so that the box still covers every
32851
+ // geometry in the group: an oversized box only costs a reader the chance to skip
32852
+ // the group, while an undersized one would hide rows that match a filter.
32853
+ // Returns false if any bound could not be made non-integral.
32854
+ function widenIntegerBounds(bbox) {
32855
+ return Object.keys(bbox).every(function(key) {
32856
+ var val = bbox[key];
32857
+ var widened;
32858
+ if (!Number.isInteger(val)) return true;
32859
+ widened = nextDouble(val, /min$/.test(key) ? -1 : 1);
32860
+ if (Number.isInteger(widened)) return false;
32861
+ bbox[key] = widened;
32862
+ return true;
32863
+ });
32864
+ }
32865
+
32866
+ var nextDoubleFloats = new Float64Array(1);
32867
+ var nextDoubleBits = new BigInt64Array(nextDoubleFloats.buffer);
32868
+
32869
+ // Returns the adjacent double to @val in the direction of @sign (-1 down, 1 up).
32870
+ function nextDouble(val, sign) {
32871
+ if (!Number.isFinite(val)) return val;
32872
+ if (val === 0) return sign > 0 ? Number.MIN_VALUE : -Number.MIN_VALUE;
32873
+ nextDoubleFloats[0] = val;
32874
+ // Incrementing the bit pattern moves a positive double up and a negative one
32875
+ // further down, so the step direction depends on the sign of the value.
32876
+ nextDoubleBits[0] += (val > 0) === (sign > 0) ? 1n : -1n;
32877
+ return nextDoubleFloats[0];
32878
+ }
32879
+
32880
+ // A shape that survives to this point can still export as null geometry if all
32881
+ // of its paths collapse, in which case the column is written as nulls.
32882
+ function layerHasGeometry(lyr) {
32883
+ if (!lyr.geometry_type || !lyr.shapes) return false;
32884
+ return lyr.shapes.some(Boolean);
32885
+ }
32886
+
32887
+ function eachRowGroupRange(rowCount, plan, cb) {
32888
+ var sizes = Array.isArray(plan) ? plan : [plan];
32889
+ var start = 0;
32890
+ for (var i = 0; start < rowCount; i++) {
32891
+ var end = Math.min(start + sizes[Math.min(i, sizes.length - 1)], rowCount);
32892
+ cb(start, end);
32893
+ start = end;
32534
32894
  }
32535
- return {columnData: columnData, geometryColumn: geometryName};
32536
32895
  }
32537
32896
 
32538
- function buildAttributeColumn(name, values) {
32539
- var info = inferColumnType(values);
32540
- return {
32541
- name: name,
32542
- data: values.map(function(value) {
32543
- return normalizeFieldValue(value, info.type);
32544
- }),
32545
- type: info.type
32546
- };
32897
+ function buildChunkColumns(cursor, fields, hasGeometry, start, end, geometryTypes) {
32898
+ var n = end - start;
32899
+ var geometry = hasGeometry ? new Array(n) : null;
32900
+ var values = fields.map(function() { return new Array(n); });
32901
+ for (var i = 0; i < n; i++) {
32902
+ var feat = cursor.getFeature(start + i);
32903
+ var props = feat.properties;
32904
+ if (hasGeometry) {
32905
+ var geom = feat.geometry || null;
32906
+ geometry[i] = geom;
32907
+ if (geom && geom.type) geometryTypes[geom.type] = true;
32908
+ }
32909
+ for (var j = 0; j < fields.length; j++) {
32910
+ values[j][i] = normalizeFieldValue(props ? props[fields[j].name] : null, fields[j].type);
32911
+ }
32912
+ }
32913
+ var columnData = hasGeometry ? [{name: GEOMETRY_COLUMN, data: geometry}] : [];
32914
+ fields.forEach(function(field, j) {
32915
+ columnData.push({name: field.name, data: values[j]});
32916
+ });
32917
+ return columnData;
32547
32918
  }
32548
32919
 
32549
- function inferColumnType(values) {
32920
+ function getFieldTypes(cursor, opts) {
32921
+ var properties = cursor.properties;
32922
+ if (!properties) return [];
32923
+ return getPropertyNames(properties, opts).map(function(name) {
32924
+ return {name: name, type: inferColumnType(properties, name)};
32925
+ });
32926
+ }
32927
+
32928
+ function inferColumnType(properties, name) {
32550
32929
  var type = null;
32551
- for (var i = 0; i < values.length; i++) {
32552
- var valueType = inferValueType(values[i]);
32930
+ for (var i = 0; i < properties.length; i++) {
32931
+ var props = properties[i];
32932
+ var valueType = inferValueType(props ? props[name] : null);
32553
32933
  if (!valueType) continue;
32554
32934
  if (!type) {
32555
32935
  type = valueType;
@@ -32558,12 +32938,11 @@ ${svg}
32558
32938
  (valueType == 'INT32' || valueType == 'DOUBLE')) {
32559
32939
  type = 'DOUBLE';
32560
32940
  } else {
32561
- type = 'STRING';
32562
- break;
32941
+ return 'STRING';
32563
32942
  }
32564
32943
  }
32565
32944
  }
32566
- return {type: type || 'STRING'};
32945
+ return type || 'STRING';
32567
32946
  }
32568
32947
 
32569
32948
  function inferValueType(value) {
@@ -32604,22 +32983,172 @@ ${svg}
32604
32983
  return String(value);
32605
32984
  }
32606
32985
 
32607
- function getPropertyNames(features) {
32986
+ function getPropertyNames(properties, opts) {
32608
32987
  var index = {};
32609
- features.forEach(function(feat) {
32610
- var props = feat.properties || {};
32611
- Object.keys(props).forEach(function(name) {
32612
- index[name] = true;
32988
+ // Hoisted fields are moved to the root of a GeoJSON Feature, so they are not
32989
+ // among a feature's properties and don't become columns.
32990
+ var hoisted = Array.isArray(opts.hoist) ? opts.hoist : [];
32991
+ properties.forEach(function(props) {
32992
+ Object.keys(props || {}).forEach(function(name) {
32993
+ if (hoisted.indexOf(name) == -1) index[name] = true;
32613
32994
  });
32614
32995
  });
32615
32996
  return Object.keys(index);
32616
32997
  }
32617
32998
 
32618
- function buildGeoMetadata(features, dataset) {
32619
- var geomTypes = utils.uniq(features.map(function(feat) {
32620
- return feat.geometry && feat.geometry.type || null;
32621
- }).filter(Boolean));
32622
- var crs = getGeoMetadataCrs(dataset);
32999
+ // A reader has to materialize an entire row group, so the meaningful unit is
33000
+ // bytes, not rows: a row of point geometry runs to a few dozen bytes where a
33001
+ // row of detailed polygon geometry can be tens of kilobytes. Sizing by a fixed
33002
+ // row count therefore lands hundreds of times off in either direction, so the
33003
+ // group size is derived from an estimate of the encoded row size instead.
33004
+ // The estimate ignores Parquet's own encoding, which only ever shrinks a
33005
+ // column, so groups tend to come out at half the target or less.
33006
+ //
33007
+ // The target is well below the 128MB that Hadoop-era guidance suggests, which
33008
+ // assumed a row group should fill an HDFS block. Mapshaper holds a whole
33009
+ // dataset in memory and needs roughly twenty times the output size to write
33010
+ // it, so it tops out around a couple of hundred megabytes -- at 128MB nearly
33011
+ // every file it can produce would be a single row group, giving readers no
33012
+ // parallelism and nothing to skip when filtering. Measured on a 23MB layer,
33013
+ // splitting one group into eight costs 0.03% of file size, so the smaller
33014
+ // target is close to free.
33015
+ var ROW_GROUP_TARGET_BYTES = 16 * 1024 * 1024;
33016
+ // A small leading group lets a reader fetching byte ranges over HTTP show the
33017
+ // start of the table without pulling a full-sized group.
33018
+ var ROW_GROUP_PREVIEW_BYTES = 1024 * 1024;
33019
+ var ROW_GROUP_MAX_ROWS = 1000000;
33020
+ var ROW_GROUP_SAMPLE_ROWS = 1000;
33021
+
33022
+ function getRowGroupSize(cursor, fields, hasGeometry, rowCount, override) {
33023
+ if (override) return override;
33024
+ var bytesPerRow = estimateRowBytes(cursor, fields, hasGeometry, rowCount);
33025
+ if (!bytesPerRow) return rowCount; // one group rather than the writer's default
33026
+ return planRowGroups(rowCount, bytesPerRow);
33027
+ }
33028
+
33029
+ // Returns a rowGroupSize for the Parquet writer: either a row count, or a
33030
+ // [preview, bulk] pair where the bulk size repeats for the rest of the file.
33031
+ function planRowGroups(rowCount, bytesPerRow) {
33032
+ var bulkRows = clampRowCount(ROW_GROUP_TARGET_BYTES / bytesPerRow, ROW_GROUP_MAX_ROWS);
33033
+ // Keep the preview well below a full group, otherwise splitting it off buys
33034
+ // a range reader nothing.
33035
+ var previewRows = clampRowCount(ROW_GROUP_PREVIEW_BYTES / bytesPerRow, Math.floor(bulkRows / 4));
33036
+ if (rowCount <= previewRows * 4) {
33037
+ return bulkRows;
33038
+ }
33039
+ // Spread the remaining rows evenly so that the file doesn't end with a runt
33040
+ // group holding a handful of rows.
33041
+ var rest = rowCount - previewRows;
33042
+ var groups = Math.ceil(rest / bulkRows);
33043
+ return [previewRows, Math.ceil(rest / groups)];
33044
+ }
33045
+
33046
+ function clampRowCount(rows, max) {
33047
+ if (!(rows > 1)) return 1;
33048
+ if (max >= 1 && rows > max) return max;
33049
+ return Math.floor(rows);
33050
+ }
33051
+
33052
+ // Converts the sampled rows one at a time and discards them, so that sizing
33053
+ // the row groups doesn't itself materialize the layer.
33054
+ function estimateRowBytes(cursor, fields, hasGeometry, rowCount) {
33055
+ // Sample at a stride rather than taking a prefix: layers are often sorted or
33056
+ // clustered, so the first rows are not representative.
33057
+ var step = Math.max(1, Math.floor(rowCount / ROW_GROUP_SAMPLE_ROWS));
33058
+ var total = 0;
33059
+ var sampled = 0;
33060
+ for (var i = 0; i < rowCount; i += step) {
33061
+ var feat = cursor.getFeature(i);
33062
+ var props = feat.properties;
33063
+ if (hasGeometry) {
33064
+ total += estimateValueBytes(feat.geometry || null);
33065
+ }
33066
+ for (var j = 0; j < fields.length; j++) {
33067
+ total += estimateValueBytes(
33068
+ normalizeFieldValue(props ? props[fields[j].name] : null, fields[j].type));
33069
+ }
33070
+ sampled++;
33071
+ }
33072
+ return sampled > 0 ? total / sampled : 0;
33073
+ }
33074
+
33075
+ function estimateValueBytes(value) {
33076
+ if (value === null || value === undefined) return 1;
33077
+ if (typeof value == 'string') return value.length + 4;
33078
+ if (typeof value == 'number' || typeof value == 'bigint') return 8;
33079
+ if (typeof value == 'boolean') return 1;
33080
+ if (value instanceof Date) return 8;
33081
+ if (value instanceof Uint8Array) return value.byteLength + 4;
33082
+ if (utils.isObject(value) && utils.isString(value.type)) {
33083
+ return estimateWkbBytes(value);
33084
+ }
33085
+ return 32; // a JSON-encoded column; the exact width doesn't change the sizing
33086
+ }
33087
+
33088
+ // WKB spends 1 byte on the byte order and 4 on the geometry type, 4 more on
33089
+ // each nested element count, and 16 on each XY position.
33090
+ function estimateWkbBytes(geom) {
33091
+ if (!geom || !geom.type) return 1;
33092
+ if (geom.type == 'GeometryCollection') {
33093
+ return (geom.geometries || []).reduce(function(memo, part) {
33094
+ return memo + estimateWkbBytes(part);
33095
+ }, 9);
33096
+ }
33097
+ return 5 + estimateCoordinateBytes(geom.coordinates);
33098
+ }
33099
+
33100
+ function estimateCoordinateBytes(coords) {
33101
+ if (!Array.isArray(coords)) return 0;
33102
+ if (typeof coords[0] == 'number') return 16;
33103
+ var bytes = 4;
33104
+ for (var i = 0; i < coords.length; i++) {
33105
+ bytes += estimateCoordinateBytes(coords[i]);
33106
+ }
33107
+ return bytes;
33108
+ }
33109
+
33110
+ function validateRowGroupSize(rowgroup) {
33111
+ if (rowgroup === undefined) return undefined;
33112
+ if (rowgroup >= 1 && Math.floor(rowgroup) === rowgroup) {
33113
+ return rowgroup;
33114
+ }
33115
+ stop$1('The rowgroup= option must be a positive integer number of rows');
33116
+ }
33117
+
33118
+ // The schema is derived from the column types alone, before any rows are
33119
+ // converted, because each row group is written as it is built and they all
33120
+ // have to share one schema.
33121
+ //
33122
+ // A reader that understands the Parquet 2.11 GEOMETRY logical type takes the
33123
+ // CRS from the logical type and ignores the "geo" metadata. When the logical
33124
+ // type carries no CRS the spec default is OGC:CRS84, so projected data was
33125
+ // being reported as WGS 84. Write the CRS in both places, as GDAL does, so
33126
+ // that GeoParquet 1.x readers keep working.
33127
+ function buildSchema(writer, fields, hasGeometry, crs) {
33128
+ var columnData = hasGeometry ?
33129
+ [{name: GEOMETRY_COLUMN, type: 'GEOMETRY'}] : [];
33130
+ var overrides;
33131
+ fields.forEach(function(field) {
33132
+ columnData.push({name: field.name, type: field.type});
33133
+ });
33134
+ if (hasGeometry && crs) {
33135
+ overrides = {};
33136
+ overrides[GEOMETRY_COLUMN] = {
33137
+ name: GEOMETRY_COLUMN,
33138
+ type: 'BYTE_ARRAY',
33139
+ repetition_type: 'OPTIONAL',
33140
+ logical_type: {type: 'GEOMETRY', crs: JSON.stringify(crs)}
33141
+ };
33142
+ // An overridden column must not also declare a type.
33143
+ columnData[0] = {name: GEOMETRY_COLUMN};
33144
+ }
33145
+ return writer.schemaFromColumnData({
33146
+ columnData: columnData,
33147
+ schemaOverrides: overrides
33148
+ });
33149
+ }
33150
+
33151
+ function buildGeoMetadata(geomTypes, crs) {
32623
33152
  var geomMeta = {
32624
33153
  encoding: 'WKB',
32625
33154
  geometry_types: geomTypes
@@ -32671,13 +33200,19 @@ ${svg}
32671
33200
  return null;
32672
33201
  }
32673
33202
 
33203
+ function isGeoParquetWriter(mod) {
33204
+ return !!mod && typeof mod.ParquetWriter == 'function' &&
33205
+ typeof mod.ByteWriter == 'function' &&
33206
+ typeof mod.schemaFromColumnData == 'function';
33207
+ }
33208
+
32674
33209
  async function loadGeoParquetWriter() {
32675
33210
  if (runningInBrowser()) {
32676
33211
  var mod = require$1('hyparquet-writer');
32677
- if (mod && mod.default && !mod.parquetWriteBuffer) {
33212
+ if (mod && mod.default && !isGeoParquetWriter(mod)) {
32678
33213
  mod = mod.default;
32679
33214
  }
32680
- if (!mod || !mod.parquetWriteBuffer) {
33215
+ if (!isGeoParquetWriter(mod)) {
32681
33216
  stop$1('GeoParquet writer library is not loaded');
32682
33217
  }
32683
33218
  return mod;
@@ -32686,7 +33221,7 @@ ${svg}
32686
33221
  writerPromise = dynamicImportModule$2('hyparquet-writer');
32687
33222
  }
32688
33223
  var nodeMod = await writerPromise;
32689
- return nodeMod.default && !nodeMod.parquetWriteBuffer ? nodeMod.default : nodeMod;
33224
+ return nodeMod.default && !isGeoParquetWriter(nodeMod) ? nodeMod.default : nodeMod;
32690
33225
  }
32691
33226
 
32692
33227
  async function getGeoParquetCompression(opts) {
@@ -32770,6 +33305,947 @@ ${svg}
32770
33305
  return codec;
32771
33306
  }
32772
33307
 
33308
+ // A minimal TIFF/GeoTIFF encoder, used by -o format=geotiff.
33309
+ //
33310
+ // This is hand-rolled rather than delegating to the geotiff dependency, whose
33311
+ // writer silently corrupts signed integer samples (its type table omits
33312
+ // Int8/16/32Array, so it falls back to writing 64-bit floats while the tags
33313
+ // still declare integer samples), tags data as Deflate without compressing it,
33314
+ // and builds its IFD in a fixed 1000-byte buffer that long ASCII tags overrun.
33315
+ //
33316
+ // Output is a classic (32-bit) little-endian TIFF with band-interleaved,
33317
+ // strip-based data, which is what our grids already look like in memory.
33318
+
33319
+
33320
+ var TIFF_TYPES = {
33321
+ ASCII: 2,
33322
+ SHORT: 3,
33323
+ LONG: 4,
33324
+ DOUBLE: 12
33325
+ };
33326
+
33327
+ var TYPE_SIZES = {
33328
+ 2: 1,
33329
+ 3: 2,
33330
+ 4: 4,
33331
+ 12: 8
33332
+ };
33333
+
33334
+ // Baseline tags, plus the GeoTIFF and GDAL extensions we need. Tags must appear
33335
+ // in the IFD in ascending numeric order.
33336
+ var TAGS = {
33337
+ ImageWidth: 256,
33338
+ ImageLength: 257,
33339
+ BitsPerSample: 258,
33340
+ Compression: 259,
33341
+ PhotometricInterpretation: 262,
33342
+ StripOffsets: 273,
33343
+ SamplesPerPixel: 277,
33344
+ RowsPerStrip: 278,
33345
+ StripByteCounts: 279,
33346
+ PlanarConfiguration: 284,
33347
+ Software: 305,
33348
+ ExtraSamples: 338,
33349
+ SampleFormat: 339,
33350
+ ModelPixelScale: 33550,
33351
+ ModelTiepoint: 33922,
33352
+ GeoKeyDirectory: 34735,
33353
+ GeoDoubleParams: 34736,
33354
+ GDAL_NODATA: 42113
33355
+ };
33356
+
33357
+ // The geo keys this writer knows how to emit, each with its id and its value
33358
+ // type. A key's type is fixed by the GeoTIFF spec: the short-valued keys hold
33359
+ // codes and the double-valued ones hold measurements, in degrees or in the
33360
+ // projection's linear units.
33361
+ var GEO_KEYS = {
33362
+ GTModelTypeGeoKey: [1024, 'short'],
33363
+ GTRasterTypeGeoKey: [1025, 'short'],
33364
+ GeographicTypeGeoKey: [2048, 'short'],
33365
+ GeogGeodeticDatumGeoKey: [2050, 'short'],
33366
+ GeogAngularUnitsGeoKey: [2054, 'short'],
33367
+ GeogEllipsoidGeoKey: [2056, 'short'],
33368
+ GeogSemiMajorAxisGeoKey: [2057, 'double'],
33369
+ GeogSemiMinorAxisGeoKey: [2058, 'double'],
33370
+ GeogInvFlatteningGeoKey: [2059, 'double'],
33371
+ GeogPrimeMeridianLongGeoKey: [2061, 'double'],
33372
+ ProjectedCSTypeGeoKey: [3072, 'short'],
33373
+ ProjectionGeoKey: [3074, 'short'],
33374
+ ProjCoordTransGeoKey: [3075, 'short'],
33375
+ ProjLinearUnitsGeoKey: [3076, 'short'],
33376
+ ProjLinearUnitSizeGeoKey: [3077, 'double'],
33377
+ ProjStdParallel1GeoKey: [3078, 'double'],
33378
+ ProjStdParallel2GeoKey: [3079, 'double'],
33379
+ ProjNatOriginLongGeoKey: [3080, 'double'],
33380
+ ProjNatOriginLatGeoKey: [3081, 'double'],
33381
+ ProjFalseEastingGeoKey: [3082, 'double'],
33382
+ ProjFalseNorthingGeoKey: [3083, 'double'],
33383
+ ProjFalseOriginLongGeoKey: [3084, 'double'],
33384
+ ProjFalseOriginLatGeoKey: [3085, 'double'],
33385
+ ProjFalseOriginEastingGeoKey: [3086, 'double'],
33386
+ ProjFalseOriginNorthingGeoKey: [3087, 'double'],
33387
+ ProjCenterLongGeoKey: [3088, 'double'],
33388
+ ProjCenterLatGeoKey: [3089, 'double'],
33389
+ ProjScaleAtNatOriginGeoKey: [3092, 'double'],
33390
+ ProjScaleAtCenterGeoKey: [3093, 'double'],
33391
+ ProjAzimuthAngleGeoKey: [3094, 'double'],
33392
+ ProjStraightVertPoleLongGeoKey: [3095, 'double'],
33393
+ ProjRectifiedGridAngleGeoKey: [3096, 'double']
33394
+ };
33395
+
33396
+ var COMPRESSION_NONE = 1;
33397
+ var COMPRESSION_DEFLATE = 8;
33398
+ var PHOTOMETRIC_BLACK_IS_ZERO = 1;
33399
+ var PHOTOMETRIC_RGB = 2;
33400
+ var PLANAR_CONFIG_CHUNKY = 1; // band-interleaved, matching grid.samples
33401
+ var EXTRA_SAMPLE_UNSPECIFIED = 0;
33402
+ var EXTRA_SAMPLE_UNASSOCIATED_ALPHA = 2;
33403
+ var SAMPLE_FORMAT_UINT = 1;
33404
+ var SAMPLE_FORMAT_INT = 2;
33405
+ var SAMPLE_FORMAT_FLOAT = 3;
33406
+
33407
+ // Rows are grouped into strips of about this size, so that a large raster is
33408
+ // compressed in independent chunks instead of one enormous one. Each strip
33409
+ // costs 8 bytes of tag data, so this trades a small header against the memory
33410
+ // a single compression call has to hold.
33411
+ var TARGET_STRIP_BYTES = 256 * 1024;
33412
+
33413
+ // grid: a mapshaper raster grid (width, height, bands, samples, bbox, nodata)
33414
+ // opts.geoKeys: GeoTIFF geo keys to write, e.g. {ProjectedCSTypeGeoKey: 32615}
33415
+ // opts.compress: false to store samples uncompressed
33416
+ // Returns a Uint8Array holding the whole file.
33417
+ function encodeGeoTIFF(grid, opts) {
33418
+ opts = opts || {};
33419
+ var compress = opts.compress !== false;
33420
+ var rowsPerStrip = getRowsPerStrip(grid);
33421
+ var strips = encodeStrips(grid, rowsPerStrip, compress);
33422
+ var entries = getTagEntries(grid, strips, rowsPerStrip, compress, opts.geoKeys);
33423
+ var layout = getFileLayout(entries, strips);
33424
+ var bytes = new Uint8Array(layout.fileSize);
33425
+ var view = new DataView(bytes.buffer);
33426
+ setStripOffsets(entries, strips, layout.stripDataOffset);
33427
+ writeHeader(view);
33428
+ writeIfd(view, bytes, entries, layout);
33429
+ writeStrips(bytes, strips, layout.stripDataOffset);
33430
+ return bytes;
33431
+ }
33432
+
33433
+ function writeHeader(view) {
33434
+ view.setUint8(0, 0x49); // 'II' -- little-endian byte order
33435
+ view.setUint8(1, 0x49);
33436
+ view.setUint16(2, 42, true); // TIFF magic number
33437
+ view.setUint32(4, 8, true); // offset of the first IFD
33438
+ }
33439
+
33440
+ function writeIfd(view, bytes, entries, layout) {
33441
+ var offset = layout.ifdOffset;
33442
+ var valueOffset = layout.valueDataOffset;
33443
+ view.setUint16(offset, entries.length, true);
33444
+ offset += 2;
33445
+ entries.forEach(function(entry) {
33446
+ var size = getEntryDataSize(entry);
33447
+ view.setUint16(offset, entry.tag, true);
33448
+ view.setUint16(offset + 2, entry.type, true);
33449
+ view.setUint32(offset + 4, entry.count, true);
33450
+ if (size <= 4) {
33451
+ // Small values live in the entry itself, left-aligned in the 4 bytes.
33452
+ writeEntryValues(view, bytes, entry, offset + 8);
33453
+ } else {
33454
+ view.setUint32(offset + 8, valueOffset, true);
33455
+ writeEntryValues(view, bytes, entry, valueOffset);
33456
+ valueOffset += size + (size % 2); // value blocks start on a word boundary
33457
+ }
33458
+ offset += 12;
33459
+ });
33460
+ view.setUint32(offset, 0, true); // no second IFD
33461
+ }
33462
+
33463
+ function writeEntryValues(view, bytes, entry, offset) {
33464
+ var values = entry.values;
33465
+ if (entry.type == TIFF_TYPES.ASCII) {
33466
+ writeAsciiValue(bytes, values, offset);
33467
+ return;
33468
+ }
33469
+ for (var i = 0; i < values.length; i++) {
33470
+ if (entry.type == TIFF_TYPES.SHORT) {
33471
+ view.setUint16(offset + i * 2, values[i], true);
33472
+ } else if (entry.type == TIFF_TYPES.LONG) {
33473
+ view.setUint32(offset + i * 4, values[i], true);
33474
+ } else if (entry.type == TIFF_TYPES.DOUBLE) {
33475
+ view.setFloat64(offset + i * 8, values[i], true);
33476
+ } else {
33477
+ error('Unsupported TIFF field type:', entry.type);
33478
+ }
33479
+ }
33480
+ }
33481
+
33482
+ // ASCII fields are NUL-terminated, and the terminator is counted.
33483
+ function writeAsciiValue(bytes, str, offset) {
33484
+ for (var i = 0; i < str.length; i++) {
33485
+ bytes[offset + i] = str.charCodeAt(i) & 0x7f;
33486
+ }
33487
+ bytes[offset + str.length] = 0;
33488
+ }
33489
+
33490
+ function getEntryDataSize(entry) {
33491
+ return TYPE_SIZES[entry.type] * entry.count;
33492
+ }
33493
+
33494
+ function getFileLayout(entries, strips) {
33495
+ var ifdOffset = 8;
33496
+ var ifdSize = 2 + entries.length * 12 + 4;
33497
+ var valueDataOffset = ifdOffset + ifdSize;
33498
+ var valueDataSize = entries.reduce(function(memo, entry) {
33499
+ var size = getEntryDataSize(entry);
33500
+ return size <= 4 ? memo : memo + size + (size % 2);
33501
+ }, 0);
33502
+ var stripDataOffset = valueDataOffset + valueDataSize;
33503
+ var stripDataSize = strips.reduce(function(memo, strip) {
33504
+ return memo + strip.length;
33505
+ }, 0);
33506
+ return {
33507
+ ifdOffset: ifdOffset,
33508
+ valueDataOffset: valueDataOffset,
33509
+ stripDataOffset: stripDataOffset,
33510
+ fileSize: stripDataOffset + stripDataSize
33511
+ };
33512
+ }
33513
+
33514
+ // StripOffsets can only be filled in once the size of the tag data is known.
33515
+ function setStripOffsets(entries, strips, stripDataOffset) {
33516
+ var entry = findEntry(entries, TAGS.StripOffsets);
33517
+ var offset = stripDataOffset;
33518
+ strips.forEach(function(strip, i) {
33519
+ entry.values[i] = offset;
33520
+ offset += strip.length;
33521
+ });
33522
+ }
33523
+
33524
+ function writeStrips(bytes, strips, stripDataOffset) {
33525
+ var offset = stripDataOffset;
33526
+ strips.forEach(function(strip) {
33527
+ bytes.set(strip, offset);
33528
+ offset += strip.length;
33529
+ });
33530
+ }
33531
+
33532
+ function findEntry(entries, tag) {
33533
+ return entries.filter(function(entry) {
33534
+ return entry.tag == tag;
33535
+ })[0];
33536
+ }
33537
+
33538
+ function encodeStrips(grid, rowsPerStrip, compress) {
33539
+ var sampleBytes = getSampleBytes(grid.samples);
33540
+ var bytesPerRow = getBytesPerRow(grid);
33541
+ var strips = [];
33542
+ var row = 0;
33543
+ var start, end, chunk;
33544
+ while (row < grid.height) {
33545
+ start = row * bytesPerRow;
33546
+ end = Math.min(start + rowsPerStrip * bytesPerRow, sampleBytes.length);
33547
+ chunk = sampleBytes.subarray(start, end);
33548
+ strips.push(compress ? deflateSync(chunk) : chunk);
33549
+ row += rowsPerStrip;
33550
+ }
33551
+ return strips;
33552
+ }
33553
+
33554
+ function getRowsPerStrip(grid) {
33555
+ var rows = Math.floor(TARGET_STRIP_BYTES / Math.max(getBytesPerRow(grid), 1));
33556
+ return Math.max(1, Math.min(rows, grid.height));
33557
+ }
33558
+
33559
+ function getBytesPerRow(grid) {
33560
+ return getBytesPerSample(grid.samples) * grid.bands * grid.width;
33561
+ }
33562
+
33563
+ // TIFF stores samples in the file's byte order, which is little-endian here.
33564
+ // Typed arrays use the platform's order, so a byte view can be handed straight
33565
+ // to the encoder on a little-endian machine (all common ones) and needs
33566
+ // swapping otherwise.
33567
+ function getSampleBytes(samples) {
33568
+ var bytes = new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength);
33569
+ var size = getBytesPerSample(samples);
33570
+ if (size == 1 || platformIsLittleEndian()) return bytes;
33571
+ return swapByteOrder(bytes, size);
33572
+ }
33573
+
33574
+ function platformIsLittleEndian() {
33575
+ var probe = new Uint16Array([1]);
33576
+ return new Uint8Array(probe.buffer)[0] === 1;
33577
+ }
33578
+
33579
+ function swapByteOrder(bytes, size) {
33580
+ var swapped = new Uint8Array(bytes.length);
33581
+ for (var i = 0; i < bytes.length; i += size) {
33582
+ for (var j = 0; j < size; j++) {
33583
+ swapped[i + j] = bytes[i + size - 1 - j];
33584
+ }
33585
+ }
33586
+ return swapped;
33587
+ }
33588
+
33589
+ function getBytesPerSample(samples) {
33590
+ return samples.BYTES_PER_ELEMENT || 1;
33591
+ }
33592
+
33593
+ function getSampleFormat(samples) {
33594
+ if (samples instanceof Float32Array || samples instanceof Float64Array) {
33595
+ return SAMPLE_FORMAT_FLOAT;
33596
+ }
33597
+ if (samples instanceof Int8Array || samples instanceof Int16Array ||
33598
+ samples instanceof Int32Array) {
33599
+ return SAMPLE_FORMAT_INT;
33600
+ }
33601
+ if (samples instanceof Uint8Array || samples instanceof Uint8ClampedArray ||
33602
+ samples instanceof Uint16Array || samples instanceof Uint32Array) {
33603
+ return SAMPLE_FORMAT_UINT;
33604
+ }
33605
+ error('Unsupported raster sample type');
33606
+ }
33607
+
33608
+ function getTagEntries(grid, strips, rowsPerStrip, compress, geoKeys) {
33609
+ var bands = grid.bands;
33610
+ var bytesPerSample = getBytesPerSample(grid.samples);
33611
+ var entries = [
33612
+ long(TAGS.ImageWidth, [grid.width]),
33613
+ long(TAGS.ImageLength, [grid.height]),
33614
+ short(TAGS.BitsPerSample, repeat(bytesPerSample * 8, bands)),
33615
+ short(TAGS.Compression, [compress ? COMPRESSION_DEFLATE : COMPRESSION_NONE]),
33616
+ short(TAGS.PhotometricInterpretation, [getPhotometricInterpretation$1(bands)]),
33617
+ long(TAGS.StripOffsets, repeat(0, strips.length)), // filled in after layout
33618
+ short(TAGS.SamplesPerPixel, [bands]),
33619
+ long(TAGS.RowsPerStrip, [rowsPerStrip]),
33620
+ long(TAGS.StripByteCounts, strips.map(function(strip) {
33621
+ return strip.length;
33622
+ })),
33623
+ short(TAGS.PlanarConfiguration, [PLANAR_CONFIG_CHUNKY]),
33624
+ ascii(TAGS.Software, 'mapshaper'),
33625
+ short(TAGS.SampleFormat, repeat(getSampleFormat(grid.samples), bands))
33626
+ ];
33627
+ var extraSamples = getExtraSamples(bands);
33628
+ if (extraSamples) {
33629
+ entries.push(short(TAGS.ExtraSamples, extraSamples));
33630
+ }
33631
+ entries = entries.concat(getGeoreferencingEntries(grid, geoKeys));
33632
+ if (grid.nodata !== null && grid.nodata !== undefined) {
33633
+ // GDAL stores this value as text, and reads it back with a float parser
33634
+ // that accepts both integers and "nan".
33635
+ entries.push(ascii(TAGS.GDAL_NODATA, String(grid.nodata)));
33636
+ }
33637
+ return entries.sort(function(a, b) {
33638
+ return a.tag - b.tag;
33639
+ });
33640
+ }
33641
+
33642
+ // Three or more bands are rendered as RGB(A) elsewhere in mapshaper, so they
33643
+ // are tagged that way here; anything else is a measurement raster whose first
33644
+ // band is the one to display.
33645
+ function getPhotometricInterpretation$1(bands) {
33646
+ return bands >= 3 ? PHOTOMETRIC_RGB : PHOTOMETRIC_BLACK_IS_ZERO;
33647
+ }
33648
+
33649
+ // Bands beyond the ones the photometric interpretation accounts for have to be
33650
+ // declared as extra samples. A fourth band is treated as alpha, matching the
33651
+ // renderer; other extras are left unspecified rather than guessing at them.
33652
+ function getExtraSamples(bands) {
33653
+ var colorBands = bands >= 3 ? 3 : 1;
33654
+ var extras = repeat(EXTRA_SAMPLE_UNSPECIFIED, bands - colorBands);
33655
+ if (extras.length === 0) return null;
33656
+ if (bands == 4) extras[0] = EXTRA_SAMPLE_UNASSOCIATED_ALPHA;
33657
+ return extras;
33658
+ }
33659
+
33660
+ function getGeoreferencingEntries(grid, geoKeys) {
33661
+ var bbox = grid.bbox;
33662
+ var dx = Math.abs(bbox[2] - bbox[0]) / grid.width;
33663
+ var dy = Math.abs(bbox[3] - bbox[1]) / grid.height;
33664
+ var entries = [
33665
+ // Pixel scale is unsigned; the tiepoint anchors the grid at its top-left
33666
+ // corner, which is where row 0 of the samples begins.
33667
+ double(TAGS.ModelPixelScale, [dx, dy, 0]),
33668
+ double(TAGS.ModelTiepoint, [0, 0, 0, bbox[0], bbox[3], 0])
33669
+ ];
33670
+ return entries.concat(getGeoKeyEntries(geoKeys));
33671
+ }
33672
+
33673
+ // The geo keys live in a directory tag of their own: a four-short header
33674
+ // (version, revision, minor revision, key count) followed by four shorts per
33675
+ // key. A short-valued key keeps its value in the directory; a double-valued one
33676
+ // points into the GeoDoubleParams tag, which holds them all end to end.
33677
+ function getGeoKeyEntries(geoKeys) {
33678
+ var names = Object.keys(geoKeys || {}).filter(function(name) {
33679
+ return name in GEO_KEYS && isFinite(geoKeys[name]);
33680
+ }).sort(function(a, b) {
33681
+ return GEO_KEYS[a][0] - GEO_KEYS[b][0];
33682
+ });
33683
+ var values = [1, 1, 0, names.length];
33684
+ var doubles = [];
33685
+ if (names.length === 0) return [];
33686
+ names.forEach(function(name) {
33687
+ var id = GEO_KEYS[name][0];
33688
+ if (GEO_KEYS[name][1] == 'double') {
33689
+ values.push(id, TAGS.GeoDoubleParams, 1, doubles.length);
33690
+ doubles.push(geoKeys[name]);
33691
+ } else {
33692
+ values.push(id, 0, 1, geoKeys[name]);
33693
+ }
33694
+ });
33695
+ var entries = [short(TAGS.GeoKeyDirectory, values)];
33696
+ if (doubles.length > 0) {
33697
+ entries.push(double(TAGS.GeoDoubleParams, doubles));
33698
+ }
33699
+ return entries;
33700
+ }
33701
+
33702
+ function repeat(val, count) {
33703
+ var arr = [];
33704
+ for (var i = 0; i < count; i++) arr.push(val);
33705
+ return arr;
33706
+ }
33707
+
33708
+ function short(tag, values) {
33709
+ return {tag: tag, type: TIFF_TYPES.SHORT, count: values.length, values: values};
33710
+ }
33711
+
33712
+ function long(tag, values) {
33713
+ return {tag: tag, type: TIFF_TYPES.LONG, count: values.length, values: values};
33714
+ }
33715
+
33716
+ function double(tag, values) {
33717
+ return {tag: tag, type: TIFF_TYPES.DOUBLE, count: values.length, values: values};
33718
+ }
33719
+
33720
+ function ascii(tag, str) {
33721
+ return {tag: tag, type: TIFF_TYPES.ASCII, count: str.length + 1, values: str};
33722
+ }
33723
+
33724
+ var GeoTIFFEncode = /*#__PURE__*/Object.freeze({
33725
+ __proto__: null,
33726
+ encodeGeoTIFF: encodeGeoTIFF
33727
+ });
33728
+
33729
+ // Translate between mapshaper's CRS objects and the GeoTIFF geo keys that
33730
+ // describe a projection, in both directions.
33731
+ //
33732
+ // A GeoTIFF names a projection either by an authority code or by spelling it
33733
+ // out: a coordinate transformation code plus the parameters it takes. This
33734
+ // module handles the second, longer form, which is the only way to write a
33735
+ // projection that has no EPSG code, and the only way to read one back.
33736
+ //
33737
+ // The key-by-key layout follows what GDAL writes for the same projection, since
33738
+ // that is what other software is used to reading.
33739
+
33740
+
33741
+ var R2D$1 = 180 / Math.PI;
33742
+
33743
+ // The coordinate transformation codes of the GeoTIFF spec, and where each
33744
+ // projection's parameters go. Projections missing from this table, including
33745
+ // mapshaper's interrupted and polyhedral ones, have no GeoTIFF representation.
33746
+ //
33747
+ // ct: the ProjCoordTransGeoKey code
33748
+ // proj: the proj4 name to read the code back as, when it differs from the entry
33749
+ // origin: the geo keys that carry lon_0 and lat_0
33750
+ // falseXY: the pair that carries x_0 and y_0
33751
+ // scale: the key that carries k_0, for projections that take one
33752
+ // parallels: whether the standard-parallel keys carry lat_ts or lat_1 and lat_2
33753
+ // azimuth: the projection is skewed by an +alpha angle
33754
+ // encodeOnly: another entry with the same code is the one to read it back as
33755
+ var PROJECTIONS = {
33756
+ tmerc: {ct: 1, origin: 'nat', falseXY: 'nat', scale: 'nat'},
33757
+ utm: {ct: 1, origin: 'nat', falseXY: 'nat', scale: 'nat', proj: 'tmerc', encodeOnly: true},
33758
+ // Oblique Mercator comes in two variants, which differ in whether the false
33759
+ // easting and northing are applied at the projection center or at the point
33760
+ // where its skewed axis crosses the equator. GDAL identifies the first by its
33761
+ // EPSG method code rather than by a GeoTIFF transformation code.
33762
+ omerc: {ct: 9815, origin: 'center', falseXY: 'nat', scale: 'center', azimuth: true},
33763
+ omercNoUoff: {ct: 3, origin: 'center', falseXY: 'nat', scale: 'center', azimuth: true, proj: 'omerc', noUoff: true},
33764
+ merc: {ct: 7, origin: 'nat', falseXY: 'nat', scale: 'nat'},
33765
+ // Mercator's scale can be set either by a scale factor at the equator or by a
33766
+ // parallel of true scale, and the two are alternatives.
33767
+ mercStdParallel: {ct: 7, origin: 'nat', falseXY: 'nat', parallels: 'lat_ts', proj: 'merc'},
33768
+ lcc: {ct: 8, origin: 'falseOrigin', falseXY: 'falseOrigin', parallels: 'lat_1_2'},
33769
+ // With a single standard parallel the cone touches the globe, and the scale
33770
+ // factor at that parallel becomes a parameter.
33771
+ lcc1sp: {ct: 9, origin: 'nat', falseXY: 'nat', scale: 'nat', proj: 'lcc', tangentParallel: true},
33772
+ laea: {ct: 10, origin: 'center', falseXY: 'nat'},
33773
+ aea: {ct: 11, origin: 'nat', falseXY: 'nat', parallels: 'lat_1_2'},
33774
+ aeqd: {ct: 12, origin: 'center', falseXY: 'nat'},
33775
+ eqdc: {ct: 13, origin: 'nat', falseXY: 'nat', parallels: 'lat_1_2'},
33776
+ stere: {ct: 14, origin: 'center', falseXY: 'nat', scale: 'nat'},
33777
+ sterePolar: {ct: 15, origin: 'polar', falseXY: 'nat', scale: 'nat', proj: 'stere'},
33778
+ sterea: {ct: 16, origin: 'nat', falseXY: 'nat', scale: 'nat'},
33779
+ eqc: {ct: 17, origin: 'center', falseXY: 'nat', parallels: 'lat_ts'},
33780
+ cass: {ct: 18, origin: 'nat', falseXY: 'nat'},
33781
+ gnom: {ct: 19, origin: 'center', falseXY: 'nat'},
33782
+ mill: {ct: 20, origin: 'center', falseXY: 'nat'},
33783
+ ortho: {ct: 21, origin: 'center', falseXY: 'nat'},
33784
+ poly: {ct: 22, origin: 'nat', falseXY: 'nat', scale: 'nat'},
33785
+ robin: {ct: 23, origin: 'centerLong', falseXY: 'nat'},
33786
+ sinu: {ct: 24, origin: 'centerLong', falseXY: 'nat'},
33787
+ vandg: {ct: 25, origin: 'centerLong', falseXY: 'nat'}
33788
+ };
33789
+
33790
+ var ORIGIN_KEYS = {
33791
+ nat: ['ProjNatOriginLongGeoKey', 'ProjNatOriginLatGeoKey'],
33792
+ center: ['ProjCenterLongGeoKey', 'ProjCenterLatGeoKey'],
33793
+ centerLong: ['ProjCenterLongGeoKey', null],
33794
+ falseOrigin: ['ProjFalseOriginLongGeoKey', 'ProjFalseOriginLatGeoKey'],
33795
+ // A polar stereographic projection's longitude is the meridian running down
33796
+ // the map, and its latitude is the parallel of true scale.
33797
+ polar: ['ProjStraightVertPoleLongGeoKey', 'ProjNatOriginLatGeoKey']
33798
+ };
33799
+
33800
+ var FALSE_XY_KEYS = {
33801
+ nat: ['ProjFalseEastingGeoKey', 'ProjFalseNorthingGeoKey'],
33802
+ falseOrigin: ['ProjFalseOriginEastingGeoKey', 'ProjFalseOriginNorthingGeoKey']
33803
+ };
33804
+
33805
+ var SCALE_KEYS = {
33806
+ nat: 'ProjScaleAtNatOriginGeoKey',
33807
+ center: 'ProjScaleAtCenterGeoKey'
33808
+ };
33809
+
33810
+ // Geographic CRS codes for the datums that mapshaper can name.
33811
+ var DATUM_CODES = {
33812
+ WGS84: 4326,
33813
+ NAD83: 4269,
33814
+ NAD27: 4267
33815
+ };
33816
+
33817
+ var LINEAR_UNIT_CODES = [
33818
+ [1, 9001], // metre
33819
+ [0.3048, 9002], // international foot
33820
+ [0.3048006096012192, 9003] // US survey foot
33821
+ ];
33822
+
33823
+ var USER_DEFINED = 32767;
33824
+ var ANGULAR_DEGREE = 9102;
33825
+ var MODEL_TYPE_PROJECTED$1 = 1;
33826
+ var MODEL_TYPE_GEOGRAPHIC$1 = 2;
33827
+
33828
+ // The proj4 parameters that describe a projection's shape and placement, as
33829
+ // opposed to the ellipsoid it sits on. These are the ones this module claims;
33830
+ // everything else in a proj4 string is left alone.
33831
+ var PROJECTION_PARAMS = ['proj', 'lat_0', 'lon_0', 'lonc', 'lat_1', 'lat_2',
33832
+ 'lat_ts', 'k', 'k_0', 'x_0', 'y_0', 'alpha', 'gamma', 'zone', 'south',
33833
+ 'no_uoff', 'no_off', 'no_rot'];
33834
+
33835
+ // Returns geo keys describing a CRS, or null if it has no GeoTIFF
33836
+ // representation. Callers add the raster-type key.
33837
+ function getCrsGeoKeys(crs) {
33838
+ var proj4 = getProj4(crs);
33839
+ var params = proj4 ? parseProj4Params(proj4) : null;
33840
+ // A mixed projection is several projections in one frame, which a GeoTIFF
33841
+ // cannot describe. Its proj4 string mentions only the main one, so it has to
33842
+ // be excluded by hand rather than by failing to match the table.
33843
+ if (!params || crs.__mixed_crs) return null;
33844
+ return crs.is_latlong ? getGeographicGeoKeys(crs, params) :
33845
+ getProjectedGeoKeys(crs, params);
33846
+ }
33847
+
33848
+ function getProj4(crs) {
33849
+ try {
33850
+ return crsToProj4(crs);
33851
+ } catch(e) {
33852
+ return null;
33853
+ }
33854
+ }
33855
+
33856
+ function getGeographicGeoKeys(crs, params) {
33857
+ return Object.assign({
33858
+ GTModelTypeGeoKey: MODEL_TYPE_GEOGRAPHIC$1
33859
+ }, getDatumGeoKeys(crs, params));
33860
+ }
33861
+
33862
+ function getProjectedGeoKeys(crs, params) {
33863
+ var defn = getEncodingDefn(crs, params);
33864
+ var keys;
33865
+ if (!defn) return null;
33866
+ keys = Object.assign({
33867
+ GTModelTypeGeoKey: MODEL_TYPE_PROJECTED$1
33868
+ }, getDatumGeoKeys(crs, params), {
33869
+ // The projection is spelled out in the keys below rather than named by a
33870
+ // code, which is what "user-defined" announces.
33871
+ ProjectedCSTypeGeoKey: USER_DEFINED,
33872
+ ProjectionGeoKey: USER_DEFINED,
33873
+ ProjCoordTransGeoKey: defn.ct
33874
+ }, getLinearUnitGeoKeys(crs));
33875
+ Object.assign(keys, getProjectionParamGeoKeys(crs, params, defn));
33876
+ return keys;
33877
+ }
33878
+
33879
+ // Picks the table entry for a projection, including the cases where one proj4
33880
+ // name maps to two coordinate transformations.
33881
+ function getEncodingDefn(crs, params) {
33882
+ var name = params.proj;
33883
+ if (name == 'lcc' && !hasSecondParallel(params)) {
33884
+ name = 'lcc1sp';
33885
+ } else if (name == 'stere' && isPolarLatitude(crs.phi0 * R2D$1)) {
33886
+ name = 'sterePolar';
33887
+ } else if (name == 'merc' && isFinite(params.lat_ts)) {
33888
+ name = 'mercStdParallel';
33889
+ } else if (name == 'omerc' && params.no_uoff) {
33890
+ name = 'omercNoUoff';
33891
+ }
33892
+ return PROJECTIONS[name] || null;
33893
+ }
33894
+
33895
+ function hasSecondParallel(params) {
33896
+ return isFinite(params.lat_2) && params.lat_2 !== params.lat_1;
33897
+ }
33898
+
33899
+ function isPolarLatitude(deg) {
33900
+ return Math.abs(deg) > 89.999;
33901
+ }
33902
+
33903
+ function getProjectionParamGeoKeys(crs, params, defn) {
33904
+ var origin = ORIGIN_KEYS[defn.origin];
33905
+ var falseXY = FALSE_XY_KEYS[defn.falseXY];
33906
+ var keys = {};
33907
+ keys[origin[0]] = getOriginLongitude(crs, params, defn);
33908
+ if (origin[1]) keys[origin[1]] = getOriginLatitude(crs, params, defn);
33909
+ keys[falseXY[0]] = crs.x0;
33910
+ keys[falseXY[1]] = crs.y0;
33911
+ if (defn.scale) keys[SCALE_KEYS[defn.scale]] = crs.k0;
33912
+ if (defn.parallels == 'lat_ts') {
33913
+ keys.ProjStdParallel1GeoKey = getTrueScaleLatitude(params);
33914
+ } else if (defn.parallels == 'lat_1_2') {
33915
+ keys.ProjStdParallel1GeoKey = isFinite(params.lat_1) ? params.lat_1 : 0;
33916
+ keys.ProjStdParallel2GeoKey = isFinite(params.lat_2) ? params.lat_2 : params.lat_1;
33917
+ }
33918
+ if (defn.azimuth) {
33919
+ keys.ProjAzimuthAngleGeoKey = params.alpha;
33920
+ // The angle from the skew axis to the map's vertical. Absent, it equals the
33921
+ // azimuth, which is how proj4 treats it too.
33922
+ keys.ProjRectifiedGridAngleGeoKey = isFinite(params.gamma) ?
33923
+ params.gamma : params.alpha;
33924
+ }
33925
+ return keys;
33926
+ }
33927
+
33928
+ // An oblique Mercator's center longitude is given by +lonc, which proj4 keeps
33929
+ // separate from the +lon_0 of other projections.
33930
+ function getOriginLongitude(crs, params, defn) {
33931
+ if (defn.azimuth && isFinite(params.lonc)) return params.lonc;
33932
+ return degrees$1(crs.lam0);
33933
+ }
33934
+
33935
+ // A polar stereographic projection is anchored at a pole, so the latitude in
33936
+ // its keys is the parallel of true scale rather than the origin.
33937
+ function getOriginLatitude(crs, params, defn) {
33938
+ if (defn.origin == 'polar' && isFinite(params.lat_ts)) return params.lat_ts;
33939
+ return degrees$1(crs.phi0);
33940
+ }
33941
+
33942
+ // An angle in degrees, cleaned up: the CRS object holds radians, so a whole
33943
+ // number of degrees comes back as 96.00000000000001 without this. Nine decimal
33944
+ // places is a fraction of a millimeter on the ground.
33945
+ function degrees$1(rad) {
33946
+ return round(rad * R2D$1, 9);
33947
+ }
33948
+
33949
+ function round(val, digits) {
33950
+ var k = Math.pow(10, digits);
33951
+ return Math.round(val * k) / k;
33952
+ }
33953
+
33954
+ function getTrueScaleLatitude(params) {
33955
+ if (isFinite(params.lat_ts)) return params.lat_ts;
33956
+ if (isFinite(params.lat_1)) return params.lat_1;
33957
+ return 0;
33958
+ }
33959
+
33960
+ // The ellipsoid and datum, which a projected CRS carries as well as a
33961
+ // geographic one.
33962
+ function getDatumGeoKeys(crs, params) {
33963
+ var code = DATUM_CODES[String(params.datum).toUpperCase()] || 0;
33964
+ var keys = {
33965
+ GeographicTypeGeoKey: code || USER_DEFINED,
33966
+ GeogAngularUnitsGeoKey: ANGULAR_DEGREE,
33967
+ // Written even when the datum is named by a code, so that a reader which
33968
+ // does not recognize the code still has the shape of the earth.
33969
+ GeogSemiMajorAxisGeoKey: crs.a,
33970
+ GeogPrimeMeridianLongGeoKey: degrees$1(crs.from_greenwich || 0)
33971
+ };
33972
+ if (!code) {
33973
+ keys.GeogGeodeticDatumGeoKey = USER_DEFINED;
33974
+ keys.GeogEllipsoidGeoKey = USER_DEFINED;
33975
+ }
33976
+ if (crs.es > 0) {
33977
+ keys.GeogInvFlatteningGeoKey = round(1 / (1 - Math.sqrt(1 - crs.es)), 9);
33978
+ } else {
33979
+ keys.GeogSemiMinorAxisGeoKey = crs.a; // a sphere
33980
+ }
33981
+ return keys;
33982
+ }
33983
+
33984
+ function getLinearUnitGeoKeys(crs) {
33985
+ var size = crs.to_meter || 1;
33986
+ var match = LINEAR_UNIT_CODES.filter(function(pair) {
33987
+ return Math.abs(pair[0] - size) < 1e-12;
33988
+ })[0];
33989
+ if (match) return {ProjLinearUnitsGeoKey: match[1]};
33990
+ return {
33991
+ ProjLinearUnitsGeoKey: USER_DEFINED,
33992
+ ProjLinearUnitSizeGeoKey: size
33993
+ };
33994
+ }
33995
+
33996
+ // Reads back the projection that getCrsGeoKeys() writes: returns the proj4
33997
+ // parameters naming the projection and placing it on the globe, as a string, or
33998
+ // null if the keys describe a projection this module does not know.
33999
+ //
34000
+ // The ellipsoid, datum and units are not included, because a caller reading a
34001
+ // file written by other software has better sources for those.
34002
+ function getGeoKeyProjection(keys) {
34003
+ var defn = getDecodingDefn(keys);
34004
+ var origin, falseXY, params;
34005
+ if (!defn) return null;
34006
+ origin = ORIGIN_KEYS[defn.origin];
34007
+ falseXY = FALSE_XY_KEYS[defn.falseXY];
34008
+ params = {proj: defn.proj || getProjectionName(defn)};
34009
+ addProjParam(params, defn.azimuth ? 'lonc' : 'lon_0', keys[origin[0]]);
34010
+ if (origin[1]) {
34011
+ Object.assign(params, getDecodedLatitude(keys[origin[1]], defn));
34012
+ }
34013
+ addProjParam(params, 'x_0', keys[falseXY[0]]);
34014
+ addProjParam(params, 'y_0', keys[falseXY[1]]);
34015
+ if (defn.scale) addProjParam(params, 'k_0', keys[SCALE_KEYS[defn.scale]]);
34016
+ if (defn.parallels == 'lat_ts') {
34017
+ addProjParam(params, 'lat_ts', keys.ProjStdParallel1GeoKey);
34018
+ } else if (defn.parallels == 'lat_1_2') {
34019
+ addProjParam(params, 'lat_1', keys.ProjStdParallel1GeoKey);
34020
+ addProjParam(params, 'lat_2', keys.ProjStdParallel2GeoKey);
34021
+ }
34022
+ if (defn.azimuth) {
34023
+ addProjParam(params, 'alpha', keys.ProjAzimuthAngleGeoKey);
34024
+ addProjParam(params, 'gamma', keys.ProjRectifiedGridAngleGeoKey);
34025
+ if (defn.noUoff) params.no_uoff = true;
34026
+ }
34027
+ return formatProj4Params(params);
34028
+ }
34029
+
34030
+ function getDecodingDefn(keys) {
34031
+ var ct = keys && keys.ProjCoordTransGeoKey;
34032
+ var names;
34033
+ if (!ct || ct == USER_DEFINED) return null;
34034
+ if (ct == PROJECTIONS.merc.ct) {
34035
+ return isFinite(keys.ProjStdParallel1GeoKey) ?
34036
+ PROJECTIONS.mercStdParallel : PROJECTIONS.merc;
34037
+ }
34038
+ names = Object.keys(PROJECTIONS).filter(function(name) {
34039
+ return PROJECTIONS[name].ct === ct && !PROJECTIONS[name].encodeOnly;
34040
+ });
34041
+ return names.length == 1 ? PROJECTIONS[names[0]] : null;
34042
+ }
34043
+
34044
+ function getProjectionName(defn) {
34045
+ return Object.keys(PROJECTIONS).filter(function(name) {
34046
+ return PROJECTIONS[name] === defn;
34047
+ })[0];
34048
+ }
34049
+
34050
+ // The latitude in a polar stereographic projection's keys is the parallel of
34051
+ // true scale; which pole the map is centered on follows from its sign.
34052
+ function getDecodedLatitude(val, defn) {
34053
+ var params = {};
34054
+ if (!isFinite(val)) return params;
34055
+ if (defn.origin == 'polar') {
34056
+ params.lat_ts = val;
34057
+ params.lat_0 = val < 0 ? -90 : 90;
34058
+ } else {
34059
+ params.lat_0 = val;
34060
+ }
34061
+ if (defn.tangentParallel) {
34062
+ // A one-parallel conic touches the globe at its origin latitude, which
34063
+ // proj4 wants stated as the standard parallel.
34064
+ params.lat_1 = params.lat_0;
34065
+ }
34066
+ return params;
34067
+ }
34068
+
34069
+ function addProjParam(params, name, val) {
34070
+ if (isFinite(val)) params[name] = val;
34071
+ }
34072
+
34073
+ // Replaces the projection parameters of a proj4 string with a different set,
34074
+ // leaving its ellipsoid, datum and unit parameters in place. Used to correct a
34075
+ // projection that another parser has read loosely.
34076
+ function replaceProj4Projection(str, projectionStr) {
34077
+ var params = parseProj4Params(str) || {};
34078
+ var kept = {};
34079
+ Object.keys(params).forEach(function(name) {
34080
+ if (PROJECTION_PARAMS.indexOf(name) == -1) kept[name] = params[name];
34081
+ });
34082
+ return projectionStr + ' ' + formatProj4Params(kept);
34083
+ }
34084
+
34085
+ function formatProj4Params(params) {
34086
+ return Object.keys(params).map(function(name) {
34087
+ return params[name] === true ? '+' + name : '+' + name + '=' + params[name];
34088
+ }).join(' ');
34089
+ }
34090
+
34091
+ // Reads the parameters that mproj keeps inside a projection's own state, and so
34092
+ // does not expose on the CRS object, out of the proj4 string that describes it.
34093
+ // Numeric values come back as numbers, flags as true.
34094
+ function parseProj4Params(str) {
34095
+ var params = {};
34096
+ String(str).split(/\s+/).forEach(function(part) {
34097
+ var match = /^\+([a-z_0-9]+)(?:=(.*))?$/i.exec(part);
34098
+ var val;
34099
+ if (!match) return;
34100
+ val = match[2];
34101
+ params[match[1]] = val === undefined ? true :
34102
+ val !== '' && isFinite(Number(val)) ? Number(val) : val;
34103
+ });
34104
+ return params.proj ? params : null;
34105
+ }
34106
+
34107
+ var GeoTIFFGeoKeys = /*#__PURE__*/Object.freeze({
34108
+ __proto__: null,
34109
+ getCrsGeoKeys: getCrsGeoKeys,
34110
+ getGeoKeyProjection: getGeoKeyProjection,
34111
+ replaceProj4Projection: replaceProj4Projection
34112
+ });
34113
+
34114
+ // GeoTIFF's model type: is the raster referenced to a projected or a
34115
+ // geographic CRS?
34116
+ var MODEL_TYPE_PROJECTED = 1;
34117
+ var MODEL_TYPE_GEOGRAPHIC = 2;
34118
+ // The raster covers areas, i.e. a coordinate refers to a pixel's corner rather
34119
+ // than its center. This matches how mapshaper's grid bbox is defined.
34120
+ var RASTER_TYPE_AREA = 1;
34121
+
34122
+ function exportGeoTIFF(dataset, opts) {
34123
+ var extension = opts.extension ||
34124
+ (opts.file ? getFileExtension(opts.file) : '') || 'tif';
34125
+ var crs = getOutputCrs(dataset);
34126
+ var files = [];
34127
+ dataset.layers.forEach(function(lyr) {
34128
+ var filename = lyr.name + '.' + extension;
34129
+ files.push({
34130
+ filename: filename,
34131
+ content: encodeRasterLayer(lyr, crs, opts)
34132
+ });
34133
+ // A projection that GeoTIFF has no way to describe travels in a sidecar
34134
+ // file instead.
34135
+ if (crs.code || crs.geoKeys) return;
34136
+ if (crs.wkt) {
34137
+ files.push({
34138
+ filename: getAuxFilename(filename),
34139
+ content: formatAuxXml(crs.wkt)
34140
+ });
34141
+ message('Wrote the CRS of', filename, 'to a', AUX_EXT, 'file, because GeoTIFF has no way to describe this projection. Keep the two files together.');
34142
+ } else {
34143
+ message('Wrote', filename, 'without CRS metadata (mapshaper could not derive it for this dataset). Other software will read the coordinates without knowing what they refer to.');
34144
+ }
34145
+ });
34146
+ return files;
34147
+ }
34148
+
34149
+ function encodeRasterLayer(lyr, crs, opts) {
34150
+ var grid = layerHasRaster(lyr) ? getRasterGrid(lyr.raster) : null;
34151
+ if (!grid || !grid.samples) {
34152
+ stop$1('GeoTIFF output requires a raster layer with pixel data');
34153
+ }
34154
+ if (!grid.bbox) {
34155
+ stop$1('Unable to export a raster layer without georeferencing');
34156
+ }
34157
+ if (!(grid.width > 0 && grid.height > 0) ||
34158
+ grid.samples.length < grid.width * grid.height * grid.bands) {
34159
+ stop$1('Unable to export a raster layer with incomplete pixel data');
34160
+ }
34161
+ if (rasterGridIsRotated(grid)) {
34162
+ // Georeferencing is written as a tiepoint plus a pixel scale, which cannot
34163
+ // express rotation or skew.
34164
+ stop$1('Exporting a rotated or skewed raster is not supported');
34165
+ }
34166
+ return encodeGeoTIFF(grid, {
34167
+ geoKeys: getGeoKeys(crs),
34168
+ compress: getCompressionSetting(opts)
34169
+ });
34170
+ }
34171
+
34172
+ function getCompressionSetting(opts) {
34173
+ var arg = opts.compression;
34174
+ if (!arg || arg == 'deflate') return true;
34175
+ if (arg == 'none') return false;
34176
+ stop$1('Unsupported GeoTIFF compression:', arg);
34177
+ }
34178
+
34179
+ function getGeoKeys(crs) {
34180
+ var keys = {GTRasterTypeGeoKey: RASTER_TYPE_AREA};
34181
+ if (crs.code) {
34182
+ // An EPSG code says everything, including which datum realization the
34183
+ // coordinates are in, so it is preferred to spelling the projection out.
34184
+ keys.GTModelTypeGeoKey = crs.isLatLng ? MODEL_TYPE_GEOGRAPHIC : MODEL_TYPE_PROJECTED;
34185
+ keys[crs.isLatLng ? 'GeographicTypeGeoKey' : 'ProjectedCSTypeGeoKey'] = crs.code;
34186
+ return keys;
34187
+ }
34188
+ // Otherwise the projection is written out parameter by parameter. Failing
34189
+ // that, the raster is georeferenced but says nothing about what its
34190
+ // coordinates refer to, and the CRS goes in a sidecar file.
34191
+ return Object.assign(keys, crs.geoKeys || {});
34192
+ }
34193
+
34194
+ // Describe the dataset's CRS in the forms the writer can use: an EPSG code,
34195
+ // a set of geo keys spelling the projection out, and a WKT string for the
34196
+ // sidecar file. Returns {code, geoKeys, isLatLng, wkt}, any member of which may
34197
+ // be missing.
34198
+ function getOutputCrs(dataset) {
34199
+ var info = (dataset && dataset.info) || {};
34200
+ var crsInfo = tryGetCrsInfo(dataset);
34201
+ var crs = crsInfo && crsInfo.crs;
34202
+ var code = getEpsgCode(info, crs);
34203
+ return {
34204
+ code: code,
34205
+ geoKeys: code || !crs ? null : getCrsGeoKeys(crs),
34206
+ isLatLng: !!crs && isLatLngCRS(crs),
34207
+ wkt: getOutputWkt(info, crs)
34208
+ };
34209
+ }
34210
+
34211
+ function tryGetCrsInfo(dataset) {
34212
+ try {
34213
+ // Besides reading the dataset's CRS metadata, this infers WGS-84 from
34214
+ // lat-long-looking bounds.
34215
+ return getDatasetCrsInfo(dataset);
34216
+ } catch(e) {
34217
+ return null;
34218
+ }
34219
+ }
34220
+
34221
+ function getEpsgCode(info, crs) {
34222
+ var authority = parseAuthorityCodeString(info.crs_string) ||
34223
+ parseAuthorityCodeFromWkt(info.wkt1);
34224
+ if (authority && authority.org == 'EPSG') return authority.code;
34225
+ if (info.geopackage_crs &&
34226
+ String(info.geopackage_crs.organization || '').toUpperCase() == 'EPSG') {
34227
+ return info.geopackage_crs.organization_coordsys_id ||
34228
+ info.geopackage_crs.srs_id || null;
34229
+ }
34230
+ // The two CRSes that mapshaper can recognize from a projection definition
34231
+ // alone; they also cover its wgs84 and webmercator aliases, which arrive here
34232
+ // without a code.
34233
+ if (crs && isWGS84(crs)) return 4326;
34234
+ if (crs && isWebMercator(crs)) return 3857;
34235
+ return null;
34236
+ }
34237
+
34238
+ function getOutputWkt(info, crs) {
34239
+ if (info.wkt1) return info.wkt1; // the CRS as the user supplied it
34240
+ if (!crs) return null;
34241
+ try {
34242
+ // WKT2 carries more of a projection's parameters than WKT1 does.
34243
+ return crsToWkt2(crs) || crsToPrj(crs) || null;
34244
+ } catch(e) {
34245
+ return null;
34246
+ }
34247
+ }
34248
+
32773
34249
  function getOutputFormat(dataset, opts) {
32774
34250
  var outFile = opts.file || null,
32775
34251
  inFmt = dataset.info && dataset.info.input_formats && dataset.info.input_formats[0],
@@ -32815,6 +34291,8 @@ ${svg}
32815
34291
  format = 'flatgeobuf';
32816
34292
  } else if (ext == 'parquet' || ext == 'geoparquet') {
32817
34293
  format = 'geoparquet';
34294
+ } else if (ext == 'tif' || ext == 'tiff') {
34295
+ format = 'geotiff';
32818
34296
  } else if (ext == 'svg') {
32819
34297
  format = 'svg';
32820
34298
  } else if (ext == 'kml' || ext == 'kmz') {
@@ -32876,8 +34354,11 @@ ${svg}
32876
34354
  var format = getOutputFormat(datasets[0], opts);
32877
34355
  var files;
32878
34356
  validateRasterExportFormat(datasets, format);
32879
- if (format != 'geoparquet' && (opts.compression || opts.level !== undefined)) {
32880
- error('The compression= and level= options only apply to GeoParquet output');
34357
+ if (format != 'geoparquet' && opts.level !== undefined) {
34358
+ error('The level= option only applies to GeoParquet output');
34359
+ }
34360
+ if (format != 'geoparquet' && format != 'geotiff' && opts.compression) {
34361
+ error('The compression= option only applies to GeoParquet and GeoTIFF output');
32881
34362
  }
32882
34363
  if (format == PACKAGE_EXT) {
32883
34364
  opts = utils.defaults({compact: true}, opts);
@@ -32946,9 +34427,15 @@ ${svg}
32946
34427
  }
32947
34428
 
32948
34429
  function validateRasterExportFormat(datasets, format) {
34430
+ if (format == 'geotiff') {
34431
+ if (datasetsHaveVectorLayers(datasets)) {
34432
+ stop$1('GeoTIFF output requires raster layers; use -target to select them');
34433
+ }
34434
+ return;
34435
+ }
32949
34436
  if (!datasetsHaveRasterLayers(datasets)) return;
32950
34437
  if (format == 'svg' || format == PACKAGE_EXT) return;
32951
- stop$1('Raster layers can only be exported as SVG or ' + PACKAGE_EXT + ' files');
34438
+ stop$1('Raster layers can only be exported as GeoTIFF, SVG or ' + PACKAGE_EXT + ' files');
32952
34439
  }
32953
34440
 
32954
34441
  function datasetsHaveRasterLayers(datasets) {
@@ -32957,6 +34444,14 @@ ${svg}
32957
34444
  });
32958
34445
  }
32959
34446
 
34447
+ function datasetsHaveVectorLayers(datasets) {
34448
+ return datasets.some(function(dataset) {
34449
+ return dataset.layers && dataset.layers.some(function(lyr) {
34450
+ return !layerHasRaster(lyr);
34451
+ });
34452
+ });
34453
+ }
34454
+
32960
34455
  // Return an array of objects with 'filename' and 'content' members.
32961
34456
  //
32962
34457
  function exportFileContent(dataset, opts) {
@@ -33017,7 +34512,8 @@ ${svg}
33017
34512
  json: exportJSON,
33018
34513
  flatgeobuf: exportFlatGeobuf,
33019
34514
  svg: exportSVG,
33020
- kml: exportKML
34515
+ kml: exportKML,
34516
+ geotiff: exportGeoTIFF
33021
34517
  };
33022
34518
 
33023
34519
 
@@ -34014,7 +35510,7 @@ ${svg}
34014
35510
  });
34015
35511
 
34016
35512
  function isSupportedOutputFormat(fmt) {
34017
- var types = ['geojson', 'topojson', 'json', 'dsv', 'dbf', 'shapefile', 'svg', 'kml', PACKAGE_EXT, 'flatgeobuf', 'geopackage', 'geoparquet'];
35513
+ var types = ['geojson', 'topojson', 'json', 'dsv', 'dbf', 'shapefile', 'svg', 'kml', PACKAGE_EXT, 'flatgeobuf', 'geopackage', 'geoparquet', 'geotiff'];
34018
35514
  return types.indexOf(fmt) > -1;
34019
35515
  }
34020
35516
 
@@ -34032,6 +35528,7 @@ ${svg}
34032
35528
  flatgeobuf: 'Flatgeobuf',
34033
35529
  geopackage: 'GeoPackage',
34034
35530
  geoparquet: 'GeoParquet',
35531
+ geotiff: 'GeoTIFF',
34035
35532
  svg: 'SVG'
34036
35533
  }[fmt] || '';
34037
35534
  }
@@ -35447,7 +36944,7 @@ ${svg}
35447
36944
  }
35448
36945
  })
35449
36946
  .option('format', {
35450
- describe: 'options: shapefile,geojson,topojson,flatgeobuf,geopackage,geoparquet,json,dbf,csv,tsv,svg'
36947
+ describe: 'options: shapefile,geojson,topojson,flatgeobuf,geopackage,geoparquet,geotiff,json,dbf,csv,tsv,svg'
35451
36948
  })
35452
36949
  .option('target', targetOpt)
35453
36950
  .option('force', {
@@ -35641,12 +37138,16 @@ ${svg}
35641
37138
  describe: '[CSV] export numbers with decimal commas not points'
35642
37139
  })
35643
37140
  .option('compression', {
35644
- describe: '[GeoParquet] options: snappy,zstd,none (default is snappy)'
37141
+ describe: '[GeoParquet] snappy,zstd,none (default snappy) [GeoTIFF] deflate,none'
35645
37142
  })
35646
37143
  .option('level', {
35647
37144
  describe: '[GeoParquet] zstd compression level',
35648
37145
  type: 'integer'
35649
37146
  })
37147
+ .option('rowgroup', {
37148
+ describe: '[GeoParquet] rows per row group (default: sized by data)',
37149
+ type: 'integer'
37150
+ })
35650
37151
  .option('show-all', {
35651
37152
  type: 'flag',
35652
37153
  describe: '[Snapshot] show all layers in Web UI'
@@ -38916,11 +40417,13 @@ ${svg}
38916
40417
  // Import path data from a non-topological source (Shapefile, GeoJSON, etc)
38917
40418
  // in preparation for identifying topology.
38918
40419
  // @opts.reserved_points -- estimate of points in dataset, for pre-allocating buffers
40420
+ // (applied when the first path arrives, so that a layer of points -- which
40421
+ // keeps its coords in shapes, not in these buffers -- never allocates it)
38919
40422
  //
38920
40423
  function PathImporter(opts) {
38921
- var bufSize = opts.reserved_points > 0 ? opts.reserved_points : 20000,
38922
- xx = new Float64Array(bufSize),
38923
- yy = new Float64Array(bufSize),
40424
+ var reservedPoints = opts.reserved_points > 0 ? opts.reserved_points : 0,
40425
+ xx = new Float64Array(20000),
40426
+ yy = new Float64Array(20000),
38924
40427
  shapes = [],
38925
40428
  properties = [],
38926
40429
  nn = [],
@@ -39064,7 +40567,7 @@ ${svg}
39064
40567
 
39065
40568
  function checkBuffers(needed) {
39066
40569
  if (needed > xx.length) {
39067
- var newLen = Math.max(needed, Math.ceil(xx.length * 1.5));
40570
+ var newLen = Math.max(needed, reservedPoints, Math.ceil(xx.length * 1.5));
39068
40571
  xx = utils.extendBuffer(xx, newLen, pointId);
39069
40572
  yy = utils.extendBuffer(yy, newLen, pointId);
39070
40573
  }
@@ -40216,8 +41719,25 @@ ${svg}
40216
41719
  return fmt;
40217
41720
  }
40218
41721
 
41722
+ // Guess the number of coordinate pairs in a GeoJSON file from its size, so that
41723
+ // the importer can size its coordinate buffers up front instead of growing them
41724
+ // a dozen times over (each growth copies the coords accumulated so far and
41725
+ // leaves the previous buffer behind, which is what makes peak memory during
41726
+ // import roughly double the size of the coordinate data).
41727
+ //
41728
+ // A pair runs from ~16 bytes ("[-122.42,37.77],") to ~42 at full double
41729
+ // precision, so this sits in the middle and errs low: undershooting just means
41730
+ // the buffers resume growing from a larger starting point, while overshooting
41731
+ // allocates memory that is never used. An attribute-heavy file overshoots, but
41732
+ // only reserves more than the growth it replaces past ~69 bytes per pair.
41733
+ function estimateGeoJSONPointCount(byteLength) {
41734
+ return Math.round(byteLength / 32);
41735
+ }
41736
+
40219
41737
  function importGeoJSONFile(fileReader, opts) {
40220
- var importer = new GeoJSONParser(opts);
41738
+ var importer = new GeoJSONParser(utils.defaults({}, opts, {
41739
+ reserved_points: estimateGeoJSONPointCount(fileReader.size())
41740
+ }));
40221
41741
  // For collections, parseGeoJSON() returns the top-level object with its
40222
41742
  // features/geometries array nulled out but all other members intact.
40223
41743
  var obj = parseGeoJSON(fileReader, importer.parseObject);
@@ -41339,13 +42859,11 @@ ${svg}
41339
42859
  if (utils.isString(crs)) {
41340
42860
  return parseAuthorityString(crs);
41341
42861
  }
41342
- var match = parseAuthorityObject(crs.id);
41343
- if (match) return match;
41344
- if (crs.base_crs && utils.isObject(crs.base_crs)) {
41345
- match = parseAuthorityObject(crs.base_crs.id);
41346
- if (match) return match;
41347
- }
41348
- return null;
42862
+ // base_crs identifies the geographic CRS that a projected CRS is derived
42863
+ // from, not the projected CRS itself, so it can't stand in for a missing
42864
+ // top-level id -- doing so reinterpreted projected coordinates as lat/long.
42865
+ // Without an id the caller falls back to converting the PROJJSON to Proj4.
42866
+ return parseAuthorityObject(crs.id);
41349
42867
  }
41350
42868
 
41351
42869
  function parseAuthorityObject(id) {
@@ -41371,7 +42889,8 @@ ${svg}
41371
42889
  var dynamicImportModule = Function('id', 'return import(id)');
41372
42890
  var DEFAULT_MAX_IMPORT_PIXELS = 16e6;
41373
42891
 
41374
- async function importGeoTIFF(input, optsArg) {
42892
+ // aux: contents of the file's .aux.xml sidecar, if it has one
42893
+ async function importGeoTIFF(input, optsArg, aux) {
41375
42894
  var opts = optsArg || {};
41376
42895
  var geotiff = await loadGeoTIFFLib();
41377
42896
  var source = getGeoTIFFSource(input);
@@ -41389,7 +42908,7 @@ ${svg}
41389
42908
  raster: imported.raster
41390
42909
  }]
41391
42910
  };
41392
- await importGeoTIFFCrs(dataset, sourceImage);
42911
+ await importGeoTIFFCrs(dataset, sourceImage, aux);
41393
42912
  return dataset;
41394
42913
  }
41395
42914
 
@@ -41733,26 +43252,122 @@ ${svg}
41733
43252
  };
41734
43253
  }
41735
43254
 
41736
- async function importGeoTIFFCrs(dataset, image) {
43255
+ // Reads the CRS a GeoTIFF claims, if mapshaper can make sense of it. A file
43256
+ // with no usable CRS metadata still imports: the raster keeps the coordinates
43257
+ // in its own georeferencing, and only the things that need to know what those
43258
+ // coordinates mean (reprojecting, basemaps, measuring) are unavailable. So a
43259
+ // CRS that cannot be read is reported and set aside, never raised as an error.
43260
+ //
43261
+ // A projection GeoTIFF cannot describe travels beside the file in a .aux.xml
43262
+ // sidecar, which is read when the file's own geo keys come up empty. The keys
43263
+ // come first, matching how GDAL treats the two: the sidecar supplements a file
43264
+ // that says nothing, rather than overruling one that does.
43265
+ async function importGeoTIFFCrs(dataset, image, aux) {
41737
43266
  var crsInfo = await getGeoTIFFCrsInfo(image);
41738
- var crsString = crsInfo.crsString;
41739
- if (!crsString) {
41740
- warnOnce(getGeoTIFFCrsWarning(crsInfo));
43267
+ var fromFile = await readCrsString(crsInfo.crsString);
43268
+ // The sidecar usually states its CRS in WKT, which has to become a proj4
43269
+ // definition before it can be used like the file's own metadata.
43270
+ var sidecarSrs = fromFile || !aux ? null : parseAuxCrsString(aux.content);
43271
+ var sidecarWkt = auxCrsIsWkt(sidecarSrs) ? sidecarSrs : null;
43272
+ var sidecarString = sidecarWkt ? tryParseWktToProj(sidecarWkt) : sidecarSrs;
43273
+ var fromSidecar = await readCrsString(sidecarString);
43274
+ var found = fromFile || fromSidecar;
43275
+ if (!found) {
43276
+ dataset.info = dataset.info || {};
43277
+ reportMissingCrs(dataset, crsInfo, sidecarString);
41741
43278
  return;
41742
43279
  }
43280
+ setDatasetCrsInfo(dataset, {crs_string: found.crsString, crs: found.crs});
43281
+ if (found.assumedDatum) {
43282
+ message('This GeoTIFF describes its projection without naming a datum;',
43283
+ 'assuming WGS 84.');
43284
+ }
43285
+ if (fromSidecar) {
43286
+ message('Read the CRS from ' + (aux.filename || 'a ' + AUX_EXT + ' file') +
43287
+ ', because GeoTIFF has no way to describe this projection.');
43288
+ if (sidecarWkt) {
43289
+ // Keep the sidecar's own wording, so that exporting the raster again
43290
+ // writes back what was read rather than a re-derived approximation.
43291
+ dataset.info.wkt1 = sidecarWkt;
43292
+ }
43293
+ }
43294
+ }
43295
+
43296
+ // Returns {crsString, crs}, or null if there is no string or it cannot be used.
43297
+ async function readCrsString(str) {
43298
+ var crs, assumed;
43299
+ if (!str) return null;
41743
43300
  try {
41744
- await initProjLibrary({crs: crsString});
41745
- setDatasetCrsInfo(dataset, getCrsInfo(crsString));
43301
+ await initProjLibrary({crs: str});
41746
43302
  } catch(e) {
41747
- dataset.info = dataset.info || {};
41748
- warnOnce(getGeoTIFFCrsWarning(crsInfo, e.message || e));
43303
+ // A projection resource that would not load leaves the string to be
43304
+ // parsed with whatever is already known.
43305
+ }
43306
+ crs = tryParseCrsString(str);
43307
+ if (crs) return {crsString: str, crs: crs};
43308
+ // A projection with no earth to sit on is tried again on a WGS-84 one, the
43309
+ // datum mapshaper assumes for any data that does not name one.
43310
+ assumed = addAssumedDatum(str);
43311
+ crs = assumed ? tryParseCrsString(assumed) : null;
43312
+ return crs ? {crsString: assumed, crs: crs, assumedDatum: true} : null;
43313
+ }
43314
+
43315
+ // GeoTIFF metadata can describe a projection without saying what shape of earth
43316
+ // it applies to: the geo keys carry a transformation and its parameters, and
43317
+ // the datum keys are absent or name something the EPSG database does not have.
43318
+ // Since the geokeys-to-proj4 library ends every definition with +no_defs, proj
43319
+ // will not supply its usual default either, and the whole projection is lost
43320
+ // over a missing ellipsoid. Returns the definition with a WGS-84 datum added,
43321
+ // or null if it already has one (or is not a proj4 definition at all).
43322
+ function addAssumedDatum(str) {
43323
+ if (!/\+proj=/.test(str) || /\+(datum|ellps|a|b|R|nadgrids|init)=/.test(str)) {
43324
+ return null;
41749
43325
  }
43326
+ return str + ' +datum=WGS84';
43327
+ }
43328
+
43329
+ // What to say about a raster that arrives without a CRS. A file that simply has
43330
+ // no CRS metadata is the ordinary case for an image, and is treated the way a
43331
+ // vector file with no projection metadata is: coordinates in the decimal-degree
43332
+ // range are taken for WGS-84 lat-long when something needs to know, which is
43333
+ // worth saying but not worth warning about. Metadata that mapshaper could not
43334
+ // make sense of is a different matter, and keeps the warning.
43335
+ function reportMissingCrs(dataset, crsInfo, sidecarString) {
43336
+ if (crsInfo.absent && !sidecarString) {
43337
+ if (probablyDecimalDegreeBounds(getDatasetBounds(dataset))) {
43338
+ message('This GeoTIFF has no CRS metadata. Its coordinates are in the',
43339
+ 'decimal-degree range, so they are taken for WGS 84 lat-long.');
43340
+ } else {
43341
+ message('This GeoTIFF has no CRS metadata, so what its coordinates refer',
43342
+ 'to is unknown, and projecting it or showing it over a basemap are',
43343
+ 'unavailable. If you know the CRS, name it with -proj init=<crs>',
43344
+ 'crs=<crs>');
43345
+ }
43346
+ return;
43347
+ }
43348
+ warnOnce(getGeoTIFFCrsWarning(crsInfo,
43349
+ getUnusableCrsDetail(crsInfo.crsString, sidecarString)));
43350
+ }
43351
+
43352
+ function getUnusableCrsDetail(fileString, sidecarString) {
43353
+ var tried = [fileString, sidecarString].filter(Boolean);
43354
+ if (!tried.length) return null;
43355
+ return 'Unable to use projection ' + tried.join(' or ');
41750
43356
  }
41751
43357
 
41752
43358
  async function getGeoTIFFCrsInfo(image) {
41753
43359
  var keys = image.getGeoKeys && image.getGeoKeys() || {};
41754
43360
  var code = keys.ProjectedCSTypeGeoKey;
41755
43361
  var customProjection;
43362
+ if (!hasCrsGeoKeys(keys)) {
43363
+ // Asked about a file that says nothing, the geokeys-to-proj4 library
43364
+ // answers "+proj=longlat" rather than nothing, having taken the silence for
43365
+ // a geographic CRS. That is a guess about the coordinates, not something
43366
+ // read out of the file, and mapshaper makes it later and better, from the
43367
+ // coordinate range, in the same way it does for a vector file with no
43368
+ // projection metadata.
43369
+ return {crsString: null, absent: true};
43370
+ }
41756
43371
  if (isGeoTIFFAuthorityCode(code)) {
41757
43372
  return {crsString: 'EPSG:' + code};
41758
43373
  }
@@ -41770,21 +43385,58 @@ ${svg}
41770
43385
  return 'The GeoTIFF does not contain usable CRS data';
41771
43386
  }
41772
43387
 
43388
+ // Keys that say something about the CRS itself, as opposed to how the pixels
43389
+ // are laid out (GTRasterTypeGeoKey) or what the file calls things (the
43390
+ // citation keys).
43391
+ var CRS_GEO_KEYS = [
43392
+ 'GTModelTypeGeoKey',
43393
+ 'GeographicTypeGeoKey', 'GeogGeodeticDatumGeoKey', 'GeogEllipsoidGeoKey',
43394
+ 'GeogPrimeMeridianGeoKey', 'GeogSemiMajorAxisGeoKey',
43395
+ 'GeogSemiMinorAxisGeoKey', 'GeogInvFlatteningGeoKey',
43396
+ 'ProjectedCSTypeGeoKey', 'ProjectionGeoKey', 'ProjCoordTransGeoKey'
43397
+ ];
43398
+
43399
+ function hasCrsGeoKeys(keys) {
43400
+ return CRS_GEO_KEYS.some(function(name) {
43401
+ return keys[name] !== undefined && keys[name] !== null;
43402
+ });
43403
+ }
43404
+
41773
43405
  function isGeoTIFFAuthorityCode(code) {
41774
43406
  return code > 0 && code != 32767;
41775
43407
  }
41776
43408
 
43409
+ // Reads a projection that the file spells out in its geo keys, rather than
43410
+ // naming by a code.
43411
+ //
43412
+ // The bulk of the work is done by the geokeys-to-proj4 library, which knows the
43413
+ // EPSG database and so reads datums, ellipsoids and units thoroughly. Its
43414
+ // reading of the projection parameters themselves is loose, though: it drops
43415
+ // the central meridian of a polar stereographic projection, the azimuth of an
43416
+ // oblique Mercator, and the true-scale parallel of a Mercator. So the
43417
+ // projection part of its answer is replaced with mapshaper's own reading of the
43418
+ // same keys, for the projections mapshaper knows how to write.
41777
43419
  async function getGeoTIFFCustomProjection(keys) {
41778
43420
  var converted = await getGeoTIFFProj4FromGeoKeys(keys);
41779
- if (converted.crsString) return converted;
41780
- if (keys.ProjectedCSTypeGeoKey != 32767 && keys.ProjectionGeoKey != 32767) return converted;
41781
- if (keys.ProjCoordTransGeoKey == 11) {
41782
- return {
41783
- crsString: getGeoTIFFAlbersProjection(keys),
41784
- warning: converted.warning
41785
- };
43421
+ var projection = getGeoKeyProjection(keys);
43422
+ if (!projection) return converted;
43423
+ if (converted.crsString) {
43424
+ return {crsString: replaceProj4Projection(converted.crsString, projection)};
41786
43425
  }
41787
- return converted;
43426
+ return {
43427
+ crsString: getGeoTIFFEllipsoidProjection(keys, projection),
43428
+ warning: converted.warning
43429
+ };
43430
+ }
43431
+
43432
+ // A last resort for a file whose datum the library could not read: the
43433
+ // projection as mapshaper reads it, on an ellipsoid taken straight from the
43434
+ // geo keys.
43435
+ function getGeoTIFFEllipsoidProjection(keys, projection) {
43436
+ var units = getGeoTIFFLinearUnits(keys);
43437
+ var ellipsoid = getGeoTIFFEllipsoid(keys);
43438
+ if (!units || !ellipsoid) return null;
43439
+ return projection + ' ' + ellipsoid + ' ' + units;
41788
43440
  }
41789
43441
 
41790
43442
  async function getGeoTIFFProj4FromGeoKeys(keys) {
@@ -41813,27 +43465,6 @@ ${svg}
41813
43465
  });
41814
43466
  }
41815
43467
 
41816
- function getGeoTIFFAlbersProjection(keys) {
41817
- var units = getGeoTIFFLinearUnits(keys);
41818
- var ellipsoid = getGeoTIFFEllipsoid(keys);
41819
- if (!units || !ellipsoid ||
41820
- !isFinite(keys.ProjStdParallel1GeoKey) || !isFinite(keys.ProjStdParallel2GeoKey) ||
41821
- !isFinite(keys.ProjNatOriginLatGeoKey) || !isFinite(keys.ProjNatOriginLongGeoKey)) {
41822
- return null;
41823
- }
41824
- return [
41825
- '+proj=aea',
41826
- '+lat_1=' + keys.ProjStdParallel1GeoKey,
41827
- '+lat_2=' + keys.ProjStdParallel2GeoKey,
41828
- '+lat_0=' + keys.ProjNatOriginLatGeoKey,
41829
- '+lon_0=' + keys.ProjNatOriginLongGeoKey,
41830
- '+x_0=' + (keys.ProjFalseEastingGeoKey || 0),
41831
- '+y_0=' + (keys.ProjFalseNorthingGeoKey || 0),
41832
- ellipsoid,
41833
- units
41834
- ].join(' ');
41835
- }
41836
-
41837
43468
  function getGeoTIFFLinearUnits(keys) {
41838
43469
  var code = keys.ProjLinearUnitsGeoKey;
41839
43470
  if (!code || code == 9001) return '+units=m';
@@ -42979,6 +44610,14 @@ ${svg}
42979
44610
  data = obj.prj;
42980
44611
  dataset = {layers: [], info: {wkt1: data.content}};
42981
44612
 
44613
+ } else if (obj.aux) {
44614
+ // A sidecar on its own carries nothing but a CRS, which is still worth
44615
+ // reading: it can be the source of a -proj command, and importing the
44616
+ // raster it belongs to picks it up anyway.
44617
+ dataFmt = 'aux';
44618
+ data = obj.aux;
44619
+ dataset = {layers: [], info: getAuxCrsInfo(data.content)};
44620
+
42982
44621
  } else if (obj.kml) {
42983
44622
  dataFmt = 'kml';
42984
44623
  data = obj.kml;
@@ -43024,7 +44663,7 @@ ${svg}
43024
44663
  } else if (obj.geotiff) {
43025
44664
  dataFmt = 'geotiff';
43026
44665
  data = obj.geotiff;
43027
- dataset = await importGeoTIFF(data, opts);
44666
+ dataset = await importGeoTIFF(data, opts, obj.aux);
43028
44667
  } else if (obj.png || obj.jpeg) {
43029
44668
  dataFmt = obj.png ? 'png' : 'jpeg';
43030
44669
  data = obj[dataFmt];
@@ -43086,6 +44725,14 @@ ${svg}
43086
44725
  return dataset;
43087
44726
  }
43088
44727
 
44728
+ // What a raster sidecar amounts to on its own: a projection, stated where the
44729
+ // contents of a .prj file would go if it is WKT, as a CRS string otherwise.
44730
+ function getAuxCrsInfo(content) {
44731
+ var srs = parseAuxCrsString(content);
44732
+ if (!srs) return {};
44733
+ return auxCrsIsWkt(srs) ? {wkt1: srs} : {crs_string: srs};
44734
+ }
44735
+
43089
44736
  function importDbf(input, opts) {
43090
44737
  var table;
43091
44738
  opts = utils.extend({}, opts);
@@ -43556,6 +45203,8 @@ ${svg}
43556
45203
  readShapefileAuxFiles(path, input, cache);
43557
45204
  } else if (isRasterImageInputType(fileType)) {
43558
45205
  readRasterImageAuxFiles(path, input, cache);
45206
+ } else if (fileType == 'geotiff') {
45207
+ readGeoTIFFAuxFiles(path, input, cache);
43559
45208
  }
43560
45209
  if (fileType == 'shp' && !input.dbf) {
43561
45210
  message(utils.format("[%s] .dbf file is missing - shapes imported without attribute data.", path));
@@ -43680,6 +45329,13 @@ ${svg}
43680
45329
  }
43681
45330
  }
43682
45331
 
45332
+ function readGeoTIFFAuxFiles(path, obj, cache) {
45333
+ var auxPath = getAuxFilename(path);
45334
+ if (cli.isFile(auxPath, cache)) {
45335
+ obj.aux = {filename: auxPath, content: cli.readFile(auxPath, 'utf-8', cache)};
45336
+ }
45337
+ }
45338
+
43683
45339
  function readRasterImageAuxFiles(path, obj, cache) {
43684
45340
  var prjPath = replaceFileExtension(path, 'prj');
43685
45341
  var worldPath = findWorldFile(path, cache);
@@ -43717,11 +45373,15 @@ ${svg}
43717
45373
 
43718
45374
  function removeRasterImageSidecars(files) {
43719
45375
  var imageBases = {};
45376
+ var rasterPaths = {};
43720
45377
  files.forEach(function(file) {
43721
45378
  var type = guessInputFileType(file);
43722
45379
  if (isRasterImageInputType(type)) {
43723
45380
  imageBases[getFileBase(file).toLowerCase()] = true;
43724
45381
  }
45382
+ if (isRasterImageInputType(type) || type == 'geotiff') {
45383
+ rasterPaths[file.toLowerCase()] = true;
45384
+ }
43725
45385
  });
43726
45386
  return files.filter(function(file) {
43727
45387
  var ext = getFileExtension(file).toLowerCase();
@@ -43730,6 +45390,11 @@ ${svg}
43730
45390
  if ((type == 'prj' || isWorldFileExtension(ext)) && imageBases[base]) {
43731
45391
  return false;
43732
45392
  }
45393
+ // A sidecar is read along with the raster it names, so importing it
45394
+ // separately would only add an empty layer.
45395
+ if (type == 'aux' && rasterPaths[getAuxSourceFilename(file).toLowerCase()]) {
45396
+ return false;
45397
+ }
43733
45398
  return true;
43734
45399
  });
43735
45400
  }
@@ -44269,7 +45934,7 @@ ${svg}
44269
45934
  var targetPoints = [];
44270
45935
  var targetPointLayers = [];
44271
45936
  var targetFlags, otherFlags, transform;
44272
- dataset.layers.filter(layerHasGeometry).forEach(function(lyr) {
45937
+ dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
44273
45938
  var hits = [],
44274
45939
  misses = [],
44275
45940
  test;
@@ -47203,7 +48868,7 @@ ${svg}
47203
48868
 
47204
48869
  function stopIfBufferReachesPole(shape, dist) {
47205
48870
  var maxAbsLat = 0;
47206
- var angularDist = dist / R$3 * R2D$7;
48871
+ var angularDist = dist / R$3 * R2D$8;
47207
48872
  (shape || []).forEach(function(path) {
47208
48873
  latLngPathIter.init(path);
47209
48874
  while (latLngPathIter.hasNext()) {
@@ -53996,7 +55661,7 @@ ${svg}
53996
55661
  var maxAbsLat = Math.max(Math.abs(bounds.ymin), Math.abs(bounds.ymax));
53997
55662
  var maxPositiveDistance = getMaxPositiveBufferDistance(lyr, dataset, opts);
53998
55663
  if (!(maxPositiveDistance > 0)) return false;
53999
- return maxAbsLat + maxPositiveDistance / R$3 * R2D$7 >= 90 - POLAR_BUFFER_MARGIN_DEGREES;
55664
+ return maxAbsLat + maxPositiveDistance / R$3 * R2D$8 >= 90 - POLAR_BUFFER_MARGIN_DEGREES;
54000
55665
  }
54001
55666
 
54002
55667
  function getMaxPositiveBufferDistance(lyr, dataset, opts) {
@@ -55248,7 +56913,7 @@ ${svg}
55248
56913
  }
55249
56914
 
55250
56915
  function getCoordinateDistance(distance, arcs) {
55251
- return arcs.isPlanar() ? distance : distance / R$3 * R2D$7;
56916
+ return arcs.isPlanar() ? distance : distance / R$3 * R2D$8;
55252
56917
  }
55253
56918
 
55254
56919
  // @shapeIndex: chunk-bounds index of the source shape (see buildShapeSegmentIndex)
@@ -55709,7 +57374,7 @@ ${svg}
55709
57374
  geometries = bboxExpressionToGeometries(opts.bbox, targetLyr, targetDataset);
55710
57375
 
55711
57376
  } else {
55712
- if (!layerHasGeometry(targetLyr)) {
57377
+ if (!layerHasGeometry$1(targetLyr)) {
55713
57378
  stop$1("Layer is missing geometric shapes");
55714
57379
  }
55715
57380
  geometries = shapesToBoxGeometries(targetLyr, targetDataset, opts);
@@ -55977,8 +57642,8 @@ ${svg}
55977
57642
  p[1] *= D2R$8;
55978
57643
  var rotate = inv ? rotatePointInv : rotatePoint;
55979
57644
  rotate(p, a, b, c);
55980
- p[0] *= R2D$7;
55981
- p[1] *= R2D$7;
57645
+ p[0] *= R2D$8;
57646
+ p[1] *= R2D$8;
55982
57647
  return p;
55983
57648
  };
55984
57649
  }
@@ -56885,7 +58550,7 @@ ${svg}
56885
58550
  // polygon layer, @clipData). This avoids performing unnecessary intersection
56886
58551
  // tests on each line segment.
56887
58552
  var layers = dataset.layers.filter(function(lyr) {
56888
- return layerHasGeometry(lyr) && !layerIsFullyEnclosed(lyr, dataset, clipData);
58553
+ return layerHasGeometry$1(lyr) && !layerIsFullyEnclosed(lyr, dataset, clipData);
56889
58554
  });
56890
58555
  if (layers.length > 0) {
56891
58556
  clipLayersInPlace(layers, clipData, dataset, 'clip', getInternalClipOpts());
@@ -58726,7 +60391,7 @@ ${svg}
58726
60391
 
58727
60392
  cmd.proj = function(dataset, catalog, opts, targetLayers) {
58728
60393
  var srcInfo, destInfo, destStr;
58729
- var implicitlyProjectedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasGeometry);
60394
+ var implicitlyProjectedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasGeometry$1);
58730
60395
  // A preserved GeoJSON "crs" member becomes invalid once the CRS changes.
58731
60396
  deleteGeoJSONMetadataCRS(dataset);
58732
60397
  if (opts.init) {
@@ -62377,13 +64042,34 @@ ${svg}
62377
64042
  var MAX_CONTOUR_LEVELS = 2000;
62378
64043
  var TARGET_LEVEL_COUNT = 15;
62379
64044
 
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;
64045
+ // Marching squares traces a staircase with treads about one pixel wide, and the
64046
+ // smoother halves the amplitude of detail at roughly five times the distance it
64047
+ // is given, so a quarter of a pixel is already aimed squarely at the stepping.
64048
+ //
64049
+ // The distance also sets how far the line may move, and that is what limits it
64050
+ // from above: neighboring contours are as far apart as the contour interval
64051
+ // divided by the local gradient, which on a coarse grid over steep ground is a
64052
+ // small fraction of a pixel. (In test/data/features/contours/sample2_detail.tif
64053
+ // -- 11x11 pixels -- lines run as close as 0.1 px.) A distance of one pixel
64054
+ // therefore let lines walk over their neighbors: 40 crossings on that file and
64055
+ // 346 on a 1200x1053 DEM, against none at a quarter pixel and one pixel of
64056
+ // visible detail lost for nothing.
64057
+ var SMOOTHING_PIXELS = 0.25;
64058
+
64059
+ // Smallest gap, as a fraction of a cell, that a traced vertex is kept from the
64060
+ // grid point at either end of the edge it crosses.
64061
+ //
64062
+ // A sample whose value is exactly the contour level -- common, since elevations
64063
+ // are usually whole units and levels are round numbers -- puts the crossing
64064
+ // exactly on that grid point. Every cell around the point does the same, so two
64065
+ // branches of the contour meet there instead of passing by. Touching is
64066
+ // harmless in the traced line, but smoothing moves the branches and turns each
64067
+ // touch into a crossing, and no smoothing distance is small enough to avoid it.
64068
+ // Holding vertices a tenth of a cell clear leaves the branches far enough apart
64069
+ // to survive smoothing: on a 1200x1053 DEM this took the crossings that
64070
+ // survived from 67 to 2. Only applied when the lines are going to be smoothed;
64071
+ // with no-smoothing the crossings are placed exactly.
64072
+ var CORNER_CLEARANCE = 0.1;
62387
64073
 
62388
64074
  // A degree of longitude vanishes at the poles; keep the implied pixel width
62389
64075
  // from collapsing to zero there.
@@ -62442,7 +64128,7 @@ ${svg}
62442
64128
  levels = getContourLevels(grid, band, opts);
62443
64129
  return {
62444
64130
  levels: levels,
62445
- lines: traceRasterContours(grid, band, levels)
64131
+ lines: traceRasterContours(grid, band, levels, getCornerClearance(opts))
62446
64132
  };
62447
64133
  }
62448
64134
 
@@ -62576,14 +64262,21 @@ ${svg}
62576
64262
  return {min: min, max: max};
62577
64263
  }
62578
64264
 
62579
- function traceRasterContours(grid, band, levels) {
64265
+ function getCornerClearance(opts) {
64266
+ return opts && opts.no_smoothing ? 0 : CORNER_CLEARANCE;
64267
+ }
64268
+
64269
+ // clearance: see CORNER_CLEARANCE; 0 places crossings exactly.
64270
+ function traceRasterContours(grid, band, levels, clearance) {
62580
64271
  var segments = collectContourSegments(grid, band, levels);
62581
64272
  var toMapXY = getLatticeToMapTransform(grid);
62582
64273
  var hCount = (grid.width - 1) * grid.height;
62583
64274
  var lines = [];
64275
+ clearance = clearance || 0;
62584
64276
  segments.forEach(function(levelSegments, i) {
62585
64277
  stitchContourSegments(levelSegments).forEach(function(path) {
62586
- var coords = getPathCoords(path, grid, band, levels[i], hCount, toMapXY);
64278
+ var coords = getPathCoords(path, grid, band, levels[i], hCount, toMapXY,
64279
+ clearance);
62587
64280
  if (coords.length > 1) {
62588
64281
  lines.push({value: levels[i], coords: coords});
62589
64282
  }
@@ -62724,12 +64417,12 @@ ${svg}
62724
64417
  return path;
62725
64418
  }
62726
64419
 
62727
- function getPathCoords(path, grid, band, level, hCount, toMapXY) {
64420
+ function getPathCoords(path, grid, band, level, hCount, toMapXY, clearance) {
62728
64421
  var coords = [];
62729
64422
  var prev = null;
62730
64423
  var point, xy;
62731
64424
  for (var i = 0; i < path.length; i++) {
62732
- point = getEdgeCrossing(grid, band, level, path[i], hCount);
64425
+ point = getEdgeCrossing(grid, band, level, path[i], hCount, clearance);
62733
64426
  xy = toMapXY(point[0], point[1]);
62734
64427
  // A corner value exactly equal to the level puts two crossings in the
62735
64428
  // same place; keeping both would emit zero-length segments.
@@ -62743,7 +64436,7 @@ ${svg}
62743
64436
  // Returns the crossing point in lattice coordinates. The result depends only
62744
64437
  // on the edge and the level, so both cells sharing an edge compute the same
62745
64438
  // point, down to the last bit.
62746
- function getEdgeCrossing(grid, band, level, edgeId, hCount) {
64439
+ function getEdgeCrossing(grid, band, level, edgeId, hCount, clearance) {
62747
64440
  var W = grid.width;
62748
64441
  var bands = grid.bands;
62749
64442
  var samples = grid.samples;
@@ -62753,21 +64446,23 @@ ${svg}
62753
64446
  x = edgeId - y * (W - 1);
62754
64447
  v0 = samples[(y * W + x) * bands + band];
62755
64448
  v1 = samples[(y * W + x + 1) * bands + band];
62756
- return [x + interpolateCrossing(level, v0, v1), y];
64449
+ return [x + interpolateCrossing(level, v0, v1, clearance), y];
62757
64450
  }
62758
64451
  edgeId -= hCount;
62759
64452
  y = Math.floor(edgeId / W);
62760
64453
  x = edgeId - y * W;
62761
64454
  v0 = samples[(y * W + x) * bands + band];
62762
64455
  v1 = samples[((y + 1) * W + x) * bands + band];
62763
- return [x, y + interpolateCrossing(level, v0, v1)];
64456
+ return [x, y + interpolateCrossing(level, v0, v1, clearance)];
62764
64457
  }
62765
64458
 
62766
- function interpolateCrossing(level, v0, v1) {
64459
+ // clearance: fraction of the edge kept free at either end. See CORNER_CLEARANCE.
64460
+ function interpolateCrossing(level, v0, v1, clearance) {
64461
+ var hi = 1 - clearance;
62767
64462
  var t;
62768
64463
  if (v1 === v0) return 0.5;
62769
64464
  t = (level - v0) / (v1 - v0);
62770
- return t < 0 ? 0 : t > 1 ? 1 : t;
64465
+ return t < clearance ? clearance : t > hi ? hi : t;
62771
64466
  }
62772
64467
 
62773
64468
  // Lattice coordinates are indexed from the center of the first pixel, not the
@@ -62792,6 +64487,7 @@ ${svg}
62792
64487
  getContourBand: getContourBand,
62793
64488
  getContourLevels: getContourLevels,
62794
64489
  getContourSmoothingDistance: getContourSmoothingDistance,
64490
+ getCornerClearance: getCornerClearance,
62795
64491
  getRasterContourLines: getRasterContourLines,
62796
64492
  getRasterSampleRange: getRasterSampleRange,
62797
64493
  traceRasterContours: traceRasterContours,
@@ -62829,12 +64525,14 @@ ${svg}
62829
64525
  function smoothContourDataset(dataset, grid) {
62830
64526
  var crs = getDatasetCRS(dataset);
62831
64527
  var distance = getContourSmoothingDistance(grid, crs);
64528
+ var unsmoothed, repair;
62832
64529
  if (!(distance > 0)) {
62833
64530
  message('Skipped contour smoothing: unable to determine the pixel size');
62834
64531
  return;
62835
64532
  }
62836
64533
  message('Smoothing contours with an auto-selected interval of ' +
62837
- formatSmoothingDistance(distance, crs) + ' (one pixel)');
64534
+ formatSmoothingDistance(distance, crs) + ' (a quarter of a pixel)');
64535
+ unsmoothed = dataset.arcs.getCopy();
62838
64536
  cmd.smooth(dataset, {
62839
64537
  distance: distance,
62840
64538
  // The contour staircase is an artifact, so there are no real corners to
@@ -62842,6 +64540,19 @@ ${svg}
62842
64540
  no_corners: true,
62843
64541
  no_prefilter: true
62844
64542
  }, dataset.layers);
64543
+ // Contour lines never cross, so any crossing is smoothing pulling a line over
64544
+ // its neighbor. The interval is small enough that this is rare, and where it
64545
+ // happens the lines involved are better left as they were traced.
64546
+ repair = repairCrossedArcs(dataset.arcs, unsmoothed);
64547
+ if (repair.reverted > 0) {
64548
+ message('Left ' + repair.reverted + ' contour ' +
64549
+ (repair.reverted == 1 ? 'line' : 'lines') +
64550
+ ' unsmoothed, to keep lines from crossing');
64551
+ }
64552
+ if (repair.remaining > 0) {
64553
+ message('Unable to remove ' + repair.remaining + ' crossing' +
64554
+ (repair.remaining == 1 ? '' : 's') + ' between contour lines');
64555
+ }
62845
64556
  }
62846
64557
 
62847
64558
  function formatSmoothingDistance(distance, crs) {
@@ -66068,7 +67779,7 @@ ${svg}
66068
67779
  }
66069
67780
 
66070
67781
  cmd.filterGeom = function(lyr, arcs, opts) {
66071
- if (!layerHasGeometry(lyr)) {
67782
+ if (!layerHasGeometry$1(lyr)) {
66072
67783
  stop$1("Layer is missing geometry");
66073
67784
  }
66074
67785
  if (opts.bbox) {
@@ -74186,7 +75897,7 @@ ${svg}
74186
75897
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
74187
75898
  }
74188
75899
 
74189
- var version = "0.7.49";
75900
+ var version = "0.7.51";
74190
75901
 
74191
75902
  // Parse command line args into commands and run them
74192
75903
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -76076,6 +77787,9 @@ ${svg}
76076
77787
  JoinTables,
76077
77788
  JsonImport,
76078
77789
  JsonTable,
77790
+ GeoTIFFAux,
77791
+ GeoTIFFEncode,
77792
+ GeoTIFFGeoKeys,
76079
77793
  KeepShapes,
76080
77794
  LatLon,
76081
77795
  LayerUtils,
@@ -76127,6 +77841,7 @@ ${svg}
76127
77841
  Scalebar,
76128
77842
  SegmentGeom,
76129
77843
  SegmentIntersection,
77844
+ SegmentIntersectionRepair,
76130
77845
  ShapeIter$1,
76131
77846
  ShapeUtils,
76132
77847
  ShpCommon,