mapshaper 0.7.49 → 0.7.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.49",
3
+ "version": "0.7.50",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -71,7 +71,7 @@
71
71
  "idb-keyval": "^6.2.0",
72
72
  "jpeg-js": "^0.4.4",
73
73
  "kdbush": "^3.0.0",
74
- "mproj": "0.1.5",
74
+ "mproj": "0.1.6",
75
75
  "msgpackr": "^1.11.12",
76
76
  "open": "^11.0.0",
77
77
  "pngjs": "^7.0.0",
@@ -7398,14 +7398,20 @@
7398
7398
  }
7399
7399
  }
7400
7400
 
7401
- function turnOff() {
7402
- var target;
7403
- if (catalog) catalog.reset(); // re-enable clickable catalog
7401
+ // Draw and select what was imported. Called when import mode ends, and by
7402
+ // importQueuedFiles() if something else has already ended it.
7403
+ function finishImport() {
7404
7404
  if (importCount > 0) {
7405
7405
  onImportComplete();
7406
7406
  importTotal += importCount;
7407
7407
  importCount = 0;
7408
7408
  }
7409
+ }
7410
+
7411
+ function turnOff() {
7412
+ var target;
7413
+ if (catalog) catalog.reset(); // re-enable clickable catalog
7414
+ finishImport();
7409
7415
  gui.clearProgressMessage();
7410
7416
  initialImport = false; // unset 'quick view' mode, if on
7411
7417
  clearQueuedFiles();
@@ -7432,8 +7438,13 @@
7432
7438
  gui.alert(e.message, 'Import error');
7433
7439
  }
7434
7440
  if (gui.getMode() == 'import') {
7435
- // Mode could also be 'alert' if an error is thrown and handled
7436
- gui.clearMode();
7441
+ gui.clearMode(); // leaving import mode finishes the import
7442
+ } else {
7443
+ // A popup shown while files were loading (an alert about one file's
7444
+ // metadata, say) takes the app out of import mode, so leaving it is no
7445
+ // longer what finishes the import. Without this, the files that did load
7446
+ // would sit in the catalog undrawn.
7447
+ finishImport();
7437
7448
  }
7438
7449
  if (undoTransaction && imported) {
7439
7450
  addUndoTransactionToHistory(gui, undoTransaction, {
@@ -10905,12 +10916,23 @@
10905
10916
  var dataset = active.dataset;
10906
10917
  var inputFmt = dataset.info && dataset.info.input_formats &&
10907
10918
  dataset.info.input_formats[0];
10908
- if (active.layer && internal.layerHasRaster(active.layer)) return 'svg';
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';
10909
10922
  return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
10910
10923
  }
10911
10924
 
10912
10925
  function getExportFormats() {
10913
- return ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', 'svg', internal.PACKAGE_EXT];
10926
+ var formats = ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', 'svg', internal.PACKAGE_EXT];
10927
+ // GeoTIFF is the one format here that only accepts raster layers, so it is
10928
+ // offered only when there is a raster to export.
10929
+ if (activeLayerHasRaster()) formats.push('geotiff');
10930
+ return formats;
10931
+ }
10932
+
10933
+ function activeLayerHasRaster() {
10934
+ var active = model.getActiveLayer();
10935
+ return !!active.layer && internal.layerHasRaster(active.layer);
10914
10936
  }
10915
10937
 
10916
10938
  function initFormatMenu() {
@@ -20962,6 +20984,31 @@
20962
20984
 
20963
20985
  utils$1.inherit(MapExtent, EventDispatcher);
20964
20986
 
20987
+ // Rules for drawing a raster that is being displayed larger than its data.
20988
+ // Kept free of browser and mapshaper dependencies so they can be tested
20989
+ // directly.
20990
+
20991
+ // Screen pixels per raster pixel at which a magnified raster is drawn as
20992
+ // squares instead of being smoothed. Below this there is little detail to lose
20993
+ // and smoothing looks better; above it, the smoothing is what a user sees
20994
+ // instead of the data.
20995
+ var MIN_CRISP_MAGNIFICATION = 2;
20996
+
20997
+ // True if a preview image holds every raster pixel in view, and so is showing
20998
+ // the data itself rather than a scaled-down rendering of it. Only such an
20999
+ // image is worth drawing as squares: magnifying a coarser preview would show
21000
+ // crisp squares that are not pixels of the raster.
21001
+ function previewHasSourcePixels(width, height, sourceWidth, sourceHeight) {
21002
+ if (!(sourceWidth > 0) || !(sourceHeight > 0)) return false;
21003
+ return width >= sourceWidth && height >= sourceHeight;
21004
+ }
21005
+
21006
+ // sourcePixels: from previewHasSourcePixels()
21007
+ // magnification: screen pixels per pixel of the preview image
21008
+ function rasterPreviewIsSmoothed(sourcePixels, magnification) {
21009
+ return !sourcePixels || !(magnification >= MIN_CRISP_MAGNIFICATION);
21010
+ }
21011
+
20965
21012
  var MAX_VIEWPORT_PREVIEW_PIXELS = 6e6;
20966
21013
  var cache$1 = new WeakMap();
20967
21014
  var requestId$1 = 0;
@@ -20988,6 +21035,7 @@
20988
21035
  stats = getCachedRasterScalingStats(params, timing);
20989
21036
  timing.renderStart = getTimer$1();
20990
21037
  preview = internal.renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
21038
+ if (preview) preview.sourcePixels = params.sourcePixels;
20991
21039
  timing.renderMs = getTimer$1() - timing.renderStart;
20992
21040
  logRasterPreviewTiming(params, timing);
20993
21041
  current = cache$1.get(layer);
@@ -21039,6 +21087,7 @@
21039
21087
  return {
21040
21088
  key: key,
21041
21089
  needed: needed,
21090
+ sourcePixels: previewHasSourcePixels(width, height, crop.width, crop.height),
21042
21091
  raster: raster,
21043
21092
  grid: grid,
21044
21093
  recipe: recipe,
@@ -21197,6 +21246,7 @@
21197
21246
  }
21198
21247
  timing.renderStart = getTimer();
21199
21248
  preview = internal.renderRasterPreview(grid, params.recipe, grid.width, grid.height, params.stats);
21249
+ preview.sourcePixels = params.sourcePixels;
21200
21250
  applyCoverageMask(preview, grid.coverage);
21201
21251
  timing.renderMs = getTimer() - timing.renderStart;
21202
21252
  logRasterReprojectionTiming(params, timing);
@@ -21220,12 +21270,13 @@
21220
21270
  var viewBbox = ext && ext.getBounds().toArray();
21221
21271
  var bbox = rasterBbox && viewBbox && internal.intersectBboxes(rasterBbox, viewBbox);
21222
21272
  var size = bbox && getPreviewSize(ext, sourceGrid);
21223
- var recipe, stats;
21273
+ var recipe, stats, sourcePixels;
21224
21274
  if (!sourceCRS || !displayCRS || !sourceGrid || !sourceGrid.samples || !bbox || !size) return null;
21225
21275
  recipe = internal.getRasterViewRecipe(sourceGrid, raster.view && raster.view.recipe);
21226
21276
  stats = internal.getRasterViewScalingStats(raster, recipe);
21277
+ sourcePixels = outputHasSourcePixels(sourceGrid, bbox, sourceCRS, displayCRS, size);
21227
21278
  return {
21228
- key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe),
21279
+ key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe, sourcePixels),
21229
21280
  grid: sourceGrid,
21230
21281
  sourceCRS: sourceCRS,
21231
21282
  displayCRS: displayCRS,
@@ -21234,11 +21285,49 @@
21234
21285
  height: size.height,
21235
21286
  recipe: recipe,
21236
21287
  stats: stats,
21237
- sampleMethod: getRasterReprojectionSampleMethod(),
21288
+ sourcePixels: sourcePixels,
21289
+ sampleMethod: getRasterReprojectionSampleMethod(sourcePixels),
21238
21290
  meshInterval: 32
21239
21291
  };
21240
21292
  }
21241
21293
 
21294
+ // True if the reprojected grid has room for every source pixel in view, so that
21295
+ // sampling it without interpolation gives back the raster's own pixels. This is
21296
+ // the zoomed-in case: reprojecting a raster to fewer pixels than it has needs
21297
+ // interpolation to avoid dropping data.
21298
+ function outputHasSourcePixels(grid, bbox, sourceCRS, displayCRS, size) {
21299
+ var sourceSize = getSourcePixelsInView(grid, bbox, sourceCRS, displayCRS);
21300
+ return !!sourceSize && previewHasSourcePixels(
21301
+ size.width, size.height, sourceSize.width, sourceSize.height);
21302
+ }
21303
+
21304
+ // Estimates how many of the raster's pixels a view covers, by taking the part
21305
+ // of the source grid that the view maps back onto. Returns null if the view
21306
+ // does not map back (e.g. it includes coordinates outside the projection).
21307
+ function getSourcePixelsInView(grid, bbox, sourceCRS, displayCRS) {
21308
+ var transform = internal.getProjTransform2(displayCRS, sourceCRS);
21309
+ var xx = [bbox[0], (bbox[0] + bbox[2]) / 2, bbox[2]];
21310
+ var yy = [bbox[1], (bbox[1] + bbox[3]) / 2, bbox[3]];
21311
+ var xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
21312
+ var p;
21313
+ if (!transform) return null;
21314
+ for (var i = 0; i < xx.length; i++) {
21315
+ for (var j = 0; j < yy.length; j++) {
21316
+ p = transform(xx[i], yy[j]);
21317
+ if (!p || !isFinite(p[0]) || !isFinite(p[1])) continue;
21318
+ if (p[0] < xmin) xmin = p[0];
21319
+ if (p[0] > xmax) xmax = p[0];
21320
+ if (p[1] < ymin) ymin = p[1];
21321
+ if (p[1] > ymax) ymax = p[1];
21322
+ }
21323
+ }
21324
+ if (!(xmax > xmin) || !(ymax > ymin)) return null;
21325
+ return {
21326
+ width: (xmax - xmin) / (grid.bbox[2] - grid.bbox[0]) * grid.width,
21327
+ height: (ymax - ymin) / (grid.bbox[3] - grid.bbox[1]) * grid.height
21328
+ };
21329
+ }
21330
+
21242
21331
  function getPreviewSize(ext, grid) {
21243
21332
  var pixelRatio = GUI.getPixelRatio();
21244
21333
  var t = ext.getTransform(pixelRatio);
@@ -21253,10 +21342,11 @@
21253
21342
  return {width: width, height: height};
21254
21343
  }
21255
21344
 
21256
- function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe) {
21345
+ function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe, sourcePixels) {
21257
21346
  return [
21258
21347
  size.width,
21259
21348
  size.height,
21349
+ sourcePixels,
21260
21350
  bbox.join(','),
21261
21351
  internal.crsToProj4(sourceCRS),
21262
21352
  internal.crsToProj4(displayCRS),
@@ -21264,7 +21354,7 @@
21264
21354
  recipe.scaling,
21265
21355
  recipe.scaleRange && recipe.scaleRange.join(','),
21266
21356
  recipe.percentileRange && recipe.percentileRange.join(','),
21267
- getRasterReprojectionSampleMethod(),
21357
+ getRasterReprojectionSampleMethod(sourcePixels),
21268
21358
  layer.raster && layer.raster.grid && layer.raster.grid.samples && layer.raster.grid.samples.length
21269
21359
  ].join('|');
21270
21360
  }
@@ -21292,10 +21382,13 @@
21292
21382
  }
21293
21383
  }
21294
21384
 
21295
- function getRasterReprojectionSampleMethod() {
21385
+ function getRasterReprojectionSampleMethod(sourcePixels) {
21296
21386
  var vars = GUI.getUrlVars();
21297
21387
  var val = vars['raster-bilinear'] ?? vars.raster_bilinear;
21298
- return val === false || val == '0' ? 'nearest' : 'bilinear';
21388
+ if (val === false || val == '0') return 'nearest';
21389
+ // Interpolating between pixels when there is room for all of them would blur
21390
+ // the data away for nothing.
21391
+ return sourcePixels ? 'nearest' : 'bilinear';
21299
21392
  }
21300
21393
 
21301
21394
  function logRasterReprojectionTiming(params, timing) {
@@ -21584,7 +21677,7 @@
21584
21677
  var raster = layer.raster;
21585
21678
  var options = opts || {};
21586
21679
  var preview = null;
21587
- var bbox;
21680
+ var bbox, grid, sourcePixels;
21588
21681
  if (layer.gui && layer.gui.dynamic_crs) {
21589
21682
  preview = getCachedRasterReprojectedPreview(layer, _ext);
21590
21683
  if (!preview) {
@@ -21595,7 +21688,7 @@
21595
21688
  }
21596
21689
  bbox = preview.bbox;
21597
21690
  if (!preview.pixels || !bbox) return;
21598
- drawRasterPreview(preview, bbox);
21691
+ drawRasterPreview(preview, bbox, preview.sourcePixels);
21599
21692
  if (options.action != 'nav' && options.onViewportPreviewReady) {
21600
21693
  scheduleRasterReprojectedPreview(layer, _ext, options.onViewportPreviewReady);
21601
21694
  }
@@ -21603,15 +21696,21 @@
21603
21696
  }
21604
21697
  preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
21605
21698
  bbox = preview && preview.bbox;
21699
+ sourcePixels = preview && preview.sourcePixels;
21606
21700
  if (!preview) {
21701
+ // The whole-raster preview made at import time, which is scaled down if
21702
+ // the raster is large.
21607
21703
  preview = raster && internal.getRasterPreview(raster);
21608
21704
  bbox = raster && internal.getRasterBBox(raster);
21705
+ grid = raster && internal.getRasterGrid(raster);
21706
+ sourcePixels = !!(preview && grid) && previewHasSourcePixels(
21707
+ preview.width, preview.height, grid.width, grid.height);
21609
21708
  if (options.action != 'nav' && options.onViewportPreviewReady) {
21610
21709
  scheduleRasterViewportPreview(layer, _ext, options.onViewportPreviewReady);
21611
21710
  }
21612
21711
  }
21613
21712
  if (!preview || !preview.pixels || !bbox) return;
21614
- drawRasterPreview(preview, bbox);
21713
+ drawRasterPreview(preview, bbox, sourcePixels);
21615
21714
  };
21616
21715
 
21617
21716
  /*
@@ -21813,12 +21912,16 @@
21813
21912
  }
21814
21913
  }
21815
21914
 
21816
- function drawRasterPreview(preview, bbox) {
21915
+ // sourcePixels: true if one pixel of the preview is one pixel of the raster,
21916
+ // so that drawing it as squares shows the data rather than an artifact of
21917
+ // whatever it was scaled down to.
21918
+ function drawRasterPreview(preview, bbox, sourcePixels) {
21817
21919
  var img = getRasterCanvas(preview);
21818
21920
  var t = _ext.getTransform(GUI.getPixelRatio());
21819
21921
  var p1 = t.transform(bbox[0], bbox[3]);
21820
21922
  var p2 = t.transform(bbox[2], bbox[1]);
21821
- _ctx.imageSmoothingEnabled = true;
21923
+ var magnification = Math.abs(p2[0] - p1[0]) / img.width / GUI.getPixelRatio();
21924
+ _ctx.imageSmoothingEnabled = rasterPreviewIsSmoothed(sourcePixels, magnification);
21822
21925
  _ctx.drawImage(img, p1[0], p1[1], p2[0] - p1[0], p2[1] - p1[1]);
21823
21926
  }
21824
21927