mapshaper 0.7.47 → 0.7.49

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.47",
3
+ "version": "0.7.49",
4
4
  "description": "A tool for editing geospatial data for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -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
  }
@@ -9336,7 +9493,9 @@
9336
9493
  this.runCommand = function(str) {
9337
9494
  str = str.trim();
9338
9495
  if (!str) return;
9339
- gui.toggleSidebarPanel('console');
9496
+ if (!gui.consoleIsOpen()) {
9497
+ gui.toggleSidebarPanel('console');
9498
+ }
9340
9499
  submit(str);
9341
9500
  };
9342
9501
 
@@ -9884,7 +10043,6 @@
9884
10043
  error: err,
9885
10044
  flags: flags,
9886
10045
  entryPrefix: 'command',
9887
- maxStates: getCommandUndoHistoryLimit(),
9888
10046
  onUndo: function() {
9889
10047
  gui.session.setCommandsActive(historyIds, false);
9890
10048
  },
@@ -9899,11 +10057,6 @@
9899
10057
  return appUndoIsEnabled(gui);
9900
10058
  }
9901
10059
 
9902
- function getCommandUndoHistoryLimit() {
9903
- var opt = gui.options && gui.options.undoHistoryLimit;
9904
- return opt > 0 ? opt : 10;
9905
- }
9906
-
9907
10060
  function logCommandTiming(commandString, commands, err, totalMillis, undoTiming) {
9908
10061
  var enabled = getQueryFlag('command-timing') == 'on' ||
9909
10062
  getQueryFlag('undo-timing') == 'on';
@@ -10835,10 +10988,17 @@
10835
10988
 
10836
10989
  var openMenu;
10837
10990
  var openMenuId;
10991
+ // Prefixes shown to the left of a menu item. The checkmark is what a copyable
10992
+ // item shows after it has been copied.
10993
+ var BULLET = '• &nbsp;';
10994
+ var CHECKMARK = '✓ &nbsp;';
10995
+ var COPIED_DELAY = 1200;
10838
10996
 
10839
10997
  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
10998
+ // Clicks inside the menu are handled by the menu itself: an item that runs a
10999
+ // command closes the menu, one that copies a value leaves it open.
11000
+ if (e.target.closest?.('.contextmenu')) {
11001
+ return;
10842
11002
  }
10843
11003
  closeOpenMenu();
10844
11004
  });
@@ -10892,20 +11052,47 @@
10892
11052
  }, 200);
10893
11053
  }
10894
11054
 
10895
- function addMenuItem(label, func, prefixArg) {
10896
- var prefix = prefixArg === undefined ? '• &nbsp;' : prefixArg;
10897
- var item = El('div')
11055
+ function createMenuItem(label, prefixArg) {
11056
+ var prefix = prefixArg === undefined ? BULLET : prefixArg;
11057
+ return El('div')
10898
11058
  .appendTo(menu)
10899
11059
  .addClass('contextmenu-item')
10900
11060
  .html(prefix + label)
10901
11061
  .show();
11062
+ }
10902
11063
 
11064
+ function addMenuItem(label, func, prefixArg) {
11065
+ var item = createMenuItem(label, prefixArg);
10903
11066
  GUI.onClick(item, function(e) {
10904
11067
  func();
10905
11068
  closeOpenMenu();
10906
11069
  });
10907
11070
  }
10908
11071
 
11072
+ // An item that copies a value to the clipboard. Unlike a command item, it
11073
+ // leaves the menu open, so that several values from the same click point can
11074
+ // be copied in turn. The menu closing is therefore no longer the sign that the
11075
+ // copy happened, so the item's prefix briefly becomes a checkmark instead.
11076
+ // content: the string to copy, or a function returning it
11077
+ function addCopyItem(label, content, prefixArg) {
11078
+ var prefix = prefixArg === undefined ? BULLET : prefixArg;
11079
+ var item = createMenuItem(label, prefix);
11080
+ var timeout;
11081
+
11082
+ GUI.onClick(item, function() {
11083
+ var str = typeof content == 'function' ? content() : content;
11084
+ saveFileContentToClipboard(str).then(showCopied);
11085
+ });
11086
+
11087
+ function showCopied() {
11088
+ item.html(CHECKMARK + label);
11089
+ clearTimeout(timeout);
11090
+ timeout = setTimeout(function() {
11091
+ item.html(prefix + label);
11092
+ }, COPIED_DELAY);
11093
+ }
11094
+ }
11095
+
10909
11096
  function addMenuLabel(label) {
10910
11097
  El('div')
10911
11098
  .appendTo(menu)
@@ -10948,7 +11135,7 @@
10948
11135
  addMenuItem('delete point', e.deletePoint);
10949
11136
  }
10950
11137
  if (e.ids?.length) {
10951
- addMenuItem('copy as GeoJSON', copyGeoJSON);
11138
+ addCopyItem('copy as GeoJSON', getSelectionGeoJSON);
10952
11139
  }
10953
11140
  if (e.deleteFeature) {
10954
11141
  addMenuItem(getDeleteLabel(), e.deleteFeature);
@@ -10963,6 +11150,9 @@
10963
11150
  addMenuLabel('x, y');
10964
11151
  addCoords(e.projected_coordinates);
10965
11152
  }
11153
+ if (e.raster_pixel) {
11154
+ addRasterPixel(e.raster_pixel);
11155
+ }
10966
11156
  }
10967
11157
 
10968
11158
  if (menu.node().childNodes.length === 0) {
@@ -11009,16 +11199,32 @@
11009
11199
  return 'delete ' + (lyr.geometry_type == 'point' ? 'point' : 'shape');
11010
11200
  }
11011
11201
 
11202
+ // info: pixel data from internal.getRasterPixelAtMapXY()
11203
+ function addRasterPixel(info) {
11204
+ var values = info.values.map(function(val) {
11205
+ return internal.formatRasterSampleValue(val, info.isFloat);
11206
+ });
11207
+ var valueStr = values.join(', ');
11208
+ addMenuLabel(getBandLabel(info.values.length) +
11209
+ (info.valid ? '' : ' (no data)'));
11210
+ addCopyItem(valueStr, valueStr);
11211
+ if (info.color) {
11212
+ addColor(getColorString(info.color));
11213
+ }
11214
+ }
11215
+
11216
+ function addColor(color) {
11217
+ addMenuLabel('color');
11218
+ addCopyItem(color, color, getSwatchHtml(color));
11219
+ }
11220
+
11012
11221
  function addCoords(p) {
11013
11222
  var coordStr = p[0] + ',' + p[1];
11014
- // var displayStr = '• &nbsp;' + coordStr.replace(/-/g, '–').replace(',', ', ');
11015
11223
  var displayStr = coordStr.replace(/-/g, '–').replace(',', ', ');
11016
- addMenuItem(displayStr, function() {
11017
- saveFileContentToClipboard(coordStr);
11018
- });
11224
+ addCopyItem(displayStr, coordStr);
11019
11225
  }
11020
11226
 
11021
- function copyGeoJSON() {
11227
+ function getSelectionGeoJSON() {
11022
11228
  var opts = {
11023
11229
  no_replace: true,
11024
11230
  ids: e.ids,
@@ -11032,13 +11238,32 @@
11032
11238
  layer.geometry_type = 'polyline';
11033
11239
  }
11034
11240
  var features = internal.exportLayerAsGeoJSON(layer, dataset, {rfc7946: true, prettify: true}, true, 'string');
11035
- var str = internal.geojson.formatCollection({"type": "FeatureCollection"}, features);
11036
- saveFileContentToClipboard(str);
11241
+ return internal.geojson.formatCollection({"type": "FeatureCollection"}, features);
11037
11242
  }
11038
11243
  };
11039
11244
  }
11040
11245
 
11041
11246
 
11247
+ // rgba: [r, g, b, a] channels in the 0-255 range
11248
+ function getColorString(rgba) {
11249
+ return internal.formatColor({
11250
+ r: rgba[0], g: rgba[1], b: rgba[2], a: rgba[3] / 255
11251
+ });
11252
+ }
11253
+
11254
+ function getBandLabel(bands) {
11255
+ if (bands == 1) return 'band value';
11256
+ if (bands == 3) return 'red, green, blue';
11257
+ if (bands == 4) return 'red, green, blue, alpha';
11258
+ return 'band values';
11259
+ }
11260
+
11261
+ // A color tile, used in place of the bullet that other menu items get.
11262
+ function getSwatchHtml(color) {
11263
+ return '<span class="contextmenu-swatch" style="background-color: ' +
11264
+ color + '"></span>';
11265
+ }
11266
+
11042
11267
  function layerHasOpenPaths(layer, arcs) {
11043
11268
  var retn = false;
11044
11269
  internal.editShapes(layer.shapes, function(part) {
@@ -12462,7 +12687,6 @@
12462
12687
  if (!preserveOnModeChange) {
12463
12688
  editSession.noteEdit();
12464
12689
  }
12465
- trimHistory(opts);
12466
12690
  fireHistoryChange();
12467
12691
  }
12468
12692
 
@@ -12505,14 +12729,6 @@
12505
12729
  fireHistoryChange();
12506
12730
  }
12507
12731
 
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
12732
  function fireHistoryChange() {
12517
12733
  gui.dispatchEvent('history_change', {
12518
12734
  canUndo: history.length - offset > 0,
@@ -12613,8 +12829,7 @@
12613
12829
  if (!wasChanged) return;
12614
12830
  getStoredUndoHistory(gui).addTransaction(finishedTx, {
12615
12831
  flags: {select: true},
12616
- entryPrefix: 'edit-session',
12617
- maxStates: getEditSessionUndoHistoryLimit()
12832
+ entryPrefix: 'edit-session'
12618
12833
  }).catch(function(e) {
12619
12834
  console.error(e);
12620
12835
  });
@@ -12661,11 +12876,6 @@
12661
12876
  return internal.UndoTransaction && (internal.UndoTransaction.UndoTransaction || internal.UndoTransaction);
12662
12877
  }
12663
12878
 
12664
- function getEditSessionUndoHistoryLimit() {
12665
- var opt = gui.options && gui.options.undoHistoryLimit;
12666
- return opt > 0 ? opt : 10;
12667
- }
12668
-
12669
12879
  }
12670
12880
 
12671
12881
  function SidebarButtons(gui) {
@@ -17332,6 +17542,11 @@
17332
17542
  // data coordinates
17333
17543
  eventData.projected_coordinates = gui.map.pixelCoordsToProjectedCoords(evt.x, evt.y);
17334
17544
  eventData.lonlat_coordinates = gui.map.pixelCoordsToLngLatCoords(evt.x, evt.y);
17545
+ if (type == 'contextmenu') {
17546
+ // Only the context menu reads pixel values, and sampling one can cost a
17547
+ // scan of the grid, so it is not done on every hover event.
17548
+ eventData.raster_pixel = gui.map.pixelCoordsToRasterPixel(evt.x, evt.y);
17549
+ }
17335
17550
  eventData.originalEvent = evt;
17336
17551
  eventData.overMap = isOverMap(evt);
17337
17552
  utils$1.defaults(eventData, evt.data);
@@ -20782,10 +20997,6 @@
20782
20997
  }, 0);
20783
20998
  }
20784
20999
 
20785
- function invalidateRasterViewportPreview(layer) {
20786
- cache$1.delete(layer);
20787
- }
20788
-
20789
21000
  function getRasterViewportPreviewParams(layer, ext) {
20790
21001
  var raster = layer && layer.raster;
20791
21002
  var grid = internal.getRasterGrid(raster);
@@ -20795,7 +21006,7 @@
20795
21006
  var preview = internal.getRasterPreview(raster);
20796
21007
  var recipe, pixelRatio, t, p1, p2, displayWidth, displayHeight, crop, width, height, scale, key, needed;
20797
21008
  if (!grid || !grid.samples || !visibleBbox || !grid.bbox) return null;
20798
- if (!supportsNorthUpRaster(grid)) return null;
21009
+ if (internal.rasterGridIsRotated(grid)) return null;
20799
21010
  crop = getRasterSourceWindow(grid, visibleBbox);
20800
21011
  if (!crop) return null;
20801
21012
  visibleBbox = crop.bbox;
@@ -20901,11 +21112,6 @@
20901
21112
  return Math.round(ms * 10) / 10 + 'ms';
20902
21113
  }
20903
21114
 
20904
- function supportsNorthUpRaster(grid) {
20905
- var t = grid.transform;
20906
- return !t || t[1] === 0 && t[3] === 0;
20907
- }
20908
-
20909
21115
  function getRasterSourcePixelSize(grid, bbox) {
20910
21116
  var crop = getRasterSourceWindow(grid, bbox);
20911
21117
  return crop ? {width: crop.width, height: crop.height} : {width: 0, height: 0};
@@ -23521,6 +23727,17 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
23521
23727
  return p1 && p2 ? formatCoordsForDisplay(p1, p2) : null;
23522
23728
  };
23523
23729
 
23730
+ // Returns band values and display color of the raster pixel under a screen
23731
+ // location, or null if the active layer is not a raster or the point misses it.
23732
+ this.pixelCoordsToRasterPixel = function(x, y) {
23733
+ var p;
23734
+ if (!_activeLyr || !internal.layerHasRaster(_activeLyr)) return null;
23735
+ // The grid is georeferenced in the layer's own CRS, so a display point has
23736
+ // to be translated back when the layer is being reprojected for display.
23737
+ p = translateDisplayPoint(_activeLyr, _ext.pixCoordsToMapCoords(x, y));
23738
+ return p ? internal.getRasterPixelAtMapXY(_activeLyr.raster, p[0], p[1]) : null;
23739
+ };
23740
+
23524
23741
  this.getDisplayCRS = function() {
23525
23742
  if (!_activeLyr) {
23526
23743
  return _dynamicCRS || internal.parseCrsString('wgs84');