Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
115 changes: 11 additions & 104 deletions dev-docs/specs/2026-05-27-antimeridian-crossing-tile-design.md

Large diffs are not rendered by default.

33 changes: 29 additions & 4 deletions dev-docs/world-copies.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ so panning across the antimeridian evicts visible tiles. See
- A tile is identified by `(x, y, z)`. Its data and primary-world common-space
position are computed once (see `RasterTileNode.getBoundingVolume`) and
cached.
- For a viewport with `subViewports.length > 1` (Web Mercator + `repeat: true`
whose bounds straddle ±180°), the traversal additionally tests each tile's
bounding volume **translated** by `worldOffset * 512` along common-space X
for `worldOffset` in `±1, ±2, … ±MAX_MAPS`.
- Whenever repeat mode is active at all (`viewport.subViewports != null` — Web
Mercator + `repeat: true`, regardless of whether the *viewport's own*
current bounds happen to straddle ±180°), the traversal additionally tests
each tile's bounding volume **translated** by `worldOffset * 512` along
common-space X for `worldOffset` in `±1, ±2, … ±MAX_MAPS`.
- Originally gated on `subViewports.length > 1` instead — that only tests
whether the *viewport* currently straddles a seam, which is unrelated to
whether a *tile's own* position is on a different world-copy frame than
the viewport's canonical one. This wrongly skipped the offset search the
moment a user zoomed in tight to one side, away from the seam, even
though a tile could still need a shifted pass to be found — see the
antimeridian design doc's ["Locating and selecting a crossing tile in the
traversal"](specs/2026-05-27-antimeridian-crossing-tile-design.md#locating-and-selecting-a-crossing-tile-in-the-traversal)
for the regression this caused and the fix. The equivalent gate in
`MosaicTileset2D.getTileIndices` (`packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts`)
— a structurally different Flatbush/lng-lat-bbox search, not an OBB — was
changed the same way for the same reason.
- A tile is selected if it passes the frustum test at **any** offset.
- Selected tile indices are returned as `(x, y, z)` triples — the rendering
pipeline already handles drawing each tile in every visible world copy.
Expand Down Expand Up @@ -67,6 +80,18 @@ The traversal walks offset = ±1, ±2, ... until either:
Walk-until-empty terminates quickly in practice. The cap exists to bound
worst-case behavior at extreme zoom-outs and aspect ratios.

An antimeridian-crossing tile's two pieces (see the antimeridian design doc's
["Seam handling"](specs/2026-05-27-antimeridian-crossing-tile-design.md#seam-handling))
rely on this same offset search to be found and drawn when only one piece is
in view — each piece's mesh sits at its own natural common-space position
(west near `TILE_SIZE`, east near `0`) rather than a shared frame, so the
ordinary per-tile offset passes above are what place it correctly, no
antimeridian-specific traversal logic needed. This holds regardless of how
wide a piece is: each piece's corrected `x` is bounded by construction to
`[0, TILE_SIZE]` for any tile under the design doc's `MAX_TOTAL_SPAN_DEG`
self-overlap limit, so a wide piece never needs more offset passes than an
ordinary tile — `MAX_MAPS = 3` did not need to scale with piece width.

## Prior art

The pattern matches `@deck.gl/geo-layers`'s OSM tile traversal:
Expand Down
23 changes: 23 additions & 0 deletions examples/antimeridian-mosaic-multi-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Antimeridian: Mosaic + MultiCOG Example

Digital Earth Pacific GeoMAD composed through `MosaicLayer` + `MultiCOGLayer` (R/G/B band composite).

## Setup

1. Install dependencies from the repository root:
```bash
pnpm install
```

2. Build the packages:
```bash
pnpm build
```

3. Run the development server:
```bash
cd examples/antimeridian-mosaic-multi-example
pnpm dev
```

4. Open your browser to http://localhost:3000
22 changes: 22 additions & 0 deletions examples/antimeridian-mosaic-multi-example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Antimeridian Mosaic + MultiCOG Example</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
39 changes: 39 additions & 0 deletions examples/antimeridian-mosaic-multi-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "deck.gl-antimeridian-mosaic-multi-example",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"publish": "pnpm build && gh-pages -d dist -b gh-pages -e examples/antimeridian-mosaic-multi-example"
},
"dependencies": {
"@chakra-ui/react": "^3.34.0",
"@deck.gl/core": "^9.3.2",
"@deck.gl/geo-layers": "^9.3.2",
"@deck.gl/layers": "^9.3.2",
"@deck.gl/mapbox": "^9.3.2",
"@deck.gl/mesh-layers": "^9.3.2",
"@developmentseed/deck.gl-geotiff": "workspace:^",
"@developmentseed/deck.gl-raster": "workspace:^",
"@developmentseed/geotiff": "workspace:^",
"@emotion/react": "^11.14.0",
"@luma.gl/core": "^9.3.2",
"deck.gl-raster-examples-shared": "workspace:*",
"hyparquet": "^1.31.2",
"hyparquet-compressors": "^1.1.2",
"maplibre-gl": "^5.24.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-map-gl": "^8.1.1"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"gh-pages": "^6.3.0",
"vite": "^8.0.0"
}
}
102 changes: 102 additions & 0 deletions examples/antimeridian-mosaic-multi-example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Text } from "@chakra-ui/react";
import { PathLayer } from "@deck.gl/layers";
import { MosaicLayer, MultiCOGLayer } from "@developmentseed/deck.gl-geotiff";
import { LinearRescale } from "@developmentseed/deck.gl-raster/gpu-modules";
import type { DebugState } from "deck.gl-raster-examples-shared";
import {
ControlPanel,
DebugControls,
DeckGlOverlay,
ExternalLink,
} from "deck.gl-raster-examples-shared";
import "maplibre-gl/dist/maplibre-gl.css";
import { useEffect, useState } from "react";
import { Map as MaplibreMap } from "react-map-gl/maplibre";
import type { GeomadItem } from "./data.js";
import { fetchGeomadItems } from "./data.js";

export default function App() {
const [geomadItems, setGeomadItems] = useState<GeomadItem[]>([]);
const [debugState, setDebugState] = useState<DebugState>({
debug: false,
debugOpacity: 0.5,
debugLevel: 1,
});

useEffect(() => {
fetchGeomadItems().then(setGeomadItems);
}, []);

const mosaicLayer = new MosaicLayer({
id: "geomad-mosaic",
sources: geomadItems,
renderSource: (source) =>
new MultiCOGLayer({
id: `geomad-${source.id}`,
sources: {
red: { url: source.assets.red },
green: { url: source.assets.green },
blue: { url: source.assets.blue },
},
composite: { r: "red", g: "green", b: "blue" },
renderPipeline: [
{
module: LinearRescale,
props: { rescaleMin: 7200 / 65535, rescaleMax: 12000 / 65535 },
},
],
debug: debugState.debug,
debugOpacity: debugState.debugOpacity,
debugLevel: debugState.debugLevel,
}),
});

// Reference line at the antimeridian (±180°).
const antimeridianLine = new PathLayer({
id: "antimeridian-line",
data: [
[
[180, 85],
[180, -85],
],
],
getPath: (d: [number, number][]) => d,
getColor: [255, 0, 200, 255],
getWidth: 2,
widthUnits: "pixels",
});

return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<MaplibreMap
initialViewState={{
longitude: 180,
latitude: -16,
zoom: 5,
pitch: 0,
bearing: 0,
}}
mapStyle="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
>
<DeckGlOverlay layers={[mosaicLayer, antimeridianLine]} interleaved />
</MaplibreMap>

<ControlPanel
title="Antimeridian: Mosaic + MultiCOG"
sourcePath="examples/antimeridian-mosaic-multi-example"
>
<Text mb="2" color="gray.600">
Digital Earth Pacific GeoMAD composed through `MosaicLayer` +{" "}
`MultiCOGLayer` (R/G/B band composite).
</Text>
<Text mb="3" fontSize="xs" color="gray.600">
<ExternalLink href="https://digitalearthpacific.org">
Digital Earth Pacific
</ExternalLink>{" "}
Landsat GeoMAD mosaic.
</Text>
<DebugControls value={debugState} onChange={setDebugState} />
</ControlPanel>
</div>
);
}
23 changes: 23 additions & 0 deletions examples/antimeridian-mosaic-multi-example/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { fetchStacGeoparquetItems } from "./stac-geoparquet.js";

export type GeomadItem = {
id: string;
bbox: [number, number, number, number];
assets: { red: string; green: string; blue: string };
};

const PARQUET_URL =
"https://s3.us-west-2.amazonaws.com/dep-public-staging/dep_ls_geomad/0-3-1-test/dep_ls_geomad.parquet";

export async function fetchGeomadItems(): Promise<GeomadItem[]> {
const items = await fetchStacGeoparquetItems(PARQUET_URL);
return items.map(({ id, bbox, assets }) => ({
id,
bbox,
assets: {
red: assets.red!.href,
green: assets.green!.href,
blue: assets.blue!.href,
},
}));
}
12 changes: 12 additions & 0 deletions examples/antimeridian-mosaic-multi-example/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ExampleProvider } from "deck.gl-raster-examples-shared";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.js";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<ExampleProvider>
<App />
</ExampleProvider>
</StrictMode>,
);
33 changes: 33 additions & 0 deletions examples/antimeridian-mosaic-multi-example/src/stac-geoparquet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { asyncBufferFromUrl, parquetReadObjects } from "hyparquet";
import { compressors } from "hyparquet-compressors";

export type StacAssets = Record<string, { href: string }>;

export type StacGeoparquetItem = {
id: string;
bbox: [number, number, number, number];
assets: StacAssets;
};

export async function fetchStacGeoparquetItems(
url: string,
): Promise<StacGeoparquetItem[]> {
const file = await asyncBufferFromUrl({ url });
const rows = await parquetReadObjects({
file,
columns: ["id", "bbox", "assets"],
compressors,
});
return rows.map((row) => {
const { id, bbox, assets } = row as {
id: string;
bbox: { xmin: number; ymin: number; xmax: number; ymax: number };
assets: StacAssets;
};
return {
id,
bbox: [bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax],
assets,
};
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
4 changes: 4 additions & 0 deletions examples/antimeridian-mosaic-multi-example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.base.json",
"include": ["src"]
}
11 changes: 11 additions & 0 deletions examples/antimeridian-mosaic-multi-example/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [react()],
base: "/deck.gl-raster/examples/antimeridian-mosaic-multi-example/",
worker: { format: "es" },
server: {
port: 3000,
},
});
40 changes: 36 additions & 4 deletions examples/cog-basic/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NativeSelect, Text } from "@chakra-ui/react";
import { PathLayer } from "@deck.gl/layers";
import { COGLayer } from "@developmentseed/deck.gl-geotiff";
import type { DebugState } from "deck.gl-raster-examples-shared";
import {
Expand All @@ -18,9 +19,25 @@ const COG_OPTIONS: { title: string; url: string; attribution?: ReactNode }[] = [
{
// Dev-only fixture served by examples/cog-basic/vite.config.ts from the
// geotiff-test-data submodule. EPSG:4326, bbox (−204, −18, −162, 24);
// crosses native −180° at u ≈ 24/42 — exercises antimeridian split.
title: "Antimeridian fixture (dev only)",
url: "/__fixtures/antimeridian.tif",
// crosses native −180° at u ≈ 24/42 — exercises antimeridian unwrap + clip.
title: "Antimeridian fixture (dev only) (4326)",
url: `${window.location.origin}/__fixtures/antimeridian.tif`,
},
{
title: "Antimeridian fixture (dev only) (3832)",
url: "https://s3.us-west-2.amazonaws.com/dep-public-staging/dep_ls_geomad/0-3-1-test/066/022/2025/dep_ls_geomad_066_022_2025_red.tif",
},
{
title: "Antimeridian fixture (dev only) (UTM Zone 60)",
url: `${window.location.origin}/__fixtures/antimeridian_utm60.tif`,
},
{
title: "Antimeridian fixture (dev only) (4326 rotated)",
url: `${window.location.origin}/__fixtures/antimeridian_rotated.tif`,
},
{
title: "Antimeridian fixture (dev only) (360°)",
url: `${window.location.origin}/__fixtures/antimeridian_360.tif`,
},
{
title: "Sentinel-2 True Color Image (New York, 2026)",
Expand Down Expand Up @@ -97,6 +114,21 @@ const COG_OPTIONS: { title: string; url: string; attribution?: ReactNode }[] = [
},
];

// Reference line at the antimeridian (±180°).
const antimeridianLine = new PathLayer({
id: "antimeridian-line",
data: [
[
[180, 85],
[180, -85],
],
],
getPath: (d: [number, number][]) => d,
getColor: [255, 0, 200, 255],
getWidth: 2,
widthUnits: "pixels",
});

export default function App() {
const mapRef = useRef<MapRef>(null);
const [selectedIndex, setSelectedIndex] = useState(0);
Expand Down Expand Up @@ -141,7 +173,7 @@ export default function App() {
}}
mapStyle="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
>
<DeckGlOverlay layers={[cogLayer]} interleaved />
<DeckGlOverlay layers={[cogLayer, antimeridianLine]} interleaved />
</MaplibreMap>

<ControlPanel title="COGLayer Example" sourcePath="examples/cog-basic">
Expand Down
Loading
Loading