Skip to content

fix(mosaic): Make MosaicLayer repeat over world copies (antimeridian) - #639

Open
willjnz wants to merge 1 commit into
developmentseed:mainfrom
willjnz:mosaic-repeat-antimeridian
Open

fix(mosaic): Make MosaicLayer repeat over world copies (antimeridian)#639
willjnz wants to merge 1 commit into
developmentseed:mainfrom
willjnz:mosaic-repeat-antimeridian

Conversation

@willjnz

@willjnz willjnz commented Aug 21, 2026

Copy link
Copy Markdown

Hey @kylebarron, great work - thank you very much.

I tried the beta v0.8 because I needed what I thought was fixed by #518. It turns out that fix doesn't make MosaicLayer repeat across world copies. This PR applies the same fix to MosaicLayer.

There are two independent layers of "which things are visible" in this codebase:

  1. Inside an already-open COG: RasterTileLayer decides which pixel tiles (x, y, z) of that one file are visible. Fixed by fix: traverse tiles across world copies (#517) #518.
  2. One level up: MosaicLayer's MosaicTileset2D decides which sources (whole COGs - e.g. STAC items) are visible at all, via a spatial query over their bounding boxes, before any RasterTileLayer for that source exists.

#518 only fixed layer 1. If a source is never selected at layer 2, it never gets a RasterTileLayer built for it (renderSource never runs), so #518's fix never gets a chance to run.

The root cause: MosaicTileset2D.getTileIndices queried its Flatbush spatial index once, using the viewport's raw (possibly unwrapped) longitude bounds. Once a WebMercatorViewport with repeat: true pans past ±180°, viewportBounds extends past that range while sources are still indexed at their true, unwrapped bbox - so items on the far side of the antimeridian were never selected.

The fix repeats the query at world-copy offsets - same subViewports.length > 1 gate and MAX_MAPS walk-until-empty pattern as raster-tile-traversal.ts - shifting the search bounds by worldOffset * 360° of longitude instead of translating a bounding volume, since Flatbush operates on WGS84 degrees, not deck.gl common space. Documented as a second, independent layer in dev-docs/world-copies.md.

Test example

Before (v0.8.0-beta.2) After (this PR)
before after 1
after 2

Note: there is still a gap right at the antimeridian, because COGs with bounds of -180 to +180 (antimeridian-spanning) are excluded - a known separate issue, unrelated to this fix. The geomad layer has some COGs that render badly before and after this PR for that reason; LULC does not.

Minimal app to test

import { COGLayer, MosaicLayer } from "@developmentseed/deck.gl-geotiff";
import type { StyleSpecification } from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { Map as MaplibreMap } from "react-map-gl/maplibre";
import { DeckGLOverlay } from "./deckgl-overlay.js";

type LulcSource = { id: string; bbox: [number, number, number, number]; href: string };

// One COG on each side of the antimeridian (dep_ls_lulc STAC collection).
const SOURCES: LulcSource[] = [
  {
    id: "dep_ls_lulc_063_019_2025",
    bbox: [177.380649859963, -19.29852907893266, 178.24303253271776, -18.4776694259545],
    href: "https://s3.us-west-2.amazonaws.com/dep-public-staging/dep_ls_lulc/0-0-9/063/019/2025/dep_ls_lulc_063_019_2025_classification.tif",
  },
  {
    id: "dep_ls_lulc_067_019_2025",
    bbox: [-179.169819449018, -19.29852907893266, -178.30743677626327, -18.4776694259545],
    href: "https://s3.us-west-2.amazonaws.com/dep-public-staging/dep_ls_lulc/0-0-9/067/019/2025/dep_ls_lulc_067_019_2025_classification.tif",
  },
];

const BASEMAP_STYLE: StyleSpecification = {
  version: 8,
  sources: {
    osm: {
      type: "raster",
      tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
      tileSize: 256,
      attribution: "© OpenStreetMap contributors",
    },
  },
  layers: [{ id: "osm", type: "raster", source: "osm" }],
};

const layer = new MosaicLayer<LulcSource>({
  id: "lulc-mosaic",
  sources: SOURCES,
  renderSource: (source) => new COGLayer({ id: `lulc-${source.id}`, geotiff: source.href }),
});

export default function App() {
  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <MaplibreMap
        initialViewState={{ longitude: 180, latitude: -18.9, zoom: 7 }}
        mapStyle={BASEMAP_STYLE}
        renderWorldCopies={true}
      >
        <DeckGLOverlay layers={[layer]} />
      </MaplibreMap>
    </div>
  );
}
Before (v0.8.0-beta.2) After (this PR)
before after

@willjnz willjnz changed the title Make MosaicLayer repeat over world copies (antimeridian) fix(mosaic): repeat MosaicTileset2D spatial search across world copies Aug 21, 2026
@github-actions github-actions Bot added the fix label Aug 21, 2026
@willjnz willjnz changed the title fix(mosaic): repeat MosaicTileset2D spatial search across world copies fix(mosaic): Make MosaicLayer repeat over world copies (antimeridian) Aug 21, 2026
MosaicTileset2D.getTileIndices queried its Flatbush spatial index once,
using the viewport's raw (possibly unwrapped) longitude bounds. Once a
WebMercatorViewport with repeat: true pans past +-180 degrees,
viewportBounds extends past that range while sources are still indexed
at their true, unwrapped bbox, so items on the far side of the
antimeridian were never selected -- RasterTileLayer's own world-copy
traversal (developmentseed#517 / developmentseed#518) never gets a chance to run, since a source's
layer is never built if it's never selected.

Repeats the query at world-copy offsets (same subViewports.length > 1
gate and MAX_MAPS walk-until-empty pattern as raster-tile-traversal.ts)
shifting the search bounds by worldOffset * 360 degrees of longitude
instead of translating a bounding volume, since Flatbush operates on
WGS84 degrees rather than deck.gl common space. Documented as a second,
independent layer in dev-docs/world-copies.md.
@willjnz
willjnz force-pushed the mosaic-repeat-antimeridian branch from 065152b to 66568f0 Compare August 25, 2026 05:18
@kylebarron

Copy link
Copy Markdown
Member

Thanks @willjnz; this looks reasonable but I'll need to take a closer look before merging. I'm focused on creating presentations for FOSS4G at the moment

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.

2 participants