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.
- package/mapshaper.js +3244 -865
- package/package.json +2 -2
- package/www/mapshaper-gui.js +403 -83
- package/www/mapshaper.js +3244 -865
- package/www/modules.js +122 -10
- package/www/page.css +11 -0
package/www/mapshaper-gui.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
6776
|
-
// stored-undo-history.addTransaction()
|
|
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
|
-
|
|
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
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
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
|
-
//
|
|
7279
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 = '• ';
|
|
11016
|
+
var CHECKMARK = '✓ ';
|
|
11017
|
+
var COPIED_DELAY = 1200;
|
|
10838
11018
|
|
|
10839
11019
|
document.addEventListener('mousedown', function(e) {
|
|
10840
|
-
|
|
10841
|
-
|
|
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
|
|
10896
|
-
var prefix = prefixArg === undefined ?
|
|
10897
|
-
|
|
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
|
-
|
|
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 = '• ' + coordStr.replace(/-/g, '–').replace(',', ', ');
|
|
11015
11245
|
var displayStr = coordStr.replace(/-/g, '–').replace(',', ', ');
|
|
11016
|
-
|
|
11017
|
-
saveFileContentToClipboard(coordStr);
|
|
11018
|
-
});
|
|
11246
|
+
addCopyItem(displayStr, coordStr);
|
|
11019
11247
|
}
|
|
11020
11248
|
|
|
11021
|
-
function
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|