Skip to content
Merged
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
18 changes: 12 additions & 6 deletions packages/deck.gl-geotiff/src/mosaic-layer/mosaic-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ export type MosaicLayerProps<
TileLayerProps,
// NOTE: `debounceTime` is intentionally not exposed.
// See https://github.com/developmentseed/deck.gl-raster/issues/562
| "extent"
| "maxCacheByteSize"
| "maxCacheSize"
| "maxRequests"
| "maxZoom"
| "minZoom"
"maxCacheByteSize" | "maxCacheSize" | "maxRequests" | "maxZoom" | "minZoom"
> & {
/**
* List of mosaic sources to render.
Expand All @@ -55,6 +50,17 @@ export type MosaicLayerProps<
*/
sources: MosaicT[];

/**
* Only load and render sources whose `bbox` overlaps this bounding box,
* given as `[minLng, minLat, maxLng, maxLat]` in WGS84 degrees.
*
* A source that overlaps it is drawn in full, not clipped to it. To limit
* what a source loads as well, pass the same `extent` to the layers
* `renderSource` returns. Unlike in deck.gl's TileLayer, it does not keep
* sources loading below `minZoom`.
*/
extent?: number[] | null;

/**
* Caps concurrent HTTP requests for this layer's source fetches.
*
Expand Down
17 changes: 16 additions & 1 deletion packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
_Tileset2DProps as Tileset2DProps,
} from "@deck.gl/geo-layers";
import { _Tileset2D as Tileset2D } from "@deck.gl/geo-layers";
import type { Bounds } from "@developmentseed/deck.gl-raster";
import type Flatbush from "flatbush";

/** Tile index.
Expand Down Expand Up @@ -120,9 +121,15 @@ export class MosaicTileset2D<MosaicT extends MosaicSource> extends Tileset2D {
}

const viewportBounds = viewport.getBounds();
const indices = index.search(...viewportBounds);
let indices = index.search(...viewportBounds);

const sources = this.getSources();
// Read `extent` on every call: TileLayer updates it through `setOptions`.
const extent = this.opts.extent as Bounds | null;
if (extent) {
indices = indices.filter((i) => overlaps(sources[i]!.bbox, extent));
}

return indices.map((sourceIndex) => {
const source = sources[sourceIndex]!;
return {
Expand All @@ -137,3 +144,11 @@ export class MosaicTileset2D<MosaicT extends MosaicSource> extends Tileset2D {
});
}
}

/**
* Whether two `[minX, minY, maxX, maxY]` boxes overlap. Boxes that only share
* an edge or a corner don't, as with tiles and `extent` in deck.gl's TileLayer.
*/
function overlaps(a: Bounds, b: Bounds): boolean {
return a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
}
67 changes: 66 additions & 1 deletion packages/deck.gl-geotiff/tests/mosaic-tileset-2d.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function buildIndex(sources: MosaicSource[]): Flatbush | null {

function makeTileset<T extends MosaicSource>(
sources: T[],
opts: { maxRequests?: number } = {},
opts: { maxRequests?: number; extent?: number[] } = {},
): MosaicTileset2D<T> {
const index = buildIndex(sources);
return new MosaicTileset2D<T>(
Expand All @@ -42,6 +42,8 @@ function makeTileset<T extends MosaicSource>(
...(opts.maxRequests !== undefined
? { maxRequests: opts.maxRequests }
: {}),
// deck.gl's TileLayer always passes `extent`, `null` by default.
extent: opts.extent ?? null,
} as unknown as Tileset2DProps,
);
}
Expand Down Expand Up @@ -125,3 +127,66 @@ describe("MosaicTileset2D tile ids", () => {
expect(tileset.getTileId(result[0]!)).toBe("stable-id");
});
});

describe("MosaicTileset2D extent", () => {
// Shows all of A, B and C.
const viewport = makeViewport([-1, -1, 51, 11]);
const names = (sources: Item[]) => sources.map((s) => s.name).sort();

it("selects only the sources that overlap the extent", () => {
// Covers part of A and part of B. Asymmetric, so reading it with swapped
// axes would select only A.
const tileset = makeTileset([A, B, C], { extent: [5, 2, 25, 8] });
expect(names(tileset.getTileIndices({ viewport }))).toEqual(["A", "B"]);
});

it("keeps array-position ids when the extent skips an earlier source", () => {
// Skips A. B and C still get ids from their positions in `sources`.
const tileset = makeTileset([A, B, C], { extent: [25, 2, 45, 8] });
const result = tileset.getTileIndices({ viewport });
const ids = Object.fromEntries(
result.map((s) => [s.name, tileset.getTileId(s)]),
);
expect(ids).toEqual({ B: "1", C: "2" });
});

it("skips sources that only touch the extent", () => {
// Shares A's east edge and B's west edge. deck.gl's TileLayer likewise
// skips tiles that only touch its extent.
const sideBySide = makeTileset([A, B, C], { extent: [10, 0, 20, 10] });
expect(names(sideBySide.getTileIndices({ viewport }))).toEqual([]);

// Shares the north edges of A, B and C.
const above = makeTileset([A, B, C], { extent: [0, 10, 50, 20] });
expect(names(above.getTileIndices({ viewport }))).toEqual([]);
});

it("keeps a visible source whose overlap with the extent is off-screen", () => {
// `wide` reaches the extent off-screen to the east, and a source is drawn
// in full, so its west end stays on screen. `near` is on screen but
// misses the extent.
const wide: Item = { name: "wide", bbox: [0, 0, 100, 10] };
const near: Item = { name: "near", bbox: [0, 20, 10, 30] };
const tileset = makeTileset([wide, near], { extent: [80, 0, 90, 10] });
const result = tileset.getTileIndices({
viewport: makeViewport([-1, -1, 11, 31]),
});
expect(names(result)).toEqual(["wide"]);
});

it("applies an extent changed through setOptions on the next call", () => {
const tileset = makeTileset([A, B, C]);
const getTileData = () => new Promise(() => {});

// deck.gl's TileLayer calls `setOptions` when its props change.
tileset.setOptions({ getTileData, extent: [5, 2, 25, 8] });
expect(names(tileset.getTileIndices({ viewport }))).toEqual(["A", "B"]);

tileset.setOptions({ getTileData, extent: null });
expect(names(tileset.getTileIndices({ viewport }))).toEqual([
"A",
"B",
"C",
]);
});
});
Loading