mapshaper 0.7.50 → 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/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.51";
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.
@@ -77581,6 +77787,7 @@ ${svg}
77581
77787
  JoinTables,
77582
77788
  JsonImport,
77583
77789
  JsonTable,
77790
+ GeoTIFFAux,
77584
77791
  GeoTIFFEncode,
77585
77792
  GeoTIFFGeoKeys,
77586
77793
  KeepShapes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.50",
3
+ "version": "0.7.51",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -101,7 +101,7 @@
101
101
  ],
102
102
  "check-leaks": true,
103
103
  "parallel": true,
104
- "jobs": 4
104
+ "jobs": 6
105
105
  },
106
106
  "bin": {
107
107
  "mapshaper": "bin/mapshaper",
@@ -6876,31 +6876,37 @@
6876
6876
  // instead.
6877
6877
 
6878
6878
 
6879
+ // Undo is on unless it has been turned off. Undoing is what a user expects by
6880
+ // default; the History menu's checkbox exists so that sessions working with
6881
+ // data too large to store restore payloads for can opt out.
6882
+ var APP_UNDO_DEFAULT = true;
6883
+
6879
6884
  // Returns true if the manifest, URL query, gui-installed checker, or
6880
6885
  // localStorage indicates that app-level undo should be active.
6881
6886
  function appUndoIsEnabled(gui) {
6882
6887
  var opt = gui && gui.options && (gui.options.undoCommands || gui.options.appUndo);
6883
6888
  var query = getUndoQueryValue();
6889
+ if (query == 'off') return false;
6884
6890
  if (opt === true || query == 'on' || query == 'commands') return true;
6885
6891
  if (gui && gui.appUndoIsEnabled) return gui.appUndoIsEnabled();
6886
6892
  return appUndoSettingIsOn();
6887
6893
  }
6888
6894
 
6889
- // True when the undo URL flag forces app undo on regardless of UI settings.
6890
- // Mirrors the toggle-disable behavior in HistoryMenu.
6895
+ // True when the undo URL flag pins app undo on or off regardless of UI
6896
+ // settings. Mirrors the toggle-disable behavior in HistoryMenu.
6891
6897
  function appUndoForcedByUrl() {
6892
6898
  var query = getUndoQueryValue();
6893
- return query == 'on' || query == 'commands';
6899
+ return query == 'on' || query == 'commands' || query == 'off';
6894
6900
  }
6895
6901
 
6896
- // Read the persisted localStorage opt-in. Returns false in non-browser
6897
- // environments or when storage is blocked.
6902
+ // Read the persisted setting. Only an explicit opt-out turns undo off, so a
6903
+ // first-time session (and one where storage is unreadable) gets the default.
6898
6904
  function appUndoSettingIsOn() {
6899
6905
  try {
6900
- return !!(typeof window != 'undefined' && window.localStorage &&
6901
- window.localStorage.getItem('mapshaper.undo') == 'on');
6906
+ if (typeof window == 'undefined' || !window.localStorage) return APP_UNDO_DEFAULT;
6907
+ return window.localStorage.getItem('mapshaper.undo') != 'off';
6902
6908
  } catch(e) {
6903
- return false;
6909
+ return APP_UNDO_DEFAULT;
6904
6910
  }
6905
6911
  }
6906
6912
 
@@ -8067,6 +8073,15 @@
8067
8073
  });
8068
8074
  }
8069
8075
 
8076
+ // A .aux.xml sidecar is named after the whole raster file
8077
+ // (world.tif.aux.xml), so it joins that file's group rather than one of its
8078
+ // own. Returns null if the raster it names was not loaded with it.
8079
+ function auxSourceGroup(d) {
8080
+ var sourceName = internal.getAuxSourceFilename(d.name).toLowerCase();
8081
+ var source = data.find(f => f.name.toLowerCase() == sourceName);
8082
+ return source ? key(fileBase(source), fileType(source)) : null;
8083
+ }
8084
+
8070
8085
  data.forEach(d => {
8071
8086
  var basename = fileBase(d);
8072
8087
  var type = fileType(d);
@@ -8078,6 +8093,8 @@
8078
8093
  } else if (type == 'dbf') {
8079
8094
  d.filename = d.name;
8080
8095
  d.group = key(basename, 'dbf');
8096
+ } else if (type == 'aux' && auxSourceGroup(d)) {
8097
+ d.group = auxSourceGroup(d);
8081
8098
  } else if ((type == 'png' || type == 'jpeg') || isRasterImagePart(d.name) && hasRasterImage(basename)) {
8082
8099
  d.group = key(basename, type == 'png' || type == 'jpeg' ? type : getRasterImageGroupType(data, basename));
8083
8100
  if (type == 'png' || type == 'jpeg') d.filename = d.name;
@@ -10641,6 +10658,8 @@
10641
10658
  var unsupportedMsg = "Exporting is not supported in this browser";
10642
10659
  var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode));
10643
10660
  var layersArr = [];
10661
+ var menuFormats = null; // formats the format menu was built with
10662
+ var formatPickedByUser = false;
10644
10663
  var toggleBtn = null; // checkbox <input> for toggling layer selection
10645
10664
  var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
10646
10665
  var ofileName = gui.container.findChild('#ofile-name');
@@ -10709,6 +10728,7 @@
10709
10728
 
10710
10729
  function turnOn() {
10711
10730
  layersArr = initLayerMenu();
10731
+ formatPickedByUser = false;
10712
10732
  // initZipOption();
10713
10733
  initFormatMenu();
10714
10734
  updateExportCheckboxes();
@@ -10890,6 +10910,7 @@
10890
10910
  }
10891
10911
 
10892
10912
  function updateToggleBtn() {
10913
+ refreshFormatMenu(); // the layer selection decides which formats apply
10893
10914
  updateExportCheckboxes(); // checkbox visibility is affected by number of export layers
10894
10915
  if (!toggleBtn) return;
10895
10916
  var state = getSelectionState();
@@ -10912,13 +10933,17 @@
10912
10933
  }
10913
10934
 
10914
10935
  function getDefaultExportFormat() {
10936
+ var layers = getExportFormatLayers();
10915
10937
  var active = model.getActiveLayer();
10916
10938
  var dataset = active.dataset;
10917
10939
  var inputFmt = dataset.info && dataset.info.input_formats &&
10918
10940
  dataset.info.input_formats[0];
10919
- // SVG stays the default for rasters: it is the only format that also
10920
- // accepts the vector layers a session usually has alongside them.
10921
- if (activeLayerHasRaster()) return 'svg';
10941
+ // Rasters and nothing else can only go to GeoTIFF, the one format that
10942
+ // takes their pixels.
10943
+ if (layers.length > 0 && layers.every(hasRaster)) return 'geotiff';
10944
+ // SVG is the default for a selection that mixes rasters with vector layers,
10945
+ // being the only format that accepts both.
10946
+ if (layers.some(hasRaster)) return 'svg';
10922
10947
  return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
10923
10948
  }
10924
10949
 
@@ -10926,16 +10951,26 @@
10926
10951
  var formats = ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', 'svg', internal.PACKAGE_EXT];
10927
10952
  // GeoTIFF is the one format here that only accepts raster layers, so it is
10928
10953
  // offered only when there is a raster to export.
10929
- if (activeLayerHasRaster()) formats.push('geotiff');
10954
+ if (getExportFormatLayers().some(hasRaster)) formats.push('geotiff');
10930
10955
  return formats;
10931
10956
  }
10932
10957
 
10933
- function activeLayerHasRaster() {
10958
+ // The layers the format menu describes: the ones checked for export, falling
10959
+ // back to the active layer when the menu is not up yet or nothing is checked.
10960
+ function getExportFormatLayers() {
10961
+ var selected = layersArr.length ? getSelectedLayerEntries() : [];
10934
10962
  var active = model.getActiveLayer();
10935
- return !!active.layer && internal.layerHasRaster(active.layer);
10963
+ if (selected.length > 0) {
10964
+ return selected.map(function(o) { return o.layer; });
10965
+ }
10966
+ return active && active.layer ? [active.layer] : [];
10967
+ }
10968
+
10969
+ function hasRaster(lyr) {
10970
+ return internal.layerHasRaster(lyr);
10936
10971
  }
10937
10972
 
10938
- function initFormatMenu() {
10973
+ function initFormatMenu(preferredFmt) {
10939
10974
  var formats = getExportFormats();
10940
10975
  var items = formats.map(function(fmt) {
10941
10976
  return utils$1.format('<td><label><input type="radio" name="format" value="%s"' +
@@ -10947,13 +10982,39 @@
10947
10982
  }
10948
10983
  table += '</table>';
10949
10984
  menu.findChild('.export-formats').html(table);
10950
- menu.findChild('.export-formats input[value="' + getDefaultExportFormat() + '"]').node().checked = true;
10985
+ menuFormats = formats;
10986
+ setSelectedFormat(formats.includes(preferredFmt) ? preferredFmt :
10987
+ getDefaultExportFormat());
10951
10988
  // update save-as settings when value changes
10952
10989
  menu.findChildren('input[type="radio"]').forEach(el => {
10953
- el.on('change', updateExportCheckboxes);
10990
+ el.on('change', onFormatChange);
10954
10991
  });
10955
10992
  }
10956
10993
 
10994
+ function onFormatChange() {
10995
+ // A format the user picked is theirs to keep, however the layer selection
10996
+ // changes afterwards.
10997
+ formatPickedByUser = true;
10998
+ updateExportCheckboxes();
10999
+ }
11000
+
11001
+ function setSelectedFormat(fmt) {
11002
+ var el = menu.findChild('.export-formats input[value="' + fmt + '"]');
11003
+ if (el) el.node().checked = true;
11004
+ }
11005
+
11006
+ // Which formats apply depends on what is checked for export, so unchecking
11007
+ // the vector layers of a mixed session both offers GeoTIFF and makes it the
11008
+ // default -- unless the user has already chosen a format.
11009
+ function refreshFormatMenu() {
11010
+ var formats = getExportFormats();
11011
+ if (formats.join(',') != (menuFormats || []).join(',')) {
11012
+ initFormatMenu(formatPickedByUser ? getSelectedFormat() : null);
11013
+ } else if (!formatPickedByUser) {
11014
+ setSelectedFormat(getDefaultExportFormat());
11015
+ }
11016
+ }
11017
+
10957
11018
 
10958
11019
  // function getInputFormats() {
10959
11020
  // return model.getDatasets().reduce(function(memo, d) {
@@ -12095,6 +12156,7 @@
12095
12156
  }
12096
12157
 
12097
12158
  function isAppUndoEnabled() {
12159
+ if (getUndoQueryValue() == 'off') return false;
12098
12160
  return appUndoForcedByUrl() || appUndoSettingIsOn();
12099
12161
  }
12100
12162
 
@@ -25430,6 +25492,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
25430
25492
  return {
25431
25493
  layerCount: dataset.layers ? dataset.layers.length : 0,
25432
25494
  arcCount: dataset.arcs ? dataset.arcs.size() : 0,
25495
+ crs_string: dataset.info && dataset.info.crs_string || null,
25433
25496
  layers: (dataset.layers || []).map(function(lyr) {
25434
25497
  return {
25435
25498
  name: getLayerName(lyr),
package/www/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.51";
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.
@@ -77581,6 +77787,7 @@ ${svg}
77581
77787
  JoinTables,
77582
77788
  JsonImport,
77583
77789
  JsonTable,
77790
+ GeoTIFFAux,
77584
77791
  GeoTIFFEncode,
77585
77792
  GeoTIFFGeoKeys,
77586
77793
  KeepShapes,