mapshaper 0.7.50 → 0.7.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -10465,6 +10465,22 @@
10465
10465
  return crs;
10466
10466
  }
10467
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
+
10468
10484
  function parseCrsString$1(str) {
10469
10485
  var defn = getProjDefn(str); // defn is a string or a Proj object
10470
10486
  var P;
@@ -10720,6 +10736,7 @@
10720
10736
  setProjectionLoader: setProjectionLoader,
10721
10737
  toLngLat: toLngLat,
10722
10738
  tryParseCrsString: tryParseCrsString,
10739
+ tryParseWktToProj: tryParseWktToProj,
10723
10740
  wkt1ToProj: wkt1ToProj,
10724
10741
  wkt2ToProj: wkt2ToProj,
10725
10742
  wktToProj: wktToProj
@@ -13129,6 +13146,67 @@
13129
13146
  toLowerCaseExtension: toLowerCaseExtension
13130
13147
  });
13131
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
+
13132
13210
  // Constants shared between the .msx (mapshaper snapshot) writer in
13133
13211
  // mapshaper-pack.mjs and the file-type registry in
13134
13212
  // io/mapshaper-file-types.mjs. Living here lets file-types reference the
@@ -13142,7 +13220,10 @@
13142
13220
  function guessInputFileType(file) {
13143
13221
  var ext = getFileExtension(file || '').toLowerCase(),
13144
13222
  type = null;
13145
- 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') {
13146
13227
  type = ext;
13147
13228
  } else if (ext == 'jpg' || ext == 'jpeg') {
13148
13229
  type = 'jpeg';
@@ -13166,7 +13247,8 @@
13166
13247
 
13167
13248
  // File types that can be imported but are not convertible to datasets
13168
13249
  function isAuxiliaryInputFileType(type) {
13169
- return type == 'prj' || type == 'shx' || type == 'cpg' || type == 'world';
13250
+ return type == 'prj' || type == 'shx' || type == 'cpg' || type == 'world' ||
13251
+ type == 'aux';
13170
13252
  }
13171
13253
 
13172
13254
  function isRasterImageInputType(type) {
@@ -34036,11 +34118,6 @@ ${svg}
34036
34118
  // The raster covers areas, i.e. a coordinate refers to a pixel's corner rather
34037
34119
  // than its center. This matches how mapshaper's grid bbox is defined.
34038
34120
  var RASTER_TYPE_AREA = 1;
34039
- // GDAL's "persistent auxiliary metadata" sidecar, which it reads for any raster
34040
- // it opens. Unlike the .prj file that accompanies a shapefile, this is a
34041
- // sidecar that GDAL-based software (QGIS included) actually consults for a
34042
- // GeoTIFF's CRS.
34043
- var AUX_EXT = '.aux.xml';
34044
34121
 
34045
34122
  function exportGeoTIFF(dataset, opts) {
34046
34123
  var extension = opts.extension ||
@@ -34058,8 +34135,8 @@ ${svg}
34058
34135
  if (crs.code || crs.geoKeys) return;
34059
34136
  if (crs.wkt) {
34060
34137
  files.push({
34061
- filename: filename + AUX_EXT,
34062
- content: getAuxXml(crs.wkt)
34138
+ filename: getAuxFilename(filename),
34139
+ content: formatAuxXml(crs.wkt)
34063
34140
  });
34064
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.');
34065
34142
  } else {
@@ -34169,14 +34246,6 @@ ${svg}
34169
34246
  }
34170
34247
  }
34171
34248
 
34172
- function getAuxXml(wkt) {
34173
- return '<PAMDataset>\n <SRS>' + escapeXml(wkt) + '</SRS>\n</PAMDataset>\n';
34174
- }
34175
-
34176
- function escapeXml(str) {
34177
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
34178
- }
34179
-
34180
34249
  function getOutputFormat(dataset, opts) {
34181
34250
  var outFile = opts.file || null,
34182
34251
  inFmt = dataset.info && dataset.info.input_formats && dataset.info.input_formats[0],
@@ -42820,7 +42889,8 @@ ${svg}
42820
42889
  var dynamicImportModule = Function('id', 'return import(id)');
42821
42890
  var DEFAULT_MAX_IMPORT_PIXELS = 16e6;
42822
42891
 
42823
- 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) {
42824
42894
  var opts = optsArg || {};
42825
42895
  var geotiff = await loadGeoTIFFLib();
42826
42896
  var source = getGeoTIFFSource(input);
@@ -42838,7 +42908,7 @@ ${svg}
42838
42908
  raster: imported.raster
42839
42909
  }]
42840
42910
  };
42841
- await importGeoTIFFCrs(dataset, sourceImage);
42911
+ await importGeoTIFFCrs(dataset, sourceImage, aux);
42842
42912
  return dataset;
42843
42913
  }
42844
42914
 
@@ -43187,32 +43257,117 @@ ${svg}
43187
43257
  // in its own georeferencing, and only the things that need to know what those
43188
43258
  // coordinates mean (reprojecting, basemaps, measuring) are unavailable. So a
43189
43259
  // CRS that cannot be read is reported and set aside, never raised as an error.
43190
- async function importGeoTIFFCrs(dataset, image) {
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) {
43191
43266
  var crsInfo = await getGeoTIFFCrsInfo(image);
43192
- var crsString = crsInfo.crsString;
43193
- var crs = null;
43194
- if (crsString) {
43195
- try {
43196
- await initProjLibrary({crs: crsString});
43197
- } catch(e) {
43198
- // A projection resource that would not load leaves the string to be
43199
- // parsed with whatever is already known.
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);
43278
+ return;
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;
43200
43292
  }
43201
- crs = tryParseCrsString(crsString);
43202
43293
  }
43203
- if (!crs) {
43204
- dataset.info = dataset.info || {};
43205
- warnOnce(getGeoTIFFCrsWarning(crsInfo, crsString ?
43206
- 'Unable to use projection ' + crsString : null));
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;
43300
+ try {
43301
+ await initProjLibrary({crs: str});
43302
+ } catch(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;
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
+ }
43207
43346
  return;
43208
43347
  }
43209
- setDatasetCrsInfo(dataset, {crs_string: crsString, crs: crs});
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 ');
43210
43356
  }
43211
43357
 
43212
43358
  async function getGeoTIFFCrsInfo(image) {
43213
43359
  var keys = image.getGeoKeys && image.getGeoKeys() || {};
43214
43360
  var code = keys.ProjectedCSTypeGeoKey;
43215
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
+ }
43216
43371
  if (isGeoTIFFAuthorityCode(code)) {
43217
43372
  return {crsString: 'EPSG:' + code};
43218
43373
  }
@@ -43230,6 +43385,23 @@ ${svg}
43230
43385
  return 'The GeoTIFF does not contain usable CRS data';
43231
43386
  }
43232
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
+
43233
43405
  function isGeoTIFFAuthorityCode(code) {
43234
43406
  return code > 0 && code != 32767;
43235
43407
  }
@@ -44438,6 +44610,14 @@ ${svg}
44438
44610
  data = obj.prj;
44439
44611
  dataset = {layers: [], info: {wkt1: data.content}};
44440
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
+
44441
44621
  } else if (obj.kml) {
44442
44622
  dataFmt = 'kml';
44443
44623
  data = obj.kml;
@@ -44483,7 +44663,7 @@ ${svg}
44483
44663
  } else if (obj.geotiff) {
44484
44664
  dataFmt = 'geotiff';
44485
44665
  data = obj.geotiff;
44486
- dataset = await importGeoTIFF(data, opts);
44666
+ dataset = await importGeoTIFF(data, opts, obj.aux);
44487
44667
  } else if (obj.png || obj.jpeg) {
44488
44668
  dataFmt = obj.png ? 'png' : 'jpeg';
44489
44669
  data = obj[dataFmt];
@@ -44545,6 +44725,14 @@ ${svg}
44545
44725
  return dataset;
44546
44726
  }
44547
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
+
44548
44736
  function importDbf(input, opts) {
44549
44737
  var table;
44550
44738
  opts = utils.extend({}, opts);
@@ -45015,6 +45203,8 @@ ${svg}
45015
45203
  readShapefileAuxFiles(path, input, cache);
45016
45204
  } else if (isRasterImageInputType(fileType)) {
45017
45205
  readRasterImageAuxFiles(path, input, cache);
45206
+ } else if (fileType == 'geotiff') {
45207
+ readGeoTIFFAuxFiles(path, input, cache);
45018
45208
  }
45019
45209
  if (fileType == 'shp' && !input.dbf) {
45020
45210
  message(utils.format("[%s] .dbf file is missing - shapes imported without attribute data.", path));
@@ -45139,6 +45329,13 @@ ${svg}
45139
45329
  }
45140
45330
  }
45141
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
+
45142
45339
  function readRasterImageAuxFiles(path, obj, cache) {
45143
45340
  var prjPath = replaceFileExtension(path, 'prj');
45144
45341
  var worldPath = findWorldFile(path, cache);
@@ -45176,11 +45373,15 @@ ${svg}
45176
45373
 
45177
45374
  function removeRasterImageSidecars(files) {
45178
45375
  var imageBases = {};
45376
+ var rasterPaths = {};
45179
45377
  files.forEach(function(file) {
45180
45378
  var type = guessInputFileType(file);
45181
45379
  if (isRasterImageInputType(type)) {
45182
45380
  imageBases[getFileBase(file).toLowerCase()] = true;
45183
45381
  }
45382
+ if (isRasterImageInputType(type) || type == 'geotiff') {
45383
+ rasterPaths[file.toLowerCase()] = true;
45384
+ }
45184
45385
  });
45185
45386
  return files.filter(function(file) {
45186
45387
  var ext = getFileExtension(file).toLowerCase();
@@ -45189,6 +45390,11 @@ ${svg}
45189
45390
  if ((type == 'prj' || isWorldFileExtension(ext)) && imageBases[base]) {
45190
45391
  return false;
45191
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
+ }
45192
45398
  return true;
45193
45399
  });
45194
45400
  }
@@ -75691,7 +75897,7 @@ ${svg}
75691
75897
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
75692
75898
  }
75693
75899
 
75694
- var version = "0.7.50";
75900
+ var version = "0.7.52";
75695
75901
 
75696
75902
  // Parse command line args into commands and run them
75697
75903
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -76353,6 +76559,256 @@ ${svg}
76353
76559
  return false;
76354
76560
  }
76355
76561
 
76562
+ // Utilities for dividing a path at one or two points ("snipping").
76563
+ //
76564
+ // The new arcs are appended to the end of the collection; the original arc is
76565
+ // left in place (it may still be referenced by other shapes). Callers are
76566
+ // responsible for updating the shapes that should use the new arcs.
76567
+
76568
+ // Divide an arc's vertices into slices at one or two cut locations.
76569
+ // n: number of vertices in the arc
76570
+ // cuts: one or two cut objects, ordered from the start of the arc:
76571
+ // {offset: i, point: [x, y]} cuts the segment between vertices i and i + 1
76572
+ // {offset: i, point: null} cuts at vertex i (which is duplicated, so that
76573
+ // both adjacent slices contain it)
76574
+ // Returns slice descriptors {start, end, prefix, suffix}, where start and end
76575
+ // are an inclusive vertex range (empty if start > end) and prefix/suffix are
76576
+ // points inserted at a cut location.
76577
+ function getArcSlices(n, cuts) {
76578
+ var slices = [];
76579
+ var start = 0;
76580
+ var prefix = null;
76581
+ cuts.forEach(function(cut) {
76582
+ var i = cut.offset;
76583
+ if (cut.point) {
76584
+ if (i < 0 || i > n - 2) error('Out-of-range arc cut offset:', i);
76585
+ slices.push({start: start, end: i, prefix: prefix, suffix: cut.point});
76586
+ prefix = cut.point;
76587
+ start = i + 1;
76588
+ } else {
76589
+ if (i < 1 || i > n - 2) error('Out-of-range arc cut offset:', i);
76590
+ slices.push({start: start, end: i, prefix: prefix, suffix: null});
76591
+ prefix = null;
76592
+ start = i;
76593
+ }
76594
+ });
76595
+ slices.push({start: start, end: n - 1, prefix: prefix, suffix: null});
76596
+ return slices;
76597
+ }
76598
+
76599
+ function getArcSliceLength(slice) {
76600
+ var n = slice.end >= slice.start ? slice.end - slice.start + 1 : 0;
76601
+ if (slice.prefix) n++;
76602
+ if (slice.suffix) n++;
76603
+ return n;
76604
+ }
76605
+
76606
+ // Append the slices of a divided arc to an ArcCollection.
76607
+ // Returns the ids of the new arcs, ordered from the start of the original arc.
76608
+ function splitArcAtCuts(arcs, arcId, cuts) {
76609
+ var data = arcs.getVertexData();
76610
+ var zlimit = arcs.getRetainedInterval();
76611
+ var arcStart = data.ii[arcId];
76612
+ var arcCount = data.nn.length;
76613
+ var slices = getArcSlices(data.nn[arcId], cuts);
76614
+ var pointCount = data.xx.length;
76615
+ var added = slices.reduce(function(memo, slice) {
76616
+ return memo + getArcSliceLength(slice);
76617
+ }, 0);
76618
+ var xx2 = new Float64Array(pointCount + added);
76619
+ var yy2 = new Float64Array(pointCount + added);
76620
+ var zz2 = arcs.isFlat() ? null : new Float64Array(pointCount + added);
76621
+ var nn2 = new Uint32Array(arcCount + slices.length);
76622
+ var i = pointCount;
76623
+ var ids = [];
76624
+
76625
+ xx2.set(data.xx);
76626
+ yy2.set(data.yy);
76627
+ nn2.set(data.nn);
76628
+ if (zz2) zz2.set(data.zz);
76629
+
76630
+ slices.forEach(function(slice, sliceId) {
76631
+ var first = i;
76632
+ var j;
76633
+ if (slice.prefix) {
76634
+ xx2[i] = slice.prefix[0];
76635
+ yy2[i] = slice.prefix[1];
76636
+ i++;
76637
+ }
76638
+ for (j = slice.start; j <= slice.end; j++) {
76639
+ xx2[i] = data.xx[arcStart + j];
76640
+ yy2[i] = data.yy[arcStart + j];
76641
+ if (zz2) zz2[i] = data.zz[arcStart + j];
76642
+ i++;
76643
+ }
76644
+ if (slice.suffix) {
76645
+ xx2[i] = slice.suffix[0];
76646
+ yy2[i] = slice.suffix[1];
76647
+ i++;
76648
+ }
76649
+ if (zz2) {
76650
+ // arc endpoints must never be removed by simplification
76651
+ zz2[first] = Infinity;
76652
+ zz2[i - 1] = Infinity;
76653
+ }
76654
+ nn2[arcCount + sliceId] = i - first;
76655
+ ids.push(arcCount + sliceId);
76656
+ });
76657
+
76658
+ arcs.updateVertexData(nn2, xx2, yy2, zz2);
76659
+ arcs.setRetainedInterval(zlimit);
76660
+ return ids;
76661
+ }
76662
+
76663
+ // A cut identifies one location along a path (an array of arc ids):
76664
+ // seq: index into the path of the arc containing the cut
76665
+ // offset: vertex index within that arc, in traversal orientation
76666
+ // point: coords of the cut, or null to cut at vertex <offset>
76667
+ // displayPoint: coords of the cut in the GUI's display CRS, or null
76668
+ // t: position along the segment (0-1), used to order two cuts
76669
+ // that land on the same segment
76670
+
76671
+ // Work out how to divide a path at one cut (open path) or two cuts (ring),
76672
+ // without mutating anything.
76673
+ //
76674
+ // path: array of arc ids
76675
+ // cutsArg: one cut (open path) or two cuts (ring), in any order
76676
+ // arcLen: function(arcId) returning the number of vertices in an arc
76677
+ // baseId: arcs.size(), used to predict the ids of arcs that will be appended
76678
+ // closed: whether the path is a ring
76679
+ //
76680
+ // Returns {splits, a, b}, where splits are arc divisions to perform in order
76681
+ // and a and b are the two resulting paths, or null if the cuts are degenerate
76682
+ // (at the endpoint of an open path, or two cuts in the same place on a ring).
76683
+ //
76684
+ function planPathDivision(path, cutsArg, arcLen, baseId, closed) {
76685
+ var cuts = sortCuts(cutsArg);
76686
+ var boundaryCuts = []; // cuts that fall between two arcs
76687
+ var interiorCuts = {}; // cuts that fall inside an arc, by path index
76688
+ var expanded = []; // path with divided arcs replaced by their pieces
76689
+ var positions = []; // position of each cut within the expanded path
76690
+ var splits = [];
76691
+ var nextArcId = baseId;
76692
+ var a, b, q, q1, q2;
76693
+
76694
+ if (cuts.length != (closed ? 2 : 1)) return null;
76695
+
76696
+ cuts.forEach(function(cut, cutId) {
76697
+ var n = arcLen(absArcId(path[cut.seq]));
76698
+ var seq;
76699
+ if (!cut.point && (cut.offset === 0 || cut.offset === n - 1)) {
76700
+ // a cut at an arc endpoint doesn't divide the arc, it divides the
76701
+ // sequence of arcs
76702
+ seq = cut.offset === 0 ? cut.seq : cut.seq + 1;
76703
+ if (closed && seq === path.length) seq = 0; // a ring's seam is one place
76704
+ boundaryCuts.push({cutId: cutId, seq: seq});
76705
+ } else {
76706
+ if (!interiorCuts[cut.seq]) interiorCuts[cut.seq] = [];
76707
+ interiorCuts[cut.seq].push({cutId: cutId, cut: cut, arcLen: n});
76708
+ }
76709
+ });
76710
+
76711
+ for (var k=0; k<=path.length; k++) {
76712
+ boundaryCuts.forEach(function(item) {
76713
+ if (item.seq === k) positions[item.cutId] = expanded.length;
76714
+ });
76715
+ if (k === path.length) break;
76716
+ if (!interiorCuts[k]) {
76717
+ expanded.push(path[k]);
76718
+ continue;
76719
+ }
76720
+ addDividedArc(interiorCuts[k], path[k]);
76721
+ }
76722
+
76723
+ if (!closed) {
76724
+ q = positions[0];
76725
+ if (q === 0 || q === expanded.length) return null; // cut at a path endpoint
76726
+ a = expanded.slice(0, q);
76727
+ b = expanded.slice(q);
76728
+ } else {
76729
+ q1 = Math.min(positions[0], positions[1]);
76730
+ q2 = Math.max(positions[0], positions[1]);
76731
+ if (q1 === q2) return null; // both cuts in the same place
76732
+ a = expanded.slice(q1, q2);
76733
+ b = expanded.slice(q2).concat(expanded.slice(0, q1));
76734
+ }
76735
+ return {splits: splits, a: a, b: b};
76736
+
76737
+ function addDividedArc(group, arcId) {
76738
+ var fwd = arcId >= 0;
76739
+ var n = group[0].arcLen;
76740
+ // convert traversal offsets to forward-orientation offsets
76741
+ var fwdCuts = group.map(function(item) {
76742
+ return {
76743
+ offset: fwd ? item.cut.offset :
76744
+ n - (item.cut.point ? 2 : 1) - item.cut.offset,
76745
+ point: item.cut.point || null,
76746
+ displayPoint: item.cut.displayPoint || null
76747
+ };
76748
+ });
76749
+ // a reversed arc is traversed from its last vertex, so traversal order is
76750
+ // the reverse of forward order
76751
+ var pieces = getSplitArcIds(nextArcId, group.length, fwd);
76752
+ splits.push({
76753
+ arcId: absArcId(arcId),
76754
+ cuts: fwd ? fwdCuts : fwdCuts.concat().reverse()
76755
+ });
76756
+ nextArcId += group.length + 1;
76757
+ pieces.forEach(function(id, i) {
76758
+ if (i > 0) positions[group[i - 1].cutId] = expanded.length;
76759
+ expanded.push(id);
76760
+ });
76761
+ }
76762
+ }
76763
+
76764
+ // Ids of the pieces of a divided arc, in traversal order
76765
+ function getSplitArcIds(baseId, cutCount, fwd) {
76766
+ var ids = [];
76767
+ for (var i=0; i<=cutCount; i++) {
76768
+ ids.push(fwd ? baseId + i : ~(baseId + cutCount - i));
76769
+ }
76770
+ return ids;
76771
+ }
76772
+
76773
+ function sortCuts(cuts) {
76774
+ return cuts.concat().sort(function(a, b) {
76775
+ return a.seq - b.seq || getCutOffset(a) - getCutOffset(b);
76776
+ });
76777
+ }
76778
+
76779
+ // A sortable offset within one arc. A cut on the segment following a vertex
76780
+ // comes after the vertex itself; two cuts on the same segment are ordered by
76781
+ // their position along it.
76782
+ function getCutOffset(cut) {
76783
+ if (!cut.point) return cut.offset;
76784
+ return cut.offset + 0.5 + (cut.t > 0 ? Math.min(cut.t, 1) : 0) * 0.4;
76785
+ }
76786
+
76787
+ function getPlanarPathLength(ids, arcs) {
76788
+ var iter = arcs.getShapeIter(ids);
76789
+ var len = 0;
76790
+ var x, y;
76791
+ if (iter.hasNext()) {
76792
+ x = iter.x;
76793
+ y = iter.y;
76794
+ }
76795
+ while (iter.hasNext()) {
76796
+ len += distance2D$1(x, y, iter.x, iter.y);
76797
+ x = iter.x;
76798
+ y = iter.y;
76799
+ }
76800
+ return len;
76801
+ }
76802
+
76803
+ var ArcSplit = /*#__PURE__*/Object.freeze({
76804
+ __proto__: null,
76805
+ getArcSliceLength: getArcSliceLength,
76806
+ getArcSlices: getArcSlices,
76807
+ getPlanarPathLength: getPlanarPathLength,
76808
+ planPathDivision: planPathDivision,
76809
+ splitArcAtCuts: splitArcAtCuts
76810
+ });
76811
+
76356
76812
  var BOX_BLUR_PASSES = 3;
76357
76813
 
76358
76814
  function blurRasterGrid(raster, optsArg, crs) {
@@ -77529,6 +77985,7 @@ ${svg}
77529
77985
  AnchorPoints,
77530
77986
  ArcClassifier,
77531
77987
  ArcDissolve,
77988
+ ArcSplit,
77532
77989
  ArcUtils,
77533
77990
  Bbox2Clipping,
77534
77991
  BinArray$1,
@@ -77581,6 +78038,7 @@ ${svg}
77581
78038
  JoinTables,
77582
78039
  JsonImport,
77583
78040
  JsonTable,
78041
+ GeoTIFFAux,
77584
78042
  GeoTIFFEncode,
77585
78043
  GeoTIFFGeoKeys,
77586
78044
  KeepShapes,