mapshaper 0.7.48 → 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.
@@ -4544,6 +4544,77 @@
4544
4544
  return this;
4545
4545
  };
4546
4546
 
4547
+ // Low-level access to raster grid samples, shared by the code that reads or
4548
+ // rewrites pixels (-proj, -blur, clipping and preview rendering). These used
4549
+ // to be reimplemented per module, which let the nodata and coverage rules
4550
+ // drift apart.
4551
+ //
4552
+ // Samples are band-interleaved: pixel i occupies
4553
+ // samples[i * bands] through samples[i * bands + bands - 1].
4554
+
4555
+ // A pixel is nodata only when every one of its color-carrying bands matches
4556
+ // the nodata value. An alpha band is ignored, so a transparent pixel whose
4557
+ // color channels hold real values is still treated as data.
4558
+ function samplesAreNodataAtOffset(samples, offset, bands, nodata) {
4559
+ var n = Math.min(bands, 3);
4560
+ if (nodata === null || nodata === undefined) return false;
4561
+ for (var i = 0; i < n; i++) {
4562
+ if (samples[offset + i] != nodata) return false;
4563
+ }
4564
+ return true;
4565
+ }
4566
+
4567
+ function rasterPixelIsNodata(grid, pixelId) {
4568
+ return samplesAreNodataAtOffset(grid.samples, pixelId * grid.bands, grid.bands, grid.nodata);
4569
+ }
4570
+
4571
+ // The coverage mask is set by reprojection to record which output pixels
4572
+ // received source content, independently of the nodata fill value.
4573
+ function rasterPixelIsCovered(grid, pixelId) {
4574
+ return !grid.coverage || grid.coverage[pixelId] > 0;
4575
+ }
4576
+
4577
+ function rasterPixelIsValid(grid, pixelId) {
4578
+ return rasterPixelIsCovered(grid, pixelId) && !rasterPixelIsNodata(grid, pixelId);
4579
+ }
4580
+
4581
+ // False when every pixel is known to be valid, so callers can skip
4582
+ // per-pixel validity testing entirely.
4583
+ function rasterGridHasInvalidPixels(grid) {
4584
+ return !!grid.coverage || grid.nodata !== null && grid.nodata !== undefined;
4585
+ }
4586
+
4587
+ // One byte per pixel, so that code sampling a grid repeatedly can test
4588
+ // validity with a single lookup instead of re-reading every band. Returns
4589
+ // the coverage mask itself when there is no nodata value to fold in, and
4590
+ // null when every pixel is valid.
4591
+ function getRasterValidityMask(grid) {
4592
+ var n = grid.width * grid.height;
4593
+ var mask, i;
4594
+ if (grid.nodata === null || grid.nodata === undefined) return grid.coverage || null;
4595
+ mask = new Uint8Array(n);
4596
+ for (i = 0; i < n; i++) {
4597
+ mask[i] = rasterPixelIsValid(grid, i) ? 1 : 0;
4598
+ }
4599
+ return mask;
4600
+ }
4601
+
4602
+ function rasterSamplesAreFloat(samples) {
4603
+ return samples instanceof Float32Array || samples instanceof Float64Array;
4604
+ }
4605
+
4606
+ // Value range of a sample array, used to clamp computed values before
4607
+ // writing them back into an integer array.
4608
+ function getSampleArrayRange(samples) {
4609
+ if (samples instanceof Uint8Array || samples instanceof Uint8ClampedArray) return {min: 0, max: 255};
4610
+ if (samples instanceof Int8Array) return {min: -128, max: 127};
4611
+ if (samples instanceof Uint16Array) return {min: 0, max: 65535};
4612
+ if (samples instanceof Int16Array) return {min: -32768, max: 32767};
4613
+ if (samples instanceof Uint32Array) return {min: 0, max: 4294967295};
4614
+ if (samples instanceof Int32Array) return {min: -2147483648, max: 2147483647};
4615
+ return {min: -Infinity, max: Infinity};
4616
+ }
4617
+
4547
4618
  function runningInBrowser() {
4548
4619
  return typeof window !== 'undefined' && typeof window.document !== 'undefined';
4549
4620
  }
@@ -4823,6 +4894,9 @@
4823
4894
  function copyRasterGrid(grid) {
4824
4895
  var copy = utils.extend({}, grid);
4825
4896
  if (grid.samples) copy.samples = copyTypedArray(grid.samples);
4897
+ // The coverage mask is mutated in place by the reprojection code, so an
4898
+ // undo snapshot that shared it would track later edits.
4899
+ if (grid.coverage) copy.coverage = copyTypedArray(grid.coverage);
4826
4900
  if (grid.sampleBands) copy.sampleBands = grid.sampleBands.concat();
4827
4901
  if (grid.bbox) copy.bbox = grid.bbox.concat();
4828
4902
  if (grid.transform) copy.transform = copyObjectOrArray(grid.transform);
@@ -4930,6 +5004,69 @@
4930
5004
  return stats;
4931
5005
  }
4932
5006
 
5007
+ // Returns the band values and display color of the pixel containing a map
5008
+ // coordinate, for readouts like the GUI's right-click menu, or null if the point
5009
+ // falls outside the raster. Rotated grids are not supported, because pixels can
5010
+ // only be located through the axis-aligned bbox.
5011
+ // x, y: a point in the coordinate system of the raster's bbox
5012
+ function getRasterPixelAtMapXY(raster, x, y) {
5013
+ var grid = getRasterGrid(raster);
5014
+ var col, row, pixelId, offset, values, valid, isColor, band;
5015
+ if (!grid || !grid.samples || !grid.bbox || rasterGridIsRotated(grid)) return null;
5016
+ col = Math.floor(mapXToRasterPixel(grid, x));
5017
+ row = Math.floor(mapYToRasterPixel(grid, y));
5018
+ if (col < 0 || col >= grid.width || row < 0 || row >= grid.height) return null;
5019
+ pixelId = row * grid.width + col;
5020
+ offset = pixelId * grid.bands;
5021
+ values = [];
5022
+ for (band = 0; band < grid.bands; band++) {
5023
+ values.push(grid.samples[offset + band]);
5024
+ }
5025
+ // The renderer reads interleaved bands in display order, so three or more
5026
+ // bands are shown as R, G, B and (if present) alpha.
5027
+ isColor = grid.bands >= 3;
5028
+ valid = rasterPixelIsValid(grid, pixelId);
5029
+ return {
5030
+ col: col,
5031
+ row: row,
5032
+ values: values,
5033
+ isColor: isColor,
5034
+ isFloat: rasterSamplesAreFloat(grid.samples),
5035
+ valid: valid,
5036
+ // Only color images get a color, so that inspecting a measurement raster
5037
+ // does not trigger a scaling-stats scan of the whole grid.
5038
+ // [r, g, b, a] in the 0-255 range, for formatColor() or a canvas.
5039
+ color: isColor && valid ? getRasterPixelDisplayColor(raster, grid, pixelId) : null
5040
+ };
5041
+ }
5042
+
5043
+ // Returns [r, g, b, a] in the 0-255 range, using the same recipe and scaling as
5044
+ // the preview renderer, so that a readout agrees with how the pixel is drawn.
5045
+ function getRasterPixelDisplayColor(raster, grid, pixelId) {
5046
+ var view = getRasterView(raster);
5047
+ var recipe = getRasterViewRecipe(grid, view && view.recipe);
5048
+ var stats = getRasterViewScalingStats(raster, recipe);
5049
+ var sourceRange = recipe.scaling == 'none' ? getPixelTypeRange(grid.pixelType) : null;
5050
+ var displayRange = getDisplayRange(recipe.scaleRange);
5051
+ var offset = pixelId * grid.bands;
5052
+ var color = [0, 1, 2].map(function(band) {
5053
+ return scaleSample(grid.samples[offset + band], stats && stats[band], sourceRange, displayRange);
5054
+ });
5055
+ color.push(grid.bands >= 4 ?
5056
+ scaleSample(grid.samples[offset + 3], stats && stats[3], sourceRange, [0, 255]) : 255);
5057
+ return color;
5058
+ }
5059
+
5060
+ // Float samples print with binary noise at full precision (a Float32 holding
5061
+ // 1234.5678 prints as 1234.5677490234375), so they are rounded to the number of
5062
+ // digits that the type can actually represent.
5063
+ function formatRasterSampleValue(val, isFloat) {
5064
+ if (val === null || val === undefined) return '';
5065
+ if (!isFinite(val)) return String(val); // NaN, Infinity
5066
+ if (!isFloat) return String(val);
5067
+ return String(+val.toPrecision(7));
5068
+ }
5069
+
4933
5070
  function renderRasterGridPreview(grid, recipe, width, height, statsArg, sourceBbox, resamplingMethod) {
4934
5071
  var samples = grid.samples;
4935
5072
  var bands = grid.bands;
@@ -5172,10 +5309,22 @@
5172
5309
  return (sy * grid.width + sx) * bands;
5173
5310
  }
5174
5311
 
5312
+ // A grid is north-up when its affine transform has no rotation/skew terms.
5313
+ // Cropping and reprojection both index pixels through the axis-aligned bbox,
5314
+ // so they can only handle north-up grids.
5315
+ function rasterGridIsRotated(grid) {
5316
+ var t = grid && grid.transform;
5317
+ return !!t && (t[1] !== 0 || t[3] !== 0);
5318
+ }
5319
+
5175
5320
  function clipRasterToBBox(lyr, bbox, opts) {
5176
5321
  var raster = lyr.raster;
5177
5322
  var grid = getRasterGrid(raster);
5178
- var clipBbox = intersectBboxes(grid.bbox, bbox);
5323
+ var clipBbox;
5324
+ if (rasterGridIsRotated(grid)) {
5325
+ stop('Clipping a rotated or skewed raster is not supported');
5326
+ }
5327
+ clipBbox = intersectBboxes(grid.bbox, bbox);
5179
5328
  if (!clipBbox) {
5180
5329
  warn('Raster clipping rectangle does not intersect the raster layer');
5181
5330
  return false;
@@ -5236,11 +5385,30 @@
5236
5385
  width: crop.width,
5237
5386
  height: crop.height,
5238
5387
  samples: samples,
5388
+ coverage: cropRasterCoverage(grid, crop),
5239
5389
  bbox: bbox,
5240
5390
  transform: updateTransformForBBox(grid.transform, bbox, crop.width, crop.height)
5241
5391
  });
5242
5392
  }
5243
5393
 
5394
+ // The coverage mask is one byte per pixel (not per sample), and must be
5395
+ // cropped alongside the samples -- consumers index it with the cropped
5396
+ // grid's dimensions.
5397
+ function cropRasterCoverage(grid, crop) {
5398
+ var coverage = grid.coverage;
5399
+ var cropped, src, dest, x, y;
5400
+ if (!coverage) return coverage;
5401
+ cropped = new Uint8Array(crop.width * crop.height);
5402
+ for (y = 0; y < crop.height; y++) {
5403
+ src = (crop.y + y) * grid.width + crop.x;
5404
+ dest = y * crop.width;
5405
+ for (x = 0; x < crop.width; x++) {
5406
+ cropped[dest + x] = coverage[src + x];
5407
+ }
5408
+ }
5409
+ return cropped;
5410
+ }
5411
+
5244
5412
  function getRasterLayerBounds(lyr) {
5245
5413
  var bbox = lyr.raster && getRasterBBox(lyr.raster);
5246
5414
  return bbox && bbox.length == 4 ? new Bounds(bbox) : null;
@@ -5501,11 +5669,7 @@
5501
5669
  }
5502
5670
 
5503
5671
  function allSamplesAreNoData(data, offset, bands, noData) {
5504
- var n = Math.min(bands, 3);
5505
- for (var i = 0; i < n; i++) {
5506
- if (data[offset + i] != noData) return false;
5507
- }
5508
- return true;
5672
+ return samplesAreNodataAtOffset(data, offset, bands, noData);
5509
5673
  }
5510
5674
 
5511
5675
  function copyObjectOrArray(obj) {
@@ -6539,7 +6703,6 @@
6539
6703
  notifyUndoWarning(gui, 'Undo restore data was not deleted', e);
6540
6704
  });
6541
6705
  }, {
6542
- maxStates: opts.maxStates,
6543
6706
  evictToken: evictToken,
6544
6707
  preserveOnModeChange: true
6545
6708
  });
@@ -6713,8 +6876,6 @@
6713
6876
  // instead.
6714
6877
 
6715
6878
 
6716
- var DEFAULT_HISTORY_LIMIT = 10;
6717
-
6718
6879
  // Returns true if the manifest, URL query, gui-installed checker, or
6719
6880
  // localStorage indicates that app-level undo should be active.
6720
6881
  function appUndoIsEnabled(gui) {
@@ -6754,11 +6915,6 @@
6754
6915
  return gui.storedUndoHistory;
6755
6916
  }
6756
6917
 
6757
- function getUndoHistoryLimit(gui) {
6758
- var opt = gui && gui.options && gui.options.undoHistoryLimit;
6759
- return opt > 0 ? opt : DEFAULT_HISTORY_LIMIT;
6760
- }
6761
-
6762
6918
  // Construct an UndoTransaction if and only if app undo is enabled and the
6763
6919
  // transaction can plausibly be added to history. Returns null when undo is
6764
6920
  // off, when the gui's undo manager is missing, or when the UndoTransaction
@@ -6772,12 +6928,13 @@
6772
6928
  return Transaction ? new Transaction(label || '') : null;
6773
6929
  }
6774
6930
 
6775
- // Add a captured transaction to gui-level history. Pass options through to
6776
- // stored-undo-history.addTransaction(); fills in maxStates when not provided.
6931
+ // Add a captured transaction to gui-level history. Options are passed through
6932
+ // to stored-undo-history.addTransaction(). History is not capped by entry
6933
+ // count: entries are dropped only when their restore data no longer fits in
6934
+ // the payload store.
6777
6935
  function addUndoTransactionToHistory(gui, tx, opts) {
6778
6936
  if (!tx) return Promise.resolve(null);
6779
- var fullOpts = Object.assign({maxStates: getUndoHistoryLimit(gui)}, opts || {});
6780
- return getStoredUndoHistory(gui).addTransaction(tx, fullOpts).catch(function(e) {
6937
+ return getStoredUndoHistory(gui).addTransaction(tx, opts || {}).catch(function(e) {
6781
6938
  console.error(e);
6782
6939
  });
6783
6940
  }
@@ -7241,14 +7398,20 @@
7241
7398
  }
7242
7399
  }
7243
7400
 
7244
- function turnOff() {
7245
- var target;
7246
- 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() {
7247
7404
  if (importCount > 0) {
7248
7405
  onImportComplete();
7249
7406
  importTotal += importCount;
7250
7407
  importCount = 0;
7251
7408
  }
7409
+ }
7410
+
7411
+ function turnOff() {
7412
+ var target;
7413
+ if (catalog) catalog.reset(); // re-enable clickable catalog
7414
+ finishImport();
7252
7415
  gui.clearProgressMessage();
7253
7416
  initialImport = false; // unset 'quick view' mode, if on
7254
7417
  clearQueuedFiles();
@@ -7275,8 +7438,13 @@
7275
7438
  gui.alert(e.message, 'Import error');
7276
7439
  }
7277
7440
  if (gui.getMode() == 'import') {
7278
- // Mode could also be 'alert' if an error is thrown and handled
7279
- 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();
7280
7448
  }
7281
7449
  if (undoTransaction && imported) {
7282
7450
  addUndoTransactionToHistory(gui, undoTransaction, {
@@ -9336,7 +9504,9 @@
9336
9504
  this.runCommand = function(str) {
9337
9505
  str = str.trim();
9338
9506
  if (!str) return;
9339
- gui.toggleSidebarPanel('console');
9507
+ if (!gui.consoleIsOpen()) {
9508
+ gui.toggleSidebarPanel('console');
9509
+ }
9340
9510
  submit(str);
9341
9511
  };
9342
9512
 
@@ -9884,7 +10054,6 @@
9884
10054
  error: err,
9885
10055
  flags: flags,
9886
10056
  entryPrefix: 'command',
9887
- maxStates: getCommandUndoHistoryLimit(),
9888
10057
  onUndo: function() {
9889
10058
  gui.session.setCommandsActive(historyIds, false);
9890
10059
  },
@@ -9899,11 +10068,6 @@
9899
10068
  return appUndoIsEnabled(gui);
9900
10069
  }
9901
10070
 
9902
- function getCommandUndoHistoryLimit() {
9903
- var opt = gui.options && gui.options.undoHistoryLimit;
9904
- return opt > 0 ? opt : 10;
9905
- }
9906
-
9907
10071
  function logCommandTiming(commandString, commands, err, totalMillis, undoTiming) {
9908
10072
  var enabled = getQueryFlag('command-timing') == 'on' ||
9909
10073
  getQueryFlag('undo-timing') == 'on';
@@ -10752,12 +10916,23 @@
10752
10916
  var dataset = active.dataset;
10753
10917
  var inputFmt = dataset.info && dataset.info.input_formats &&
10754
10918
  dataset.info.input_formats[0];
10755
- 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';
10756
10922
  return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
10757
10923
  }
10758
10924
 
10759
10925
  function getExportFormats() {
10760
- 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);
10761
10936
  }
10762
10937
 
10763
10938
  function initFormatMenu() {
@@ -10835,10 +11010,17 @@
10835
11010
 
10836
11011
  var openMenu;
10837
11012
  var openMenuId;
11013
+ // Prefixes shown to the left of a menu item. The checkmark is what a copyable
11014
+ // item shows after it has been copied.
11015
+ var BULLET = '• &nbsp;';
11016
+ var CHECKMARK = '✓ &nbsp;';
11017
+ var COPIED_DELAY = 1200;
10838
11018
 
10839
11019
  document.addEventListener('mousedown', function(e) {
10840
- if (e.target.classList.contains('contextmenu-item')) {
10841
- return; // don't close menu if clicking on a menu link
11020
+ // Clicks inside the menu are handled by the menu itself: an item that runs a
11021
+ // command closes the menu, one that copies a value leaves it open.
11022
+ if (e.target.closest?.('.contextmenu')) {
11023
+ return;
10842
11024
  }
10843
11025
  closeOpenMenu();
10844
11026
  });
@@ -10892,20 +11074,47 @@
10892
11074
  }, 200);
10893
11075
  }
10894
11076
 
10895
- function addMenuItem(label, func, prefixArg) {
10896
- var prefix = prefixArg === undefined ? '• &nbsp;' : prefixArg;
10897
- var item = El('div')
11077
+ function createMenuItem(label, prefixArg) {
11078
+ var prefix = prefixArg === undefined ? BULLET : prefixArg;
11079
+ return El('div')
10898
11080
  .appendTo(menu)
10899
11081
  .addClass('contextmenu-item')
10900
11082
  .html(prefix + label)
10901
11083
  .show();
11084
+ }
10902
11085
 
11086
+ function addMenuItem(label, func, prefixArg) {
11087
+ var item = createMenuItem(label, prefixArg);
10903
11088
  GUI.onClick(item, function(e) {
10904
11089
  func();
10905
11090
  closeOpenMenu();
10906
11091
  });
10907
11092
  }
10908
11093
 
11094
+ // An item that copies a value to the clipboard. Unlike a command item, it
11095
+ // leaves the menu open, so that several values from the same click point can
11096
+ // be copied in turn. The menu closing is therefore no longer the sign that the
11097
+ // copy happened, so the item's prefix briefly becomes a checkmark instead.
11098
+ // content: the string to copy, or a function returning it
11099
+ function addCopyItem(label, content, prefixArg) {
11100
+ var prefix = prefixArg === undefined ? BULLET : prefixArg;
11101
+ var item = createMenuItem(label, prefix);
11102
+ var timeout;
11103
+
11104
+ GUI.onClick(item, function() {
11105
+ var str = typeof content == 'function' ? content() : content;
11106
+ saveFileContentToClipboard(str).then(showCopied);
11107
+ });
11108
+
11109
+ function showCopied() {
11110
+ item.html(CHECKMARK + label);
11111
+ clearTimeout(timeout);
11112
+ timeout = setTimeout(function() {
11113
+ item.html(prefix + label);
11114
+ }, COPIED_DELAY);
11115
+ }
11116
+ }
11117
+
10909
11118
  function addMenuLabel(label) {
10910
11119
  El('div')
10911
11120
  .appendTo(menu)
@@ -10948,7 +11157,7 @@
10948
11157
  addMenuItem('delete point', e.deletePoint);
10949
11158
  }
10950
11159
  if (e.ids?.length) {
10951
- addMenuItem('copy as GeoJSON', copyGeoJSON);
11160
+ addCopyItem('copy as GeoJSON', getSelectionGeoJSON);
10952
11161
  }
10953
11162
  if (e.deleteFeature) {
10954
11163
  addMenuItem(getDeleteLabel(), e.deleteFeature);
@@ -10963,6 +11172,9 @@
10963
11172
  addMenuLabel('x, y');
10964
11173
  addCoords(e.projected_coordinates);
10965
11174
  }
11175
+ if (e.raster_pixel) {
11176
+ addRasterPixel(e.raster_pixel);
11177
+ }
10966
11178
  }
10967
11179
 
10968
11180
  if (menu.node().childNodes.length === 0) {
@@ -11009,16 +11221,32 @@
11009
11221
  return 'delete ' + (lyr.geometry_type == 'point' ? 'point' : 'shape');
11010
11222
  }
11011
11223
 
11224
+ // info: pixel data from internal.getRasterPixelAtMapXY()
11225
+ function addRasterPixel(info) {
11226
+ var values = info.values.map(function(val) {
11227
+ return internal.formatRasterSampleValue(val, info.isFloat);
11228
+ });
11229
+ var valueStr = values.join(', ');
11230
+ addMenuLabel(getBandLabel(info.values.length) +
11231
+ (info.valid ? '' : ' (no data)'));
11232
+ addCopyItem(valueStr, valueStr);
11233
+ if (info.color) {
11234
+ addColor(getColorString(info.color));
11235
+ }
11236
+ }
11237
+
11238
+ function addColor(color) {
11239
+ addMenuLabel('color');
11240
+ addCopyItem(color, color, getSwatchHtml(color));
11241
+ }
11242
+
11012
11243
  function addCoords(p) {
11013
11244
  var coordStr = p[0] + ',' + p[1];
11014
- // var displayStr = '• &nbsp;' + coordStr.replace(/-/g, '–').replace(',', ', ');
11015
11245
  var displayStr = coordStr.replace(/-/g, '–').replace(',', ', ');
11016
- addMenuItem(displayStr, function() {
11017
- saveFileContentToClipboard(coordStr);
11018
- });
11246
+ addCopyItem(displayStr, coordStr);
11019
11247
  }
11020
11248
 
11021
- function copyGeoJSON() {
11249
+ function getSelectionGeoJSON() {
11022
11250
  var opts = {
11023
11251
  no_replace: true,
11024
11252
  ids: e.ids,
@@ -11032,13 +11260,32 @@
11032
11260
  layer.geometry_type = 'polyline';
11033
11261
  }
11034
11262
  var features = internal.exportLayerAsGeoJSON(layer, dataset, {rfc7946: true, prettify: true}, true, 'string');
11035
- var str = internal.geojson.formatCollection({"type": "FeatureCollection"}, features);
11036
- saveFileContentToClipboard(str);
11263
+ return internal.geojson.formatCollection({"type": "FeatureCollection"}, features);
11037
11264
  }
11038
11265
  };
11039
11266
  }
11040
11267
 
11041
11268
 
11269
+ // rgba: [r, g, b, a] channels in the 0-255 range
11270
+ function getColorString(rgba) {
11271
+ return internal.formatColor({
11272
+ r: rgba[0], g: rgba[1], b: rgba[2], a: rgba[3] / 255
11273
+ });
11274
+ }
11275
+
11276
+ function getBandLabel(bands) {
11277
+ if (bands == 1) return 'band value';
11278
+ if (bands == 3) return 'red, green, blue';
11279
+ if (bands == 4) return 'red, green, blue, alpha';
11280
+ return 'band values';
11281
+ }
11282
+
11283
+ // A color tile, used in place of the bullet that other menu items get.
11284
+ function getSwatchHtml(color) {
11285
+ return '<span class="contextmenu-swatch" style="background-color: ' +
11286
+ color + '"></span>';
11287
+ }
11288
+
11042
11289
  function layerHasOpenPaths(layer, arcs) {
11043
11290
  var retn = false;
11044
11291
  internal.editShapes(layer.shapes, function(part) {
@@ -12462,7 +12709,6 @@
12462
12709
  if (!preserveOnModeChange) {
12463
12710
  editSession.noteEdit();
12464
12711
  }
12465
- trimHistory(opts);
12466
12712
  fireHistoryChange();
12467
12713
  }
12468
12714
 
@@ -12505,14 +12751,6 @@
12505
12751
  fireHistoryChange();
12506
12752
  }
12507
12753
 
12508
- function trimHistory(opts) {
12509
- var max = opts && opts.maxStates;
12510
- var overflow;
12511
- if (!(max > 0) || history.length <= max) return;
12512
- overflow = history.length - max;
12513
- disposeHistoryItems(history.splice(0, overflow));
12514
- }
12515
-
12516
12754
  function fireHistoryChange() {
12517
12755
  gui.dispatchEvent('history_change', {
12518
12756
  canUndo: history.length - offset > 0,
@@ -12613,8 +12851,7 @@
12613
12851
  if (!wasChanged) return;
12614
12852
  getStoredUndoHistory(gui).addTransaction(finishedTx, {
12615
12853
  flags: {select: true},
12616
- entryPrefix: 'edit-session',
12617
- maxStates: getEditSessionUndoHistoryLimit()
12854
+ entryPrefix: 'edit-session'
12618
12855
  }).catch(function(e) {
12619
12856
  console.error(e);
12620
12857
  });
@@ -12661,11 +12898,6 @@
12661
12898
  return internal.UndoTransaction && (internal.UndoTransaction.UndoTransaction || internal.UndoTransaction);
12662
12899
  }
12663
12900
 
12664
- function getEditSessionUndoHistoryLimit() {
12665
- var opt = gui.options && gui.options.undoHistoryLimit;
12666
- return opt > 0 ? opt : 10;
12667
- }
12668
-
12669
12901
  }
12670
12902
 
12671
12903
  function SidebarButtons(gui) {
@@ -17332,6 +17564,11 @@
17332
17564
  // data coordinates
17333
17565
  eventData.projected_coordinates = gui.map.pixelCoordsToProjectedCoords(evt.x, evt.y);
17334
17566
  eventData.lonlat_coordinates = gui.map.pixelCoordsToLngLatCoords(evt.x, evt.y);
17567
+ if (type == 'contextmenu') {
17568
+ // Only the context menu reads pixel values, and sampling one can cost a
17569
+ // scan of the grid, so it is not done on every hover event.
17570
+ eventData.raster_pixel = gui.map.pixelCoordsToRasterPixel(evt.x, evt.y);
17571
+ }
17335
17572
  eventData.originalEvent = evt;
17336
17573
  eventData.overMap = isOverMap(evt);
17337
17574
  utils$1.defaults(eventData, evt.data);
@@ -20747,6 +20984,31 @@
20747
20984
 
20748
20985
  utils$1.inherit(MapExtent, EventDispatcher);
20749
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
+
20750
21012
  var MAX_VIEWPORT_PREVIEW_PIXELS = 6e6;
20751
21013
  var cache$1 = new WeakMap();
20752
21014
  var requestId$1 = 0;
@@ -20773,6 +21035,7 @@
20773
21035
  stats = getCachedRasterScalingStats(params, timing);
20774
21036
  timing.renderStart = getTimer$1();
20775
21037
  preview = internal.renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
21038
+ if (preview) preview.sourcePixels = params.sourcePixels;
20776
21039
  timing.renderMs = getTimer$1() - timing.renderStart;
20777
21040
  logRasterPreviewTiming(params, timing);
20778
21041
  current = cache$1.get(layer);
@@ -20782,10 +21045,6 @@
20782
21045
  }, 0);
20783
21046
  }
20784
21047
 
20785
- function invalidateRasterViewportPreview(layer) {
20786
- cache$1.delete(layer);
20787
- }
20788
-
20789
21048
  function getRasterViewportPreviewParams(layer, ext) {
20790
21049
  var raster = layer && layer.raster;
20791
21050
  var grid = internal.getRasterGrid(raster);
@@ -20795,7 +21054,7 @@
20795
21054
  var preview = internal.getRasterPreview(raster);
20796
21055
  var recipe, pixelRatio, t, p1, p2, displayWidth, displayHeight, crop, width, height, scale, key, needed;
20797
21056
  if (!grid || !grid.samples || !visibleBbox || !grid.bbox) return null;
20798
- if (!supportsNorthUpRaster(grid)) return null;
21057
+ if (internal.rasterGridIsRotated(grid)) return null;
20799
21058
  crop = getRasterSourceWindow(grid, visibleBbox);
20800
21059
  if (!crop) return null;
20801
21060
  visibleBbox = crop.bbox;
@@ -20828,6 +21087,7 @@
20828
21087
  return {
20829
21088
  key: key,
20830
21089
  needed: needed,
21090
+ sourcePixels: previewHasSourcePixels(width, height, crop.width, crop.height),
20831
21091
  raster: raster,
20832
21092
  grid: grid,
20833
21093
  recipe: recipe,
@@ -20901,11 +21161,6 @@
20901
21161
  return Math.round(ms * 10) / 10 + 'ms';
20902
21162
  }
20903
21163
 
20904
- function supportsNorthUpRaster(grid) {
20905
- var t = grid.transform;
20906
- return !t || t[1] === 0 && t[3] === 0;
20907
- }
20908
-
20909
21164
  function getRasterSourcePixelSize(grid, bbox) {
20910
21165
  var crop = getRasterSourceWindow(grid, bbox);
20911
21166
  return crop ? {width: crop.width, height: crop.height} : {width: 0, height: 0};
@@ -20991,6 +21246,7 @@
20991
21246
  }
20992
21247
  timing.renderStart = getTimer();
20993
21248
  preview = internal.renderRasterPreview(grid, params.recipe, grid.width, grid.height, params.stats);
21249
+ preview.sourcePixels = params.sourcePixels;
20994
21250
  applyCoverageMask(preview, grid.coverage);
20995
21251
  timing.renderMs = getTimer() - timing.renderStart;
20996
21252
  logRasterReprojectionTiming(params, timing);
@@ -21014,12 +21270,13 @@
21014
21270
  var viewBbox = ext && ext.getBounds().toArray();
21015
21271
  var bbox = rasterBbox && viewBbox && internal.intersectBboxes(rasterBbox, viewBbox);
21016
21272
  var size = bbox && getPreviewSize(ext, sourceGrid);
21017
- var recipe, stats;
21273
+ var recipe, stats, sourcePixels;
21018
21274
  if (!sourceCRS || !displayCRS || !sourceGrid || !sourceGrid.samples || !bbox || !size) return null;
21019
21275
  recipe = internal.getRasterViewRecipe(sourceGrid, raster.view && raster.view.recipe);
21020
21276
  stats = internal.getRasterViewScalingStats(raster, recipe);
21277
+ sourcePixels = outputHasSourcePixels(sourceGrid, bbox, sourceCRS, displayCRS, size);
21021
21278
  return {
21022
- key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe),
21279
+ key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe, sourcePixels),
21023
21280
  grid: sourceGrid,
21024
21281
  sourceCRS: sourceCRS,
21025
21282
  displayCRS: displayCRS,
@@ -21028,11 +21285,49 @@
21028
21285
  height: size.height,
21029
21286
  recipe: recipe,
21030
21287
  stats: stats,
21031
- sampleMethod: getRasterReprojectionSampleMethod(),
21288
+ sourcePixels: sourcePixels,
21289
+ sampleMethod: getRasterReprojectionSampleMethod(sourcePixels),
21032
21290
  meshInterval: 32
21033
21291
  };
21034
21292
  }
21035
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
+
21036
21331
  function getPreviewSize(ext, grid) {
21037
21332
  var pixelRatio = GUI.getPixelRatio();
21038
21333
  var t = ext.getTransform(pixelRatio);
@@ -21047,10 +21342,11 @@
21047
21342
  return {width: width, height: height};
21048
21343
  }
21049
21344
 
21050
- function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe) {
21345
+ function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe, sourcePixels) {
21051
21346
  return [
21052
21347
  size.width,
21053
21348
  size.height,
21349
+ sourcePixels,
21054
21350
  bbox.join(','),
21055
21351
  internal.crsToProj4(sourceCRS),
21056
21352
  internal.crsToProj4(displayCRS),
@@ -21058,7 +21354,7 @@
21058
21354
  recipe.scaling,
21059
21355
  recipe.scaleRange && recipe.scaleRange.join(','),
21060
21356
  recipe.percentileRange && recipe.percentileRange.join(','),
21061
- getRasterReprojectionSampleMethod(),
21357
+ getRasterReprojectionSampleMethod(sourcePixels),
21062
21358
  layer.raster && layer.raster.grid && layer.raster.grid.samples && layer.raster.grid.samples.length
21063
21359
  ].join('|');
21064
21360
  }
@@ -21086,10 +21382,13 @@
21086
21382
  }
21087
21383
  }
21088
21384
 
21089
- function getRasterReprojectionSampleMethod() {
21385
+ function getRasterReprojectionSampleMethod(sourcePixels) {
21090
21386
  var vars = GUI.getUrlVars();
21091
21387
  var val = vars['raster-bilinear'] ?? vars.raster_bilinear;
21092
- 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';
21093
21392
  }
21094
21393
 
21095
21394
  function logRasterReprojectionTiming(params, timing) {
@@ -21378,7 +21677,7 @@
21378
21677
  var raster = layer.raster;
21379
21678
  var options = opts || {};
21380
21679
  var preview = null;
21381
- var bbox;
21680
+ var bbox, grid, sourcePixels;
21382
21681
  if (layer.gui && layer.gui.dynamic_crs) {
21383
21682
  preview = getCachedRasterReprojectedPreview(layer, _ext);
21384
21683
  if (!preview) {
@@ -21389,7 +21688,7 @@
21389
21688
  }
21390
21689
  bbox = preview.bbox;
21391
21690
  if (!preview.pixels || !bbox) return;
21392
- drawRasterPreview(preview, bbox);
21691
+ drawRasterPreview(preview, bbox, preview.sourcePixels);
21393
21692
  if (options.action != 'nav' && options.onViewportPreviewReady) {
21394
21693
  scheduleRasterReprojectedPreview(layer, _ext, options.onViewportPreviewReady);
21395
21694
  }
@@ -21397,15 +21696,21 @@
21397
21696
  }
21398
21697
  preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
21399
21698
  bbox = preview && preview.bbox;
21699
+ sourcePixels = preview && preview.sourcePixels;
21400
21700
  if (!preview) {
21701
+ // The whole-raster preview made at import time, which is scaled down if
21702
+ // the raster is large.
21401
21703
  preview = raster && internal.getRasterPreview(raster);
21402
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);
21403
21708
  if (options.action != 'nav' && options.onViewportPreviewReady) {
21404
21709
  scheduleRasterViewportPreview(layer, _ext, options.onViewportPreviewReady);
21405
21710
  }
21406
21711
  }
21407
21712
  if (!preview || !preview.pixels || !bbox) return;
21408
- drawRasterPreview(preview, bbox);
21713
+ drawRasterPreview(preview, bbox, sourcePixels);
21409
21714
  };
21410
21715
 
21411
21716
  /*
@@ -21607,12 +21912,16 @@
21607
21912
  }
21608
21913
  }
21609
21914
 
21610
- 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) {
21611
21919
  var img = getRasterCanvas(preview);
21612
21920
  var t = _ext.getTransform(GUI.getPixelRatio());
21613
21921
  var p1 = t.transform(bbox[0], bbox[3]);
21614
21922
  var p2 = t.transform(bbox[2], bbox[1]);
21615
- _ctx.imageSmoothingEnabled = true;
21923
+ var magnification = Math.abs(p2[0] - p1[0]) / img.width / GUI.getPixelRatio();
21924
+ _ctx.imageSmoothingEnabled = rasterPreviewIsSmoothed(sourcePixels, magnification);
21616
21925
  _ctx.drawImage(img, p1[0], p1[1], p2[0] - p1[0], p2[1] - p1[1]);
21617
21926
  }
21618
21927
 
@@ -23521,6 +23830,17 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
23521
23830
  return p1 && p2 ? formatCoordsForDisplay(p1, p2) : null;
23522
23831
  };
23523
23832
 
23833
+ // Returns band values and display color of the raster pixel under a screen
23834
+ // location, or null if the active layer is not a raster or the point misses it.
23835
+ this.pixelCoordsToRasterPixel = function(x, y) {
23836
+ var p;
23837
+ if (!_activeLyr || !internal.layerHasRaster(_activeLyr)) return null;
23838
+ // The grid is georeferenced in the layer's own CRS, so a display point has
23839
+ // to be translated back when the layer is being reprojected for display.
23840
+ p = translateDisplayPoint(_activeLyr, _ext.pixCoordsToMapCoords(x, y));
23841
+ return p ? internal.getRasterPixelAtMapXY(_activeLyr.raster, p[0], p[1]) : null;
23842
+ };
23843
+
23524
23844
  this.getDisplayCRS = function() {
23525
23845
  if (!_activeLyr) {
23526
23846
  return _dynamicCRS || internal.parseCrsString('wgs84');