Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/cog-basic/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import type { MapRef } from "react-map-gl/maplibre";
import { Map as MaplibreMap } from "react-map-gl/maplibre";

const COG_OPTIONS: { title: string; url: string; attribution?: ReactNode }[] = [
{
title: "HFP 100",
url: "https://data.source.coop/vizzuality/hfp-100/hfp_2017_100m_v1-2_cog.tif",
},
{
title: "Sentinel-2 True Color Image (New York, 2026)",
url: "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/18/T/WL/2026/1/S2B_18TWL_20260101_0_L2A/TCI.tif",
Expand Down
29 changes: 25 additions & 4 deletions packages/deck.gl-raster/src/raster-tileset/affine-tileset-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ export class AffineTilesetLevel implements RasterTilesetLevel {

private readonly _affine: Affine;
private readonly _invAffine: Affine;
private readonly _arrayWidth: number;
private readonly _arrayHeight: number;

constructor(options: AffineTilesetLevelOptions) {
this._affine = options.affine;
this._invAffine = affine.invert(options.affine);
this._arrayWidth = options.arrayWidth;
this._arrayHeight = options.arrayHeight;
this.tileWidth = options.tileWidth;
this.tileHeight = options.tileHeight;
this.matrixWidth = Math.ceil(options.arrayWidth / options.tileWidth);
Expand Down Expand Up @@ -79,11 +83,28 @@ export class AffineTilesetLevel implements RasterTilesetLevel {
const th = this.tileHeight;
const af = this._affine;

// Clip the tile's pixel-space rectangle to the array extent. Tiles at the
// right/bottom edge of the matrix may extend past the array when
// `matrixWidth*tileWidth > arrayWidth` (likewise for height); the affine
// still extrapolates past the data, but for projections whose valid
// domain isn't a rectangle aligned with the affine (e.g. Mollweide,
// Sinusoidal, Equal Earth) those extrapolated points fall outside the
// projection domain. The downstream bounding-volume sampler reprojects
// these corners and, for points outside the domain, sees them collapse to
// the pole — producing a degenerate volume that frustum culling rejects.
// Clipping to the actual data extent keeps every corner inside the
// projection's domain (modulo the projection's own corner-cases) and is
// also the semantically correct extent for the tile's data.
const x0 = Math.min(col * tw, this._arrayWidth);
const x1 = Math.min((col + 1) * tw, this._arrayWidth);
const y0 = Math.min(row * th, this._arrayHeight);
const y1 = Math.min((row + 1) * th, this._arrayHeight);

return {
topLeft: affine.apply(af, col * tw, row * th),
topRight: affine.apply(af, (col + 1) * tw, row * th),
bottomLeft: affine.apply(af, col * tw, (row + 1) * th),
bottomRight: affine.apply(af, (col + 1) * tw, (row + 1) * th),
topLeft: affine.apply(af, x0, y0),
topRight: affine.apply(af, x1, y0),
bottomLeft: affine.apply(af, x0, y1),
bottomRight: affine.apply(af, x1, y1),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,43 @@ describe("AffineTilesetLevel", () => {
expect(corners.topRight[0]).toBeCloseTo(100 + 4 * 10 * Math.cos(rad), 10);
expect(corners.topRight[1]).toBeCloseTo(200 + 4 * 10 * Math.sin(rad), 10);
});

it("clips an edge tile's corners to the array extent when tileSize does not divide arraySize evenly", () => {
// 5x3 array tiled in 4x4 chunks → 2x1 matrix; tile (1, 0) nominally
// covers pixels [4..8, 0..4] but the array only has data at [4..5, 0..3].
const level = new AffineTilesetLevel({
affine: SQUARE_AFFINE,
arrayWidth: 5,
arrayHeight: 3,
tileWidth: 4,
tileHeight: 4,
mpu: 1,
});
const corners = level.projectedTileCorners(1, 0);
// Without clipping, topRight would be at affine(8, 0) = (180, 200).
// With clipping it should sit at affine(5, 0) = (150, 200).
expect(corners.topLeft).toEqual([140, 200]);
expect(corners.topRight).toEqual([150, 200]);
expect(corners.bottomLeft).toEqual([140, 170]);
expect(corners.bottomRight).toEqual([150, 170]);
});

it("leaves interior tiles unaffected by clipping", () => {
// Interior tile (0, 0) is well within the array — clipping is a no-op.
const level = new AffineTilesetLevel({
affine: SQUARE_AFFINE,
arrayWidth: 12,
arrayHeight: 12,
tileWidth: 4,
tileHeight: 4,
mpu: 1,
});
const corners = level.projectedTileCorners(0, 0);
expect(corners.topLeft).toEqual([100, 200]);
expect(corners.topRight).toEqual([140, 200]);
expect(corners.bottomLeft).toEqual([100, 160]);
expect(corners.bottomRight).toEqual([140, 160]);
});
});

describe("tileTransform", () => {
Expand Down