mapshaper 0.7.49 → 0.7.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.49",
3
+ "version": "0.7.51",
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",
@@ -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
 
@@ -7398,14 +7404,20 @@
7398
7404
  }
7399
7405
  }
7400
7406
 
7401
- function turnOff() {
7402
- var target;
7403
- if (catalog) catalog.reset(); // re-enable clickable catalog
7407
+ // Draw and select what was imported. Called when import mode ends, and by
7408
+ // importQueuedFiles() if something else has already ended it.
7409
+ function finishImport() {
7404
7410
  if (importCount > 0) {
7405
7411
  onImportComplete();
7406
7412
  importTotal += importCount;
7407
7413
  importCount = 0;
7408
7414
  }
7415
+ }
7416
+
7417
+ function turnOff() {
7418
+ var target;
7419
+ if (catalog) catalog.reset(); // re-enable clickable catalog
7420
+ finishImport();
7409
7421
  gui.clearProgressMessage();
7410
7422
  initialImport = false; // unset 'quick view' mode, if on
7411
7423
  clearQueuedFiles();
@@ -7432,8 +7444,13 @@
7432
7444
  gui.alert(e.message, 'Import error');
7433
7445
  }
7434
7446
  if (gui.getMode() == 'import') {
7435
- // Mode could also be 'alert' if an error is thrown and handled
7436
- gui.clearMode();
7447
+ gui.clearMode(); // leaving import mode finishes the import
7448
+ } else {
7449
+ // A popup shown while files were loading (an alert about one file's
7450
+ // metadata, say) takes the app out of import mode, so leaving it is no
7451
+ // longer what finishes the import. Without this, the files that did load
7452
+ // would sit in the catalog undrawn.
7453
+ finishImport();
7437
7454
  }
7438
7455
  if (undoTransaction && imported) {
7439
7456
  addUndoTransactionToHistory(gui, undoTransaction, {
@@ -8056,6 +8073,15 @@
8056
8073
  });
8057
8074
  }
8058
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
+
8059
8085
  data.forEach(d => {
8060
8086
  var basename = fileBase(d);
8061
8087
  var type = fileType(d);
@@ -8067,6 +8093,8 @@
8067
8093
  } else if (type == 'dbf') {
8068
8094
  d.filename = d.name;
8069
8095
  d.group = key(basename, 'dbf');
8096
+ } else if (type == 'aux' && auxSourceGroup(d)) {
8097
+ d.group = auxSourceGroup(d);
8070
8098
  } else if ((type == 'png' || type == 'jpeg') || isRasterImagePart(d.name) && hasRasterImage(basename)) {
8071
8099
  d.group = key(basename, type == 'png' || type == 'jpeg' ? type : getRasterImageGroupType(data, basename));
8072
8100
  if (type == 'png' || type == 'jpeg') d.filename = d.name;
@@ -10630,6 +10658,8 @@
10630
10658
  var unsupportedMsg = "Exporting is not supported in this browser";
10631
10659
  var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode));
10632
10660
  var layersArr = [];
10661
+ var menuFormats = null; // formats the format menu was built with
10662
+ var formatPickedByUser = false;
10633
10663
  var toggleBtn = null; // checkbox <input> for toggling layer selection
10634
10664
  var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
10635
10665
  var ofileName = gui.container.findChild('#ofile-name');
@@ -10698,6 +10728,7 @@
10698
10728
 
10699
10729
  function turnOn() {
10700
10730
  layersArr = initLayerMenu();
10731
+ formatPickedByUser = false;
10701
10732
  // initZipOption();
10702
10733
  initFormatMenu();
10703
10734
  updateExportCheckboxes();
@@ -10879,6 +10910,7 @@
10879
10910
  }
10880
10911
 
10881
10912
  function updateToggleBtn() {
10913
+ refreshFormatMenu(); // the layer selection decides which formats apply
10882
10914
  updateExportCheckboxes(); // checkbox visibility is affected by number of export layers
10883
10915
  if (!toggleBtn) return;
10884
10916
  var state = getSelectionState();
@@ -10901,19 +10933,44 @@
10901
10933
  }
10902
10934
 
10903
10935
  function getDefaultExportFormat() {
10936
+ var layers = getExportFormatLayers();
10904
10937
  var active = model.getActiveLayer();
10905
10938
  var dataset = active.dataset;
10906
10939
  var inputFmt = dataset.info && dataset.info.input_formats &&
10907
10940
  dataset.info.input_formats[0];
10908
- if (active.layer && internal.layerHasRaster(active.layer)) 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';
10909
10947
  return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
10910
10948
  }
10911
10949
 
10912
10950
  function getExportFormats() {
10913
- return ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', 'svg', internal.PACKAGE_EXT];
10951
+ var formats = ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', 'svg', internal.PACKAGE_EXT];
10952
+ // GeoTIFF is the one format here that only accepts raster layers, so it is
10953
+ // offered only when there is a raster to export.
10954
+ if (getExportFormatLayers().some(hasRaster)) formats.push('geotiff');
10955
+ return formats;
10956
+ }
10957
+
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() : [];
10962
+ var active = model.getActiveLayer();
10963
+ if (selected.length > 0) {
10964
+ return selected.map(function(o) { return o.layer; });
10965
+ }
10966
+ return active && active.layer ? [active.layer] : [];
10914
10967
  }
10915
10968
 
10916
- function initFormatMenu() {
10969
+ function hasRaster(lyr) {
10970
+ return internal.layerHasRaster(lyr);
10971
+ }
10972
+
10973
+ function initFormatMenu(preferredFmt) {
10917
10974
  var formats = getExportFormats();
10918
10975
  var items = formats.map(function(fmt) {
10919
10976
  return utils$1.format('<td><label><input type="radio" name="format" value="%s"' +
@@ -10925,13 +10982,39 @@
10925
10982
  }
10926
10983
  table += '</table>';
10927
10984
  menu.findChild('.export-formats').html(table);
10928
- menu.findChild('.export-formats input[value="' + getDefaultExportFormat() + '"]').node().checked = true;
10985
+ menuFormats = formats;
10986
+ setSelectedFormat(formats.includes(preferredFmt) ? preferredFmt :
10987
+ getDefaultExportFormat());
10929
10988
  // update save-as settings when value changes
10930
10989
  menu.findChildren('input[type="radio"]').forEach(el => {
10931
- el.on('change', updateExportCheckboxes);
10990
+ el.on('change', onFormatChange);
10932
10991
  });
10933
10992
  }
10934
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
+
10935
11018
 
10936
11019
  // function getInputFormats() {
10937
11020
  // return model.getDatasets().reduce(function(memo, d) {
@@ -12073,6 +12156,7 @@
12073
12156
  }
12074
12157
 
12075
12158
  function isAppUndoEnabled() {
12159
+ if (getUndoQueryValue() == 'off') return false;
12076
12160
  return appUndoForcedByUrl() || appUndoSettingIsOn();
12077
12161
  }
12078
12162
 
@@ -20962,6 +21046,31 @@
20962
21046
 
20963
21047
  utils$1.inherit(MapExtent, EventDispatcher);
20964
21048
 
21049
+ // Rules for drawing a raster that is being displayed larger than its data.
21050
+ // Kept free of browser and mapshaper dependencies so they can be tested
21051
+ // directly.
21052
+
21053
+ // Screen pixels per raster pixel at which a magnified raster is drawn as
21054
+ // squares instead of being smoothed. Below this there is little detail to lose
21055
+ // and smoothing looks better; above it, the smoothing is what a user sees
21056
+ // instead of the data.
21057
+ var MIN_CRISP_MAGNIFICATION = 2;
21058
+
21059
+ // True if a preview image holds every raster pixel in view, and so is showing
21060
+ // the data itself rather than a scaled-down rendering of it. Only such an
21061
+ // image is worth drawing as squares: magnifying a coarser preview would show
21062
+ // crisp squares that are not pixels of the raster.
21063
+ function previewHasSourcePixels(width, height, sourceWidth, sourceHeight) {
21064
+ if (!(sourceWidth > 0) || !(sourceHeight > 0)) return false;
21065
+ return width >= sourceWidth && height >= sourceHeight;
21066
+ }
21067
+
21068
+ // sourcePixels: from previewHasSourcePixels()
21069
+ // magnification: screen pixels per pixel of the preview image
21070
+ function rasterPreviewIsSmoothed(sourcePixels, magnification) {
21071
+ return !sourcePixels || !(magnification >= MIN_CRISP_MAGNIFICATION);
21072
+ }
21073
+
20965
21074
  var MAX_VIEWPORT_PREVIEW_PIXELS = 6e6;
20966
21075
  var cache$1 = new WeakMap();
20967
21076
  var requestId$1 = 0;
@@ -20988,6 +21097,7 @@
20988
21097
  stats = getCachedRasterScalingStats(params, timing);
20989
21098
  timing.renderStart = getTimer$1();
20990
21099
  preview = internal.renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
21100
+ if (preview) preview.sourcePixels = params.sourcePixels;
20991
21101
  timing.renderMs = getTimer$1() - timing.renderStart;
20992
21102
  logRasterPreviewTiming(params, timing);
20993
21103
  current = cache$1.get(layer);
@@ -21039,6 +21149,7 @@
21039
21149
  return {
21040
21150
  key: key,
21041
21151
  needed: needed,
21152
+ sourcePixels: previewHasSourcePixels(width, height, crop.width, crop.height),
21042
21153
  raster: raster,
21043
21154
  grid: grid,
21044
21155
  recipe: recipe,
@@ -21197,6 +21308,7 @@
21197
21308
  }
21198
21309
  timing.renderStart = getTimer();
21199
21310
  preview = internal.renderRasterPreview(grid, params.recipe, grid.width, grid.height, params.stats);
21311
+ preview.sourcePixels = params.sourcePixels;
21200
21312
  applyCoverageMask(preview, grid.coverage);
21201
21313
  timing.renderMs = getTimer() - timing.renderStart;
21202
21314
  logRasterReprojectionTiming(params, timing);
@@ -21220,12 +21332,13 @@
21220
21332
  var viewBbox = ext && ext.getBounds().toArray();
21221
21333
  var bbox = rasterBbox && viewBbox && internal.intersectBboxes(rasterBbox, viewBbox);
21222
21334
  var size = bbox && getPreviewSize(ext, sourceGrid);
21223
- var recipe, stats;
21335
+ var recipe, stats, sourcePixels;
21224
21336
  if (!sourceCRS || !displayCRS || !sourceGrid || !sourceGrid.samples || !bbox || !size) return null;
21225
21337
  recipe = internal.getRasterViewRecipe(sourceGrid, raster.view && raster.view.recipe);
21226
21338
  stats = internal.getRasterViewScalingStats(raster, recipe);
21339
+ sourcePixels = outputHasSourcePixels(sourceGrid, bbox, sourceCRS, displayCRS, size);
21227
21340
  return {
21228
- key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe),
21341
+ key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe, sourcePixels),
21229
21342
  grid: sourceGrid,
21230
21343
  sourceCRS: sourceCRS,
21231
21344
  displayCRS: displayCRS,
@@ -21234,11 +21347,49 @@
21234
21347
  height: size.height,
21235
21348
  recipe: recipe,
21236
21349
  stats: stats,
21237
- sampleMethod: getRasterReprojectionSampleMethod(),
21350
+ sourcePixels: sourcePixels,
21351
+ sampleMethod: getRasterReprojectionSampleMethod(sourcePixels),
21238
21352
  meshInterval: 32
21239
21353
  };
21240
21354
  }
21241
21355
 
21356
+ // True if the reprojected grid has room for every source pixel in view, so that
21357
+ // sampling it without interpolation gives back the raster's own pixels. This is
21358
+ // the zoomed-in case: reprojecting a raster to fewer pixels than it has needs
21359
+ // interpolation to avoid dropping data.
21360
+ function outputHasSourcePixels(grid, bbox, sourceCRS, displayCRS, size) {
21361
+ var sourceSize = getSourcePixelsInView(grid, bbox, sourceCRS, displayCRS);
21362
+ return !!sourceSize && previewHasSourcePixels(
21363
+ size.width, size.height, sourceSize.width, sourceSize.height);
21364
+ }
21365
+
21366
+ // Estimates how many of the raster's pixels a view covers, by taking the part
21367
+ // of the source grid that the view maps back onto. Returns null if the view
21368
+ // does not map back (e.g. it includes coordinates outside the projection).
21369
+ function getSourcePixelsInView(grid, bbox, sourceCRS, displayCRS) {
21370
+ var transform = internal.getProjTransform2(displayCRS, sourceCRS);
21371
+ var xx = [bbox[0], (bbox[0] + bbox[2]) / 2, bbox[2]];
21372
+ var yy = [bbox[1], (bbox[1] + bbox[3]) / 2, bbox[3]];
21373
+ var xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
21374
+ var p;
21375
+ if (!transform) return null;
21376
+ for (var i = 0; i < xx.length; i++) {
21377
+ for (var j = 0; j < yy.length; j++) {
21378
+ p = transform(xx[i], yy[j]);
21379
+ if (!p || !isFinite(p[0]) || !isFinite(p[1])) continue;
21380
+ if (p[0] < xmin) xmin = p[0];
21381
+ if (p[0] > xmax) xmax = p[0];
21382
+ if (p[1] < ymin) ymin = p[1];
21383
+ if (p[1] > ymax) ymax = p[1];
21384
+ }
21385
+ }
21386
+ if (!(xmax > xmin) || !(ymax > ymin)) return null;
21387
+ return {
21388
+ width: (xmax - xmin) / (grid.bbox[2] - grid.bbox[0]) * grid.width,
21389
+ height: (ymax - ymin) / (grid.bbox[3] - grid.bbox[1]) * grid.height
21390
+ };
21391
+ }
21392
+
21242
21393
  function getPreviewSize(ext, grid) {
21243
21394
  var pixelRatio = GUI.getPixelRatio();
21244
21395
  var t = ext.getTransform(pixelRatio);
@@ -21253,10 +21404,11 @@
21253
21404
  return {width: width, height: height};
21254
21405
  }
21255
21406
 
21256
- function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe) {
21407
+ function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe, sourcePixels) {
21257
21408
  return [
21258
21409
  size.width,
21259
21410
  size.height,
21411
+ sourcePixels,
21260
21412
  bbox.join(','),
21261
21413
  internal.crsToProj4(sourceCRS),
21262
21414
  internal.crsToProj4(displayCRS),
@@ -21264,7 +21416,7 @@
21264
21416
  recipe.scaling,
21265
21417
  recipe.scaleRange && recipe.scaleRange.join(','),
21266
21418
  recipe.percentileRange && recipe.percentileRange.join(','),
21267
- getRasterReprojectionSampleMethod(),
21419
+ getRasterReprojectionSampleMethod(sourcePixels),
21268
21420
  layer.raster && layer.raster.grid && layer.raster.grid.samples && layer.raster.grid.samples.length
21269
21421
  ].join('|');
21270
21422
  }
@@ -21292,10 +21444,13 @@
21292
21444
  }
21293
21445
  }
21294
21446
 
21295
- function getRasterReprojectionSampleMethod() {
21447
+ function getRasterReprojectionSampleMethod(sourcePixels) {
21296
21448
  var vars = GUI.getUrlVars();
21297
21449
  var val = vars['raster-bilinear'] ?? vars.raster_bilinear;
21298
- return val === false || val == '0' ? 'nearest' : 'bilinear';
21450
+ if (val === false || val == '0') return 'nearest';
21451
+ // Interpolating between pixels when there is room for all of them would blur
21452
+ // the data away for nothing.
21453
+ return sourcePixels ? 'nearest' : 'bilinear';
21299
21454
  }
21300
21455
 
21301
21456
  function logRasterReprojectionTiming(params, timing) {
@@ -21584,7 +21739,7 @@
21584
21739
  var raster = layer.raster;
21585
21740
  var options = opts || {};
21586
21741
  var preview = null;
21587
- var bbox;
21742
+ var bbox, grid, sourcePixels;
21588
21743
  if (layer.gui && layer.gui.dynamic_crs) {
21589
21744
  preview = getCachedRasterReprojectedPreview(layer, _ext);
21590
21745
  if (!preview) {
@@ -21595,7 +21750,7 @@
21595
21750
  }
21596
21751
  bbox = preview.bbox;
21597
21752
  if (!preview.pixels || !bbox) return;
21598
- drawRasterPreview(preview, bbox);
21753
+ drawRasterPreview(preview, bbox, preview.sourcePixels);
21599
21754
  if (options.action != 'nav' && options.onViewportPreviewReady) {
21600
21755
  scheduleRasterReprojectedPreview(layer, _ext, options.onViewportPreviewReady);
21601
21756
  }
@@ -21603,15 +21758,21 @@
21603
21758
  }
21604
21759
  preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
21605
21760
  bbox = preview && preview.bbox;
21761
+ sourcePixels = preview && preview.sourcePixels;
21606
21762
  if (!preview) {
21763
+ // The whole-raster preview made at import time, which is scaled down if
21764
+ // the raster is large.
21607
21765
  preview = raster && internal.getRasterPreview(raster);
21608
21766
  bbox = raster && internal.getRasterBBox(raster);
21767
+ grid = raster && internal.getRasterGrid(raster);
21768
+ sourcePixels = !!(preview && grid) && previewHasSourcePixels(
21769
+ preview.width, preview.height, grid.width, grid.height);
21609
21770
  if (options.action != 'nav' && options.onViewportPreviewReady) {
21610
21771
  scheduleRasterViewportPreview(layer, _ext, options.onViewportPreviewReady);
21611
21772
  }
21612
21773
  }
21613
21774
  if (!preview || !preview.pixels || !bbox) return;
21614
- drawRasterPreview(preview, bbox);
21775
+ drawRasterPreview(preview, bbox, sourcePixels);
21615
21776
  };
21616
21777
 
21617
21778
  /*
@@ -21813,12 +21974,16 @@
21813
21974
  }
21814
21975
  }
21815
21976
 
21816
- function drawRasterPreview(preview, bbox) {
21977
+ // sourcePixels: true if one pixel of the preview is one pixel of the raster,
21978
+ // so that drawing it as squares shows the data rather than an artifact of
21979
+ // whatever it was scaled down to.
21980
+ function drawRasterPreview(preview, bbox, sourcePixels) {
21817
21981
  var img = getRasterCanvas(preview);
21818
21982
  var t = _ext.getTransform(GUI.getPixelRatio());
21819
21983
  var p1 = t.transform(bbox[0], bbox[3]);
21820
21984
  var p2 = t.transform(bbox[2], bbox[1]);
21821
- _ctx.imageSmoothingEnabled = true;
21985
+ var magnification = Math.abs(p2[0] - p1[0]) / img.width / GUI.getPixelRatio();
21986
+ _ctx.imageSmoothingEnabled = rasterPreviewIsSmoothed(sourcePixels, magnification);
21822
21987
  _ctx.drawImage(img, p1[0], p1[1], p2[0] - p1[0], p2[1] - p1[1]);
21823
21988
  }
21824
21989
 
@@ -25327,6 +25492,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
25327
25492
  return {
25328
25493
  layerCount: dataset.layers ? dataset.layers.length : 0,
25329
25494
  arcCount: dataset.arcs ? dataset.arcs.size() : 0,
25495
+ crs_string: dataset.info && dataset.info.crs_string || null,
25330
25496
  layers: (dataset.layers || []).map(function(lyr) {
25331
25497
  return {
25332
25498
  name: getLayerName(lyr),