Skip to content

fix(deck.gl-geotiff): Honor extent in MosaicLayer - #688

Open
kylebarron wants to merge 1 commit into
mainfrom
kyle/mosaic-extent
Open

kylebarron wants to merge 1 commit into
mainfrom
kyle/mosaic-extent

Conversation

@kylebarron

Copy link
Copy Markdown
Member

Note

This PR was written by Claude (Claude Code), not by @kylebarron.

MosaicLayer accepts extent and passes it to its inner TileLayer, which hands it to MosaicTileset2D. But MosaicTileset2D overrides getTileIndices and never reads this.opts.extent, so the prop does nothing. Every source whose bbox meets the viewport is still fetched, rendered and reported by onViewportLoad, with no warning. It has been this way since MosaicLayer was added in #184.

#684 makes the same fix for RasterTileset2D, the repo's only other Tileset2D subclass, and its description lists MosaicTileset2D as not changed there. Ref #684

Reproduction

new MosaicLayer({
  id: "mosaic",
  sources: [
    { bbox: [0, 0, 10, 10] }, // A
    { bbox: [20, 0, 30, 10] }, // B
    { bbox: [40, 0, 50, 10] }, // C
  ],
  extent: [15, -5, 35, 5], // overlaps only B
  getSource: async (source) => {
    console.log("fetching", source.bbox);
  },
  renderSource: () => null,
});

In a viewport that shows all three sources (longitude 25, latitude 5, zoom 3, 512×512), main fetches all three. I ran a version of this with named sources and an onViewportLoad callback through deck.gl's LayerManager in vitest (no GPU device, public API only), logging what getSource fetched and what onViewportLoad reported:

main     extent=[15,-5,35,5]: fetched=["A","B","C"] onViewportLoad=["A","B","C"]
this PR  extent=[15,-5,35,5]: fetched=["B"] onViewportLoad=["B"]

On main, changing extent on later renders (none, then [15,-5,35,5], then [5,-5,25,5], then none) never changes the selection. With this PR, onViewportLoad reports ["A","B","C"], ["B"], ["A","B"], then ["A","B","C"].

Fix

  • MosaicTileset2D.getTileIndices keeps its zoom gate and its Flatbush search of the viewport, then drops the hits whose bbox doesn't overlap extent. It reads this.opts.extent on every call, because TileLayer updates it through setOptions. The filter runs before the hits are mapped, so a source's default tile id is still its position in sources.
  • It uses the same rule as deck.gl's TileLayer (OSMNode.update's insideBounds) and fix(deck.gl-raster): Infer missing TileMatrixSet bounds and honor extent #684. The overlap is strict, so a source that only touches the extent at an edge or a corner is skipped. Each source is tested against the whole extent, not against the part of it in view, so a source that is on screen and overlaps the extent off-screen still loads (it is drawn in full).
  • extent is no longer picked from TileLayerProps. MosaicLayerProps now declares it with TSDoc, which covers:
    • the format: WGS84 [minLng, minLat, maxLng, maxLat]
    • that an overlapping source is drawn in full, so to limit what it loads, pass the same extent to the layers renderSource returns
    • that unlike deck.gl's TileLayer, it doesn't keep sources loading below minZoom

Tests

There's a new MosaicTileset2D extent block in tests/mosaic-tileset-2d.test.ts. Its tests check that the tileset:

  • selects only the sources that overlap an asymmetric extent
  • keeps array-position ids when the extent skips an earlier source
  • skips sources that only touch the extent, at an east/west edge and at a north edge
  • keeps a visible source whose overlap with the extent is off-screen, and skips an on-screen source that misses the extent
  • applies an extent changed through setOptions on the next call

Red/green: with only mosaic-tileset-2d.ts and mosaic-layer.ts reverted to main, all 5 new tests fail (Tests 5 failed | 6 passed (11)). With this PR, Tests 11 passed (11). I also tried these wrong versions of the fix, and each one fails at least one test:

  • inclusive comparisons, on both axes or on y alone
  • the extent read with its axes swapped
  • searching viewport ∩ extent instead of testing each source
  • the extent captured once in the constructor
  • ids numbered after filtering

Checks:

  • pnpm --filter @developmentseed/deck.gl-geotiff test: Test Files 7 passed (7), Tests 38 passed (38).
  • pnpm --filter @developmentseed/deck.gl-geotiff typecheck passes under TypeScript 6.0.3. I haven't run it under TypeScript 7 (chore(deps-dev): upgrade to TypeScript 7 #685); CI will.
  • pnpm exec biome ci .: Checked 385 files ... No fixes applied.

Open questions and judgment calls

Related bug, not fixed here

MosaicLayer's getSources closure (() => this.props.sources) stays bound to the first layer instance. TileLayer creates the tileset once, and deck.gl moves state to each new layer instance but leaves the old instance's props as they were. So after the first render, the tileset pairs the rebuilt spatial index with the first sources array. In the same LayerManager setup, with no extent (main gives the same results):

# sources grow after the first render
sources=["A"]: fetched=["A"] onViewportLoad=["A"]
sources=["A","B","C"]: fetched=[] onViewportLoad=null
# sources shrink after the first render
sources=["A","B","C"]: fetched=["A","B","C"] onViewportLoad=["A","B","C"]
sources=["C"]: fetched=[] onViewportLoad=["A"]

So filtering sources yourself is not a reliable substitute for extent once the layer has rendered. #673 switches the closure to this.state.sources, which would fix this; it probably deserves its own issue. The new filter assumes, like the existing .map, that every index hit has a matching entry in sources, so it doesn't make this worse.

🤖 Written by Claude Code

MosaicLayer forwards `extent` to its tileset, but MosaicTileset2D
overrides `getTileIndices` without reading it, so every source in view
still loaded. Keep only the sources whose bbox overlaps the extent (not
just touching it, as in deck.gl's TileLayer), reading it on every call
so prop updates apply. `extent` is now documented on MosaicLayerProps as
WGS84 `[minLng, minLat, maxLng, maxLat]`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the fix label Oct 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant