Skip to content

Commit ceec0ac

Browse files
authored
fix: Support TileLayer refinement strategies (#354)
* fix: Refinement strategy * fmt * Define TileMetadata as type * define type * depend on proj * return densified bounds * clean up * cleaner * clean up tests
1 parent 7a23c74 commit ceec0ac

File tree

9 files changed

+444
-30
lines changed

9 files changed

+444
-30
lines changed

packages/deck.gl-geotiff/src/cog-layer.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { PathLayer } from "@deck.gl/layers";
1616
import type {
1717
RasterLayerProps,
1818
RasterModule,
19+
TileMetadata,
1920
} from "@developmentseed/deck.gl-raster";
2021
import {
2122
RasterLayer,
@@ -407,7 +408,12 @@ export class COGLayer<
407408
inverseFrom3857: ReprojectionFns["inverseReproject"],
408409
): Layer | LayersList | null {
409410
const { maxError, debug, debugOpacity } = this.props;
410-
const { tile } = props;
411+
412+
// Cast to include TileMetadata from raster-tileset's `getTileMetadata`
413+
// method.
414+
// TODO: implement generic handling of tile metadata upstream in TileLayer
415+
const tile = props.tile as Tile2DHeader<GetTileDataResult<DataT>> &
416+
TileMetadata;
411417

412418
if (!props.data) {
413419
return null;
@@ -495,35 +501,34 @@ export class COGLayer<
495501
}
496502

497503
if (debug) {
498-
// Get projected bounds from tile data
499-
// getTileMetadata returns data that includes projectedBounds
500-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
501-
const projectedBounds: {
502-
topLeft: [number, number];
503-
topRight: [number, number];
504-
bottomLeft: [number, number];
505-
bottomRight: [number, number];
506-
} = (tile as any)?.projectedBounds;
507-
508-
if (!projectedBounds || !tms) {
504+
const { projectedCorners } = tile;
505+
506+
if (!projectedCorners || !tms) {
509507
return [];
510508
}
511509

512-
// Project bounds from image CRS to WGS84
513-
const { topLeft, topRight, bottomLeft, bottomRight } = projectedBounds;
514-
510+
// Create a closed path in WGS84 projection around the tile bounds
511+
//
512+
// The tile has a `bbox` field which is already the bounding box in WGS84,
513+
// but that uses `transformBounds` and densifies edges. So the corners of
514+
// the bounding boxes don't line up with each other.
515+
//
516+
// In this case in the debug mode, it looks better if we ignore the actual
517+
// non-linearities of the edges and just draw a box connecting the
518+
// reprojected corners. In any case, the _image itself_ will be densified
519+
// on the edges as a feature of the mesh generation.
520+
const { topLeft, topRight, bottomRight, bottomLeft } = projectedCorners;
515521
const topLeftWgs84 = forwardTo4326(topLeft[0], topLeft[1]);
516522
const topRightWgs84 = forwardTo4326(topRight[0], topRight[1]);
517523
const bottomRightWgs84 = forwardTo4326(bottomRight[0], bottomRight[1]);
518524
const bottomLeftWgs84 = forwardTo4326(bottomLeft[0], bottomLeft[1]);
519525

520-
// Create a closed path around the tile bounds
521526
const path = [
522527
topLeftWgs84,
523528
topRightWgs84,
524529
bottomRightWgs84,
525530
bottomLeftWgs84,
526-
topLeftWgs84, // Close the path
531+
topLeftWgs84,
527532
];
528533

529534
layers.push(

packages/deck.gl-raster/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"dependencies": {
6363
"@developmentseed/affine": "workspace:^",
6464
"@developmentseed/morecantile": "workspace:^",
65+
"@developmentseed/proj": "workspace:^",
6566
"@developmentseed/raster-reproject": "workspace:^",
6667
"@math.gl/core": "^4.1.0",
6768
"@math.gl/culling": "^4.1.0",

packages/deck.gl-raster/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type { RasterModule } from "./gpu-modules/types.js";
22
export type { RasterLayerProps } from "./raster-layer.js";
33
export { RasterLayer } from "./raster-layer.js";
4+
export type { TileMetadata } from "./raster-tileset/index.js";
45
export { TileMatrixSetTileset } from "./raster-tileset/index.js";
56

67
import { __TEST_EXPORTS as traversalTestExports } from "./raster-tileset/raster-tile-traversal.js";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export type { TileMetadata } from "./raster-tileset-2d.js";
12
export { TileMatrixSetTileset } from "./raster-tileset-2d.js";

packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,77 @@
66
*/
77

88
import type { Viewport } from "@deck.gl/core";
9-
import type { _Tileset2DProps as Tileset2DProps } from "@deck.gl/geo-layers";
9+
import type {
10+
GeoBoundingBox,
11+
_Tileset2DProps as Tileset2DProps,
12+
} from "@deck.gl/geo-layers";
1013
import { _Tileset2D as Tileset2D } from "@deck.gl/geo-layers";
1114
import * as affine from "@developmentseed/affine";
12-
import type { BoundingBox, TileMatrixSet } from "@developmentseed/morecantile";
15+
import type {
16+
BoundingBox,
17+
TileMatrix,
18+
TileMatrixSet,
19+
} from "@developmentseed/morecantile";
1320
import { tileTransform } from "@developmentseed/morecantile";
21+
import { transformBounds } from "@developmentseed/proj";
1422
import type { Matrix4 } from "@math.gl/core";
15-
1623
import { getTileIndices } from "./raster-tile-traversal";
1724
import type {
1825
Bounds,
1926
CornerBounds,
2027
Point,
28+
ProjectedBoundingBox,
2129
ProjectionFunction,
2230
TileIndex,
2331
ZRange,
2432
} from "./types";
2533

34+
/** Type returned by `getTileMetadata` */
35+
export type TileMetadata = {
36+
/**
37+
* **Axis-aligned** bounding box of the tile in **WGS84 coordinates**.
38+
*/
39+
bbox: GeoBoundingBox;
40+
41+
/**
42+
* **Axis-aligned** bounding box of the tile in **projected coordinates**.
43+
*/
44+
projectedBbox: ProjectedBoundingBox;
45+
46+
/**
47+
* "Rotated" bounding box of the tile in **projected coordinates**,
48+
* represented as four corners.
49+
*
50+
* This preserves rotation/skew information that would be lost in the
51+
* axis-aligned bbox.
52+
*/
53+
projectedCorners: {
54+
topLeft: Point;
55+
topRight: Point;
56+
bottomLeft: Point;
57+
bottomRight: Point;
58+
};
59+
60+
/**
61+
* Tile width in pixels.
62+
*
63+
* Note this may differ between levels in some TileMatrixSets.
64+
*/
65+
tileWidth: number;
66+
67+
/**
68+
* Tile height in pixels.
69+
*
70+
* Note this may differ between levels in some TileMatrixSets.
71+
*/
72+
tileHeight: number;
73+
74+
/**
75+
* A reference to the underlying TileMatrix.
76+
*/
77+
tileMatrix: TileMatrix;
78+
};
79+
2680
/**
2781
* A generic tileset implementation organized according to the OGC
2882
* [TileMatrixSet](https://docs.ogc.org/is/17-083r4/17-083r4.html)
@@ -109,11 +163,25 @@ export class TileMatrixSetTileset extends Tileset2D {
109163
const currentOverview = this.tms.tileMatrices[index.z]!;
110164
const parentOverview = this.tms.tileMatrices[index.z - 1]!;
111165

112-
const decimation = currentOverview.cellSize / parentOverview.cellSize;
166+
// Decimation is the number of child tiles that fit across one parent tile.
167+
// Must use tile footprint (cellSize × tileWidth/Height), not cellSize alone,
168+
// because tileWidth can change between levels (e.g. the last Sentinel-2
169+
// overview doubles tileWidth while halving cellSize, giving a 1:1 spatial
170+
// mapping where decimation = 1).
171+
const parentFootprintX = parentOverview.cellSize * parentOverview.tileWidth;
172+
const parentFootprintY =
173+
parentOverview.cellSize * parentOverview.tileHeight;
174+
const currentFootprintX =
175+
currentOverview.cellSize * currentOverview.tileWidth;
176+
const currentFootprintY =
177+
currentOverview.cellSize * currentOverview.tileHeight;
178+
179+
const decimationX = parentFootprintX / currentFootprintX;
180+
const decimationY = parentFootprintY / currentFootprintY;
113181

114182
return {
115-
x: Math.floor(index.x / decimation),
116-
y: Math.floor(index.y / decimation),
183+
x: Math.floor(index.x / decimationX),
184+
y: Math.floor(index.y / decimationY),
117185
z: index.z - 1,
118186
};
119187
}
@@ -122,7 +190,7 @@ export class TileMatrixSetTileset extends Tileset2D {
122190
return index.z;
123191
}
124192

125-
override getTileMetadata(index: TileIndex): Record<string, unknown> {
193+
override getTileMetadata(index: TileIndex): TileMetadata {
126194
const { x, y, z } = index;
127195
const { tileMatrices } = this.tms;
128196
const tileMatrix = tileMatrices[z]!;
@@ -138,24 +206,44 @@ export class TileMatrixSetTileset extends Tileset2D {
138206

139207
// Return the projected bounds as four corners
140208
// This preserves rotation/skew information
141-
const projectedBounds = {
209+
const projectedCorners = {
142210
topLeft,
143211
topRight,
144212
bottomLeft,
145213
bottomRight,
146214
};
147215

148216
// Also compute axis-aligned bounding box for compatibility
149-
const bounds: Bounds = [
217+
const projectedBounds: Bounds = [
150218
Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]),
151219
Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]),
152220
Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]),
153221
Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]),
154222
];
155223

224+
// deck.gl's Tile2DHeader uses `bbox` (GeoBoundingBox) for screen-space
225+
// culling in filterSubLayer → isTileVisible. Without this, all tiles
226+
// would pass (or fail) the cull-rect test and the refinementStrategy
227+
// (best-available) would not show parent tiles correctly.
228+
const [west, south, east, north] = transformBounds(
229+
this.projectTo4326,
230+
...projectedBounds,
231+
);
232+
156233
return {
157-
bounds,
158-
projectedBounds,
234+
bbox: {
235+
west,
236+
south,
237+
east,
238+
north,
239+
},
240+
projectedBbox: {
241+
left: projectedBounds[0],
242+
bottom: projectedBounds[1],
243+
right: projectedBounds[2],
244+
top: projectedBounds[3],
245+
},
246+
projectedCorners,
159247
tileWidth,
160248
tileHeight,
161249
tileMatrix,

packages/deck.gl-raster/src/raster-tileset/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ export type GeoBoundingBox = {
88
east: number;
99
south: number;
1010
};
11-
export type NonGeoBoundingBox = {
11+
12+
export type ProjectedBoundingBox = {
1213
left: number;
1314
top: number;
1415
right: number;
1516
bottom: number;
1617
};
1718

18-
export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox;
19+
export type TileBoundingBox = ProjectedBoundingBox | GeoBoundingBox;
1920

2021
export type TileLoadProps = {
2122
index: TileIndex;

0 commit comments

Comments
 (0)