@developmentseed/deck.gl-geotiff 0.5.0-beta.1 → 0.6.0-alpha.1
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/dist/cog-layer.d.ts +3 -4
- package/dist/cog-layer.d.ts.map +1 -1
- package/dist/cog-layer.js +84 -115
- package/dist/cog-layer.js.map +1 -1
- package/dist/geotiff/render-pipeline.d.ts.map +1 -1
- package/dist/geotiff/render-pipeline.js +3 -54
- package/dist/geotiff/render-pipeline.js.map +1 -1
- package/dist/geotiff-layer.d.ts +2 -3
- package/dist/geotiff-layer.d.ts.map +1 -1
- package/dist/geotiff-layer.js +2 -3
- package/dist/geotiff-layer.js.map +1 -1
- package/dist/geotiff-reprojection.d.ts +1 -1
- package/dist/geotiff-reprojection.d.ts.map +1 -1
- package/dist/geotiff-reprojection.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/multi-cog-layer.d.ts +301 -0
- package/dist/multi-cog-layer.d.ts.map +1 -0
- package/dist/multi-cog-layer.js +649 -0
- package/dist/multi-cog-layer.js.map +1 -0
- package/package.json +15 -14
- package/dist/proj.d.ts +0 -24
- package/dist/proj.d.ts.map +0 -1
- package/dist/proj.js +0 -72
- package/dist/proj.js.map +0 -1
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { COORDINATE_SYSTEM, CompositeLayer } from "@deck.gl/core";
|
|
2
|
+
import { TileLayer } from "@deck.gl/geo-layers";
|
|
3
|
+
import { PathLayer, TextLayer } from "@deck.gl/layers";
|
|
4
|
+
import { createMultiTilesetDescriptor, RasterLayer, RasterTileset2D, resolveSecondaryTiles, selectSecondaryLevel, TileMatrixSetAdaptor, tilesetLevelsEqual, } from "@developmentseed/deck.gl-raster";
|
|
5
|
+
import { buildCompositeBandsProps, CompositeBands, } from "@developmentseed/deck.gl-raster/gpu-modules";
|
|
6
|
+
import { assembleTiles, defaultDecoderPool, generateTileMatrixSet, } from "@developmentseed/geotiff";
|
|
7
|
+
import { tileTransform } from "@developmentseed/morecantile";
|
|
8
|
+
import { epsgResolver as defaultEpsgResolver, makeClampedForwardTo3857, parseWkt, } from "@developmentseed/proj";
|
|
9
|
+
import proj4 from "proj4";
|
|
10
|
+
import { fetchGeoTIFF, getGeographicBounds } from "./geotiff/geotiff.js";
|
|
11
|
+
import { fromAffine } from "./geotiff-reprojection.js";
|
|
12
|
+
/** Size of deck.gl's common coordinate space in world units. */
|
|
13
|
+
const TILE_SIZE = 512;
|
|
14
|
+
/** The size of the globe in web mercator meters. */
|
|
15
|
+
const WEB_MERCATOR_METER_CIRCUMFERENCE = 40075016.686;
|
|
16
|
+
/**
|
|
17
|
+
* Scale factor for converting EPSG:3857 meters into deck.gl world units
|
|
18
|
+
* (512x512).
|
|
19
|
+
*/
|
|
20
|
+
const WEB_MERCATOR_TO_WORLD_SCALE = TILE_SIZE / WEB_MERCATOR_METER_CIRCUMFERENCE;
|
|
21
|
+
/**
|
|
22
|
+
* Color palette for debug overlays.
|
|
23
|
+
*
|
|
24
|
+
* Index 0 is the primary tileset (red outline, white text).
|
|
25
|
+
* Indices 1+ cycle through distinct colors for secondary tilesets.
|
|
26
|
+
*/
|
|
27
|
+
const DEBUG_COLORS = [
|
|
28
|
+
{ outline: [255, 0, 0, 255], text: [255, 255, 255, 255] }, // primary: red outline, white text
|
|
29
|
+
{ outline: [0, 255, 255, 255], text: [0, 255, 255, 255] }, // cyan
|
|
30
|
+
{ outline: [255, 255, 0, 255], text: [255, 255, 0, 255] }, // yellow
|
|
31
|
+
{ outline: [255, 0, 255, 255], text: [255, 0, 255, 255] }, // magenta
|
|
32
|
+
{ outline: [0, 255, 128, 255], text: [0, 255, 128, 255] }, // lime
|
|
33
|
+
];
|
|
34
|
+
const defaultProps = {
|
|
35
|
+
epsgResolver: { type: "accessor", value: defaultEpsgResolver },
|
|
36
|
+
maxError: { type: "number", value: 0.125 },
|
|
37
|
+
debug: { type: "boolean", value: false },
|
|
38
|
+
debugOpacity: { type: "number", value: 0.5 },
|
|
39
|
+
debugLevel: { type: "number", value: 1 },
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A deck.gl {@link CompositeLayer} that opens multiple Cloud-Optimized GeoTIFFs
|
|
43
|
+
* (COGs) in parallel, builds a {@link TilesetDescriptor} for each, and groups
|
|
44
|
+
* them into a single {@link MultiTilesetDescriptor}.
|
|
45
|
+
*
|
|
46
|
+
* The finest-resolution source is automatically selected as the primary
|
|
47
|
+
* tileset, which drives the tile grid. Secondary sources are sampled at the
|
|
48
|
+
* closest matching resolution.
|
|
49
|
+
*
|
|
50
|
+
* @see {@link MultiCOGLayerProps} for accepted props.
|
|
51
|
+
* @see {@link createMultiTilesetDescriptor} for the grouping logic.
|
|
52
|
+
* @see {@link TileMatrixSetAdaptor} for the per-source tileset adapter.
|
|
53
|
+
*/
|
|
54
|
+
export class MultiCOGLayer extends CompositeLayer {
|
|
55
|
+
static layerName = "MultiCOGLayer";
|
|
56
|
+
static defaultProps = defaultProps;
|
|
57
|
+
initializeState() {
|
|
58
|
+
this.setState({
|
|
59
|
+
sources: null,
|
|
60
|
+
multiDescriptor: null,
|
|
61
|
+
forwardTo4326: null,
|
|
62
|
+
inverseFrom4326: null,
|
|
63
|
+
forwardTo3857: null,
|
|
64
|
+
inverseFrom3857: null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
updateState({ changeFlags, props, oldProps, }) {
|
|
68
|
+
if (changeFlags.dataChanged || props.sources !== oldProps.sources) {
|
|
69
|
+
// Reset state so renderLayers() returns null while we re-open COGs.
|
|
70
|
+
// Without this, the TileLayer renders with new props but stale state,
|
|
71
|
+
// caching tiles with the wrong bands.
|
|
72
|
+
this.setState({
|
|
73
|
+
sources: null,
|
|
74
|
+
multiDescriptor: null,
|
|
75
|
+
});
|
|
76
|
+
this._parseAllSources();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Open all configured COG sources in parallel, compute shared projection
|
|
81
|
+
* functions, and build the {@link MultiTilesetDescriptor}.
|
|
82
|
+
*
|
|
83
|
+
* All sources are assumed to share the same CRS; the projection of the
|
|
84
|
+
* first source is used for the shared coordinate converters.
|
|
85
|
+
*
|
|
86
|
+
* @returns Resolves when all sources have been opened and state has been set.
|
|
87
|
+
*/
|
|
88
|
+
async _parseAllSources() {
|
|
89
|
+
const { sources } = this.props;
|
|
90
|
+
const entries = Object.entries(sources);
|
|
91
|
+
// Open all COGs in parallel
|
|
92
|
+
const cogSources = await Promise.all(entries.map(async ([name, config]) => {
|
|
93
|
+
const geotiff = await fetchGeoTIFF(config.url);
|
|
94
|
+
const crs = geotiff.crs;
|
|
95
|
+
const sourceProjection = typeof crs === "number"
|
|
96
|
+
? await this.props.epsgResolver(crs)
|
|
97
|
+
: parseWkt(crs);
|
|
98
|
+
const tms = generateTileMatrixSet(geotiff, sourceProjection);
|
|
99
|
+
return { name, geotiff, tms, sourceProjection };
|
|
100
|
+
}));
|
|
101
|
+
// Use the first source's projection for shared projection functions
|
|
102
|
+
// (all sources must share the same CRS)
|
|
103
|
+
const firstCogSource = cogSources[0];
|
|
104
|
+
const sourceProjection = firstCogSource.sourceProjection;
|
|
105
|
+
// @ts-expect-error - proj4 typings are incomplete and don't support
|
|
106
|
+
// wkt-parser input
|
|
107
|
+
const converter4326 = proj4(sourceProjection, "EPSG:4326");
|
|
108
|
+
const forwardTo4326 = (x, y) => converter4326.forward([x, y], false);
|
|
109
|
+
const inverseFrom4326 = (x, y) => converter4326.inverse([x, y], false);
|
|
110
|
+
// @ts-expect-error - proj4 typings are incomplete and don't support
|
|
111
|
+
// wkt-parser input
|
|
112
|
+
const converter3857 = proj4(sourceProjection, "EPSG:3857");
|
|
113
|
+
const forwardTo3857 = makeClampedForwardTo3857((x, y) => converter3857.forward([x, y], false), forwardTo4326);
|
|
114
|
+
const inverseFrom3857 = (x, y) => converter3857.inverse([x, y], false);
|
|
115
|
+
// Build TilesetDescriptors
|
|
116
|
+
const tilesetMap = new Map();
|
|
117
|
+
const sourceMap = new Map();
|
|
118
|
+
for (const cogSource of cogSources) {
|
|
119
|
+
const descriptor = new TileMatrixSetAdaptor(cogSource.tms, {
|
|
120
|
+
projectTo4326: forwardTo4326,
|
|
121
|
+
projectTo3857: forwardTo3857,
|
|
122
|
+
});
|
|
123
|
+
tilesetMap.set(cogSource.name, descriptor);
|
|
124
|
+
sourceMap.set(cogSource.name, {
|
|
125
|
+
geotiff: cogSource.geotiff,
|
|
126
|
+
tms: cogSource.tms,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const multiDescriptor = createMultiTilesetDescriptor(tilesetMap);
|
|
130
|
+
this.setState({
|
|
131
|
+
sources: sourceMap,
|
|
132
|
+
multiDescriptor,
|
|
133
|
+
forwardTo4326,
|
|
134
|
+
inverseFrom4326,
|
|
135
|
+
forwardTo3857,
|
|
136
|
+
inverseFrom3857,
|
|
137
|
+
});
|
|
138
|
+
if (this.props.onGeoTIFFLoad) {
|
|
139
|
+
const primaryKey = multiDescriptor.primaryKey;
|
|
140
|
+
const primaryGeotiff = sourceMap.get(primaryKey).geotiff;
|
|
141
|
+
const geographicBounds = getGeographicBounds(primaryGeotiff, converter4326);
|
|
142
|
+
const geotiffMap = new Map();
|
|
143
|
+
for (const [name, state] of sourceMap) {
|
|
144
|
+
geotiffMap.set(name, state.geotiff);
|
|
145
|
+
}
|
|
146
|
+
this.props.onGeoTIFFLoad(geotiffMap, { primaryKey, geographicBounds });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Fetch tile data for all configured sources at the given tile index.
|
|
151
|
+
*
|
|
152
|
+
* Primary-grid sources are fetched directly at (x, y, z). Secondary
|
|
153
|
+
* sources are resolved to covering tiles at the closest matching zoom
|
|
154
|
+
* level, fetched (potentially multiple tiles), stitched if necessary,
|
|
155
|
+
* and returned with the appropriate UV transform.
|
|
156
|
+
*
|
|
157
|
+
* @param tile - Tile load props from the TileLayer, containing index and signal.
|
|
158
|
+
* @returns Per-band textures, UV transforms, and reprojection functions.
|
|
159
|
+
*/
|
|
160
|
+
async _getTileData(tile) {
|
|
161
|
+
const { signal } = tile;
|
|
162
|
+
const { x, y, z } = tile.index;
|
|
163
|
+
const { multiDescriptor, sources } = this.state;
|
|
164
|
+
const pool = this.props.pool ?? defaultDecoderPool();
|
|
165
|
+
const device = this.context.device;
|
|
166
|
+
// Combine abort signals if both are defined
|
|
167
|
+
const combinedSignal = signal && this.props.signal
|
|
168
|
+
? AbortSignal.any([signal, this.props.signal])
|
|
169
|
+
: signal || this.props.signal;
|
|
170
|
+
// Compute reprojection transforms from the primary TMS
|
|
171
|
+
const primaryKey = multiDescriptor.primaryKey;
|
|
172
|
+
const primarySource = sources.get(primaryKey);
|
|
173
|
+
const primaryTms = primarySource.tms;
|
|
174
|
+
const tileMatrix = primaryTms.tileMatrices[z];
|
|
175
|
+
const tileAffine = tileTransform(tileMatrix, { col: x, row: y });
|
|
176
|
+
const { forwardTransform, inverseTransform } = fromAffine(tileAffine);
|
|
177
|
+
const primaryLevel = multiDescriptor.primary.levels[z];
|
|
178
|
+
// Collect fetch promises for all bands
|
|
179
|
+
const bandPromises = [];
|
|
180
|
+
for (const [name, sourceState] of sources) {
|
|
181
|
+
const descriptor = name === primaryKey
|
|
182
|
+
? multiDescriptor.primary
|
|
183
|
+
: multiDescriptor.secondaries.get(name);
|
|
184
|
+
const isPrimary = name === primaryKey ||
|
|
185
|
+
tilesetLevelsEqual(descriptor.levels[z] ?? descriptor.levels[0], primaryLevel);
|
|
186
|
+
if (isPrimary) {
|
|
187
|
+
// Primary-grid source: fetch tile directly with identity UV transform
|
|
188
|
+
bandPromises.push(this._fetchPrimaryBand(name, sourceState, {
|
|
189
|
+
x,
|
|
190
|
+
y,
|
|
191
|
+
z,
|
|
192
|
+
pool,
|
|
193
|
+
signal: combinedSignal,
|
|
194
|
+
device,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Secondary source: resolve covering tiles and fetch
|
|
199
|
+
bandPromises.push(this._fetchSecondaryBand(name, sourceState, {
|
|
200
|
+
descriptor,
|
|
201
|
+
primaryLevel,
|
|
202
|
+
primaryCol: x,
|
|
203
|
+
primaryRow: y,
|
|
204
|
+
primaryZ: z,
|
|
205
|
+
pool,
|
|
206
|
+
signal: combinedSignal,
|
|
207
|
+
device,
|
|
208
|
+
debug: this.props.debug ?? false,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const bandEntries = await Promise.all(bandPromises);
|
|
213
|
+
const bands = new Map(bandEntries.map(([name, data]) => [name, data]));
|
|
214
|
+
// Collect debug info from secondary bands
|
|
215
|
+
let debugInfo;
|
|
216
|
+
if (this.props.debug) {
|
|
217
|
+
const debugBands = new Map();
|
|
218
|
+
for (const [name, , bandDebug] of bandEntries) {
|
|
219
|
+
if (bandDebug) {
|
|
220
|
+
debugBands.set(name, bandDebug);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
debugInfo = { bands: debugBands };
|
|
224
|
+
}
|
|
225
|
+
const byteLength = [...bands.values()].reduce((sum, band) => sum + band.byteLength, 0);
|
|
226
|
+
console.log(`Tile (${x}, ${y}, ${z}): fetched bands [${[...bands.keys()].join(", ")}], total byte length: ${byteLength}`);
|
|
227
|
+
return {
|
|
228
|
+
bands,
|
|
229
|
+
forwardTransform,
|
|
230
|
+
inverseTransform,
|
|
231
|
+
width: primaryLevel.tileWidth,
|
|
232
|
+
height: primaryLevel.tileHeight,
|
|
233
|
+
byteLength,
|
|
234
|
+
debugInfo,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Fetch a single tile for a source that shares the primary tile grid.
|
|
239
|
+
*
|
|
240
|
+
* @returns A `[name, BandTileData, null]` tuple with identity UV transform
|
|
241
|
+
* and no debug info (primary bands don't need it).
|
|
242
|
+
*/
|
|
243
|
+
async _fetchPrimaryBand(name, sourceState, opts) {
|
|
244
|
+
const { x, y, z, pool, signal, device } = opts;
|
|
245
|
+
const image = selectImage(sourceState.geotiff, z);
|
|
246
|
+
const tile = await image.fetchTile(x, y, {
|
|
247
|
+
boundless: true,
|
|
248
|
+
pool,
|
|
249
|
+
signal,
|
|
250
|
+
});
|
|
251
|
+
const texture = createBandTexture(device, tile.array);
|
|
252
|
+
const arr = tile.array;
|
|
253
|
+
const byteLength = arr.layout === "pixel-interleaved"
|
|
254
|
+
? arr.data.byteLength
|
|
255
|
+
: arr.bands.reduce((sum, b) => sum + b.byteLength, 0);
|
|
256
|
+
return [
|
|
257
|
+
name,
|
|
258
|
+
{
|
|
259
|
+
texture,
|
|
260
|
+
uvTransform: [0, 0, 1, 1],
|
|
261
|
+
width: arr.width,
|
|
262
|
+
height: arr.height,
|
|
263
|
+
byteLength,
|
|
264
|
+
},
|
|
265
|
+
null,
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Fetch covering tiles for a secondary source and stitch them into a
|
|
270
|
+
* single texture using {@link assembleTiles}.
|
|
271
|
+
*
|
|
272
|
+
* @returns A `[name, BandTileData, BandDebugInfo | null]` tuple with the
|
|
273
|
+
* computed UV transform and optional debug metadata.
|
|
274
|
+
*/
|
|
275
|
+
async _fetchSecondaryBand(name, sourceState, opts) {
|
|
276
|
+
const { descriptor, primaryLevel, primaryCol, primaryRow, primaryZ, pool, signal, device, } = opts;
|
|
277
|
+
// Select the best secondary level
|
|
278
|
+
const primaryMpp = this.state.multiDescriptor.primary.levels[primaryZ].metersPerPixel;
|
|
279
|
+
const secondaryLevel = selectSecondaryLevel(descriptor.levels, primaryMpp);
|
|
280
|
+
const secondaryZ = descriptor.levels.indexOf(secondaryLevel);
|
|
281
|
+
// Resolve covering tile indices and UV transform
|
|
282
|
+
const resolution = resolveSecondaryTiles(primaryLevel, primaryCol, primaryRow, secondaryLevel, secondaryZ);
|
|
283
|
+
// Collect debug info if requested
|
|
284
|
+
let debugInfo = null;
|
|
285
|
+
if (opts.debug) {
|
|
286
|
+
const secondaryTileCorners = resolution.tileIndices.map((idx) => secondaryLevel.projectedTileCorners(idx.x, idx.y));
|
|
287
|
+
debugInfo = {
|
|
288
|
+
secondaryTileCorners,
|
|
289
|
+
secondaryZ,
|
|
290
|
+
uvTransform: resolution.uvTransform,
|
|
291
|
+
stitchedWidth: resolution.stitchedWidth,
|
|
292
|
+
stitchedHeight: resolution.stitchedHeight,
|
|
293
|
+
tileCount: resolution.tileIndices.length,
|
|
294
|
+
metersPerPixel: secondaryLevel.metersPerPixel,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Fetch all covering tiles via fetchTiles
|
|
298
|
+
const image = selectImage(sourceState.geotiff, secondaryZ);
|
|
299
|
+
const xy = resolution.tileIndices.map((idx) => [
|
|
300
|
+
idx.x,
|
|
301
|
+
idx.y,
|
|
302
|
+
]);
|
|
303
|
+
const tiles = await image.fetchTiles(xy, {
|
|
304
|
+
boundless: true,
|
|
305
|
+
pool,
|
|
306
|
+
signal,
|
|
307
|
+
});
|
|
308
|
+
// Assemble into a single RasterArray (handles stitching + typed array preservation)
|
|
309
|
+
const assembled = assembleTiles(tiles, {
|
|
310
|
+
width: resolution.stitchedWidth,
|
|
311
|
+
height: resolution.stitchedHeight,
|
|
312
|
+
tileWidth: secondaryLevel.tileWidth,
|
|
313
|
+
tileHeight: secondaryLevel.tileHeight,
|
|
314
|
+
minCol: resolution.minCol,
|
|
315
|
+
minRow: resolution.minRow,
|
|
316
|
+
});
|
|
317
|
+
const texture = createBandTexture(device, assembled);
|
|
318
|
+
const assembledByteLength = assembled.layout === "pixel-interleaved"
|
|
319
|
+
? assembled.data.byteLength
|
|
320
|
+
: assembled.bands.reduce((sum, b) => sum + b.byteLength, 0);
|
|
321
|
+
return [
|
|
322
|
+
name,
|
|
323
|
+
{
|
|
324
|
+
texture,
|
|
325
|
+
uvTransform: resolution.uvTransform,
|
|
326
|
+
width: assembled.width,
|
|
327
|
+
height: assembled.height,
|
|
328
|
+
byteLength: assembledByteLength,
|
|
329
|
+
},
|
|
330
|
+
debugInfo,
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Create sub-layers for a single loaded tile.
|
|
335
|
+
*
|
|
336
|
+
* Builds a {@link RasterLayer} with reprojection functions and a render
|
|
337
|
+
* pipeline that starts with a {@link CompositeBands} module binding all
|
|
338
|
+
* band textures, followed by any user-provided pipeline modules.
|
|
339
|
+
*/
|
|
340
|
+
_renderSubLayers(props, forwardTo4326, inverseFrom4326, forwardTo3857, inverseFrom3857) {
|
|
341
|
+
const { maxError, debug, debugOpacity } = this.props;
|
|
342
|
+
if (!props.data) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const { bands, forwardTransform, inverseTransform, width, height } = props.data;
|
|
346
|
+
// Build the composite bands mapping — default to first source for R if
|
|
347
|
+
// no composite mapping is provided
|
|
348
|
+
const composite = this.props.composite ?? {
|
|
349
|
+
r: [...bands.keys()][0],
|
|
350
|
+
};
|
|
351
|
+
// Skip rendering if cached tile data doesn't have the required bands
|
|
352
|
+
// (happens when switching presets — old tiles will be re-fetched)
|
|
353
|
+
const requiredBands = [
|
|
354
|
+
composite.r,
|
|
355
|
+
composite.g,
|
|
356
|
+
composite.b,
|
|
357
|
+
composite.a,
|
|
358
|
+
].filter((n) => n != null);
|
|
359
|
+
if (requiredBands.some((name) => !bands.has(name))) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
// Map named bands to fixed slot indices and build module props
|
|
363
|
+
const compositeBandsProps = buildCompositeBandsProps(composite, bands);
|
|
364
|
+
const renderPipeline = [
|
|
365
|
+
{
|
|
366
|
+
module: CompositeBands,
|
|
367
|
+
props: compositeBandsProps,
|
|
368
|
+
},
|
|
369
|
+
...(this.props.renderPipeline ?? []),
|
|
370
|
+
];
|
|
371
|
+
// Determine projection mode (globe vs web mercator)
|
|
372
|
+
const isGlobe = this.context.viewport.resolution !== undefined;
|
|
373
|
+
let reprojectionFns;
|
|
374
|
+
let deckProjectionProps;
|
|
375
|
+
if (isGlobe) {
|
|
376
|
+
reprojectionFns = {
|
|
377
|
+
forwardTransform,
|
|
378
|
+
inverseTransform,
|
|
379
|
+
forwardReproject: forwardTo4326,
|
|
380
|
+
inverseReproject: inverseFrom4326,
|
|
381
|
+
};
|
|
382
|
+
deckProjectionProps = {};
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
reprojectionFns = {
|
|
386
|
+
forwardTransform,
|
|
387
|
+
inverseTransform,
|
|
388
|
+
forwardReproject: forwardTo3857,
|
|
389
|
+
inverseReproject: inverseFrom3857,
|
|
390
|
+
};
|
|
391
|
+
deckProjectionProps = {
|
|
392
|
+
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
|
|
393
|
+
coordinateOrigin: [TILE_SIZE / 2, TILE_SIZE / 2, 0],
|
|
394
|
+
// biome-ignore format: array
|
|
395
|
+
modelMatrix: [
|
|
396
|
+
WEB_MERCATOR_TO_WORLD_SCALE, 0, 0, 0,
|
|
397
|
+
0, WEB_MERCATOR_TO_WORLD_SCALE, 0, 0,
|
|
398
|
+
0, 0, 1, 0,
|
|
399
|
+
0, 0, 0, 1
|
|
400
|
+
],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const rasterLayer = new RasterLayer(this.getSubLayerProps({
|
|
404
|
+
id: `${props.id}-raster`,
|
|
405
|
+
width,
|
|
406
|
+
height,
|
|
407
|
+
renderPipeline,
|
|
408
|
+
maxError,
|
|
409
|
+
reprojectionFns,
|
|
410
|
+
debug,
|
|
411
|
+
debugOpacity,
|
|
412
|
+
...deckProjectionProps,
|
|
413
|
+
}));
|
|
414
|
+
const sublayers = [rasterLayer];
|
|
415
|
+
if (debug && props.data) {
|
|
416
|
+
sublayers.push(...this._renderDebugLayers(props.id, props.tile, props.data, forwardTo4326));
|
|
417
|
+
}
|
|
418
|
+
return sublayers;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Render debug overlay layers for a single tile: colored outlines for
|
|
422
|
+
* primary and secondary tile boundaries, and tiered text labels.
|
|
423
|
+
*
|
|
424
|
+
* @param tileId - Base id for sub-layer naming.
|
|
425
|
+
* @param tile - The tile header with index info.
|
|
426
|
+
* @param data - The fetched multi-tile result containing debug info.
|
|
427
|
+
* @param forwardTo4326 - Projection function for converting CRS corners to WGS84.
|
|
428
|
+
* @returns Array of PathLayer and TextLayer sub-layers.
|
|
429
|
+
*/
|
|
430
|
+
_renderDebugLayers(tileId, tile, data, forwardTo4326) {
|
|
431
|
+
const layers = [];
|
|
432
|
+
const debugLevel = this.props.debugLevel ?? 1;
|
|
433
|
+
const { multiDescriptor } = this.state;
|
|
434
|
+
if (!multiDescriptor)
|
|
435
|
+
return layers;
|
|
436
|
+
const { x, y, z } = tile.index;
|
|
437
|
+
const primaryLevel = multiDescriptor.primary.levels[z];
|
|
438
|
+
if (!primaryLevel)
|
|
439
|
+
return layers;
|
|
440
|
+
// --- Primary tile outline and label ---
|
|
441
|
+
const primaryCrsCorners = primaryLevel.projectedTileCorners(x, y);
|
|
442
|
+
const { path: primaryPath, center: primaryCenter } = cornersToWgs84Path(primaryCrsCorners, forwardTo4326);
|
|
443
|
+
const primaryColor = DEBUG_COLORS[0];
|
|
444
|
+
layers.push(new PathLayer({
|
|
445
|
+
id: `${tileId}-debug-primary-outline`,
|
|
446
|
+
data: [primaryPath],
|
|
447
|
+
getPath: (d) => d,
|
|
448
|
+
getColor: primaryColor.outline,
|
|
449
|
+
getWidth: 2,
|
|
450
|
+
widthUnits: "pixels",
|
|
451
|
+
pickable: false,
|
|
452
|
+
}));
|
|
453
|
+
// Build primary label text
|
|
454
|
+
let primaryText = `x=${x} y=${y} z=${z}`;
|
|
455
|
+
if (debugLevel >= 2) {
|
|
456
|
+
primaryText += ` ${data.width}x${data.height}`;
|
|
457
|
+
}
|
|
458
|
+
if (debugLevel >= 3) {
|
|
459
|
+
primaryText += ` ${primaryLevel.metersPerPixel.toFixed(1)}m/px`;
|
|
460
|
+
}
|
|
461
|
+
// Count total label lines for vertical stacking
|
|
462
|
+
const secondaryNames = data.debugInfo
|
|
463
|
+
? [...data.debugInfo.bands.keys()]
|
|
464
|
+
: [];
|
|
465
|
+
const totalLines = 1 + secondaryNames.length;
|
|
466
|
+
const lineSpacing = 18; // pixels
|
|
467
|
+
const topOffset = ((totalLines - 1) * lineSpacing) / 2;
|
|
468
|
+
layers.push(new TextLayer({
|
|
469
|
+
id: `${tileId}-debug-primary-label`,
|
|
470
|
+
data: [
|
|
471
|
+
{
|
|
472
|
+
position: primaryCenter,
|
|
473
|
+
text: primaryText,
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
getColor: primaryColor.text,
|
|
477
|
+
getSize: 14,
|
|
478
|
+
getPixelOffset: [0, -topOffset],
|
|
479
|
+
sizeUnits: "pixels",
|
|
480
|
+
outlineWidth: 3,
|
|
481
|
+
outlineColor: [0, 0, 0, 255],
|
|
482
|
+
fontSettings: { sdf: true },
|
|
483
|
+
}));
|
|
484
|
+
// --- Secondary tile outlines and labels ---
|
|
485
|
+
if (!data.debugInfo)
|
|
486
|
+
return layers;
|
|
487
|
+
let secondaryIdx = 0;
|
|
488
|
+
for (const [name, info] of data.debugInfo.bands) {
|
|
489
|
+
const colorEntry = DEBUG_COLORS[1 + (secondaryIdx % (DEBUG_COLORS.length - 1))];
|
|
490
|
+
// Draw outline for each secondary tile
|
|
491
|
+
for (let i = 0; i < info.secondaryTileCorners.length; i++) {
|
|
492
|
+
const { path: secondaryPath } = cornersToWgs84Path(info.secondaryTileCorners[i], forwardTo4326);
|
|
493
|
+
layers.push(new PathLayer({
|
|
494
|
+
id: `${tileId}-debug-${name}-outline-${i}`,
|
|
495
|
+
data: [secondaryPath],
|
|
496
|
+
getPath: (d) => d,
|
|
497
|
+
getColor: colorEntry.outline,
|
|
498
|
+
getWidth: 2,
|
|
499
|
+
widthUnits: "pixels",
|
|
500
|
+
pickable: false,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
// Build secondary label text
|
|
504
|
+
const mpp = info.metersPerPixel.toFixed(1);
|
|
505
|
+
let labelText = `${name}: ${mpp}m z=${info.secondaryZ}`;
|
|
506
|
+
if (debugLevel >= 2) {
|
|
507
|
+
const uv = info.uvTransform;
|
|
508
|
+
labelText += ` uv=[${uv.map((v) => v.toFixed(2)).join(",")}] ${info.tileCount} tiles`;
|
|
509
|
+
}
|
|
510
|
+
if (debugLevel >= 3) {
|
|
511
|
+
labelText += ` stitch=${info.stitchedWidth}x${info.stitchedHeight}`;
|
|
512
|
+
}
|
|
513
|
+
const lineOffset = -topOffset + (1 + secondaryIdx) * lineSpacing;
|
|
514
|
+
layers.push(new TextLayer({
|
|
515
|
+
id: `${tileId}-debug-${name}-label`,
|
|
516
|
+
data: [
|
|
517
|
+
{
|
|
518
|
+
position: primaryCenter,
|
|
519
|
+
text: labelText,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
getColor: colorEntry.text,
|
|
523
|
+
getSize: 12,
|
|
524
|
+
getPixelOffset: [0, lineOffset],
|
|
525
|
+
sizeUnits: "pixels",
|
|
526
|
+
outlineWidth: 2,
|
|
527
|
+
outlineColor: [0, 0, 0, 255],
|
|
528
|
+
fontSettings: { sdf: true },
|
|
529
|
+
}));
|
|
530
|
+
secondaryIdx++;
|
|
531
|
+
}
|
|
532
|
+
return layers;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Build the tile layer that drives tile traversal and rendering.
|
|
536
|
+
*
|
|
537
|
+
* Creates a {@link RasterTileset2D} factory from the primary tileset,
|
|
538
|
+
* then returns a {@link TileLayer} wired up with tile fetching and
|
|
539
|
+
* sub-layer rendering.
|
|
540
|
+
*/
|
|
541
|
+
renderTileLayer(multiDescriptor, forwardTo4326, inverseFrom4326, forwardTo3857, inverseFrom3857) {
|
|
542
|
+
const { primary } = multiDescriptor;
|
|
543
|
+
// Create a factory class that wraps RasterTileset2D with the primary descriptor
|
|
544
|
+
class PrimaryTilesetFactory extends RasterTileset2D {
|
|
545
|
+
constructor(opts) {
|
|
546
|
+
super(opts, primary, {
|
|
547
|
+
projectTo4326: forwardTo4326,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const { maxRequests, maxCacheSize, maxCacheByteSize, debounceTime, refinementStrategy, } = this.props;
|
|
552
|
+
// Stringify sources to detect when the set of COG URLs changes.
|
|
553
|
+
// This triggers TileLayer to invalidate its cache and re-fetch.
|
|
554
|
+
const sourceKeys = Object.keys(this.props.sources).sort().join(",");
|
|
555
|
+
const sourceUrls = Object.values(this.props.sources)
|
|
556
|
+
.map((s) => String(s.url))
|
|
557
|
+
.sort()
|
|
558
|
+
.join(",");
|
|
559
|
+
return new TileLayer({
|
|
560
|
+
id: `multi-cog-tile-layer-${this.id}-${sourceUrls}`,
|
|
561
|
+
TilesetClass: PrimaryTilesetFactory,
|
|
562
|
+
getTileData: async (tile) => this._getTileData(tile),
|
|
563
|
+
renderSubLayers: (props) => this._renderSubLayers(props, forwardTo4326, inverseFrom4326, forwardTo3857, inverseFrom3857),
|
|
564
|
+
updateTriggers: {
|
|
565
|
+
getTileData: [sourceKeys, sourceUrls],
|
|
566
|
+
},
|
|
567
|
+
debounceTime,
|
|
568
|
+
maxCacheByteSize,
|
|
569
|
+
maxCacheSize,
|
|
570
|
+
maxRequests,
|
|
571
|
+
refinementStrategy,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
renderLayers() {
|
|
575
|
+
const { multiDescriptor, forwardTo4326, inverseFrom4326, forwardTo3857, inverseFrom3857, } = this.state;
|
|
576
|
+
if (!multiDescriptor ||
|
|
577
|
+
!forwardTo4326 ||
|
|
578
|
+
!inverseFrom4326 ||
|
|
579
|
+
!forwardTo3857 ||
|
|
580
|
+
!inverseFrom3857) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
return this.renderTileLayer(multiDescriptor, forwardTo4326, inverseFrom4326, forwardTo3857, inverseFrom3857);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Select the correct GeoTIFF image (full-res or overview) for a zoom level.
|
|
588
|
+
*
|
|
589
|
+
* z=0 is the coarsest overview, z=max is full resolution.
|
|
590
|
+
*/
|
|
591
|
+
function selectImage(geotiff, z) {
|
|
592
|
+
const images = [geotiff, ...geotiff.overviews];
|
|
593
|
+
return images[images.length - 1 - z];
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Create a GPU texture from a {@link RasterArray}.
|
|
597
|
+
*
|
|
598
|
+
* Infers the texture format from the typed array type. Currently supports
|
|
599
|
+
* single-band `Uint8Array` (`r8unorm`) and `Uint16Array` (`r16unorm`).
|
|
600
|
+
*
|
|
601
|
+
* TODO: use `inferTextureFormat` from `texture.ts` for full format support.
|
|
602
|
+
*/
|
|
603
|
+
function createBandTexture(device, array) {
|
|
604
|
+
if (array.layout !== "pixel-interleaved") {
|
|
605
|
+
throw new Error("Band-separate layout not yet supported in MultiCOGLayer");
|
|
606
|
+
}
|
|
607
|
+
const { data, width, height } = array;
|
|
608
|
+
let format;
|
|
609
|
+
if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
|
|
610
|
+
format = "r8unorm";
|
|
611
|
+
}
|
|
612
|
+
else if (data instanceof Uint16Array) {
|
|
613
|
+
format = "r16unorm";
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
throw new Error(`Unsupported typed array type: ${data.constructor.name}. ` +
|
|
617
|
+
"Currently only Uint8Array and Uint16Array are supported.");
|
|
618
|
+
}
|
|
619
|
+
return device.createTexture({
|
|
620
|
+
data,
|
|
621
|
+
format,
|
|
622
|
+
width,
|
|
623
|
+
height,
|
|
624
|
+
sampler: { minFilter: "linear", magFilter: "linear" },
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Project CRS tile corners to WGS84 and return a closed path suitable for
|
|
629
|
+
* PathLayer, plus the center point for label placement.
|
|
630
|
+
*
|
|
631
|
+
* @param corners - Tile corners in the source CRS.
|
|
632
|
+
* @param projectTo4326 - Projection function from source CRS to WGS84.
|
|
633
|
+
* @returns A closed `[topLeft, topRight, bottomRight, bottomLeft, topLeft]`
|
|
634
|
+
* path and the geographic center.
|
|
635
|
+
*/
|
|
636
|
+
function cornersToWgs84Path(corners, projectTo4326) {
|
|
637
|
+
const topLeft = projectTo4326(corners.topLeft[0], corners.topLeft[1]);
|
|
638
|
+
const topRight = projectTo4326(corners.topRight[0], corners.topRight[1]);
|
|
639
|
+
const bottomRight = projectTo4326(corners.bottomRight[0], corners.bottomRight[1]);
|
|
640
|
+
const bottomLeft = projectTo4326(corners.bottomLeft[0], corners.bottomLeft[1]);
|
|
641
|
+
return {
|
|
642
|
+
path: [topLeft, topRight, bottomRight, bottomLeft, topLeft],
|
|
643
|
+
center: [
|
|
644
|
+
(topLeft[0] + bottomRight[0]) / 2,
|
|
645
|
+
(topLeft[1] + bottomRight[1]) / 2,
|
|
646
|
+
],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
//# sourceMappingURL=multi-cog-layer.js.map
|