diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts index fbcbe026..0768d3da 100644 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts @@ -614,7 +614,6 @@ function getOverlappingChildRange( const childTileWidthCRS = tileWidth * cellSize; const childTileHeightCRS = tileHeight * cellSize; - // Note: we assume top left origin const originX = pointOfOrigin[0]; const originY = pointOfOrigin[1]; @@ -622,8 +621,18 @@ function getOverlappingChildRange( let minCol = Math.floor((pMinX - originX) / childTileWidthCRS); let maxCol = Math.floor((pMaxX - originX) / childTileWidthCRS); - let minRow = Math.floor((originY - pMaxY) / childTileHeightCRS); - let maxRow = Math.floor((originY - pMinY) / childTileHeightCRS); + // Row direction depends on cornerOfOrigin: + // - topLeft: row 0 is at originY (max Y), rows increase downward (lower Y) + // - bottomLeft: row 0 is at originY (min Y), rows increase upward (higher Y) + let minRow: number; + let maxRow: number; + if (childMatrix.cornerOfOrigin === "bottomLeft") { + minRow = Math.floor((pMinY - originY) / childTileHeightCRS); + maxRow = Math.floor((pMaxY - originY) / childTileHeightCRS); + } else { + minRow = Math.floor((originY - pMaxY) / childTileHeightCRS); + maxRow = Math.floor((originY - pMinY) / childTileHeightCRS); + } // Clamp to matrix bounds minCol = Math.max(0, Math.min(matrixWidth - 1, minCol)); diff --git a/packages/geotiff/src/tile-matrix-set.ts b/packages/geotiff/src/tile-matrix-set.ts index 7d92c97f..9b50299e 100644 --- a/packages/geotiff/src/tile-matrix-set.ts +++ b/packages/geotiff/src/tile-matrix-set.ts @@ -119,13 +119,20 @@ export function generateTileMatrixSet( for (let idx = 0; idx < overviewsCoarseFirst.length; idx++) { const overview = overviewsCoarseFirst[idx]!; const { x: matrixWidth, y: matrixHeight } = overview.tileCount; + // Clamp tile dimensions to the actual image size. When an overview is + // smaller than the tile block size (e.g. a 1×1 pixel image with a 1024 + // block), using the raw block size as tileWidth would make tileTransform + // project pixel (1024,1024) to a CRS coordinate far outside the image + // extent, causing proj4 to return null for out-of-range inputs. + const tileWidth = Math.min(overview.tileWidth, overview.width); + const tileHeight = Math.min(overview.tileHeight, overview.height); tileMatrices.push( buildTileMatrix( String(idx), overview.transform, mpu, - overview.tileWidth, - overview.tileHeight, + tileWidth, + tileHeight, matrixWidth, matrixHeight, ), @@ -146,8 +153,8 @@ export function generateTileMatrixSet( String(geotiff.overviews.length), geotiff.transform, mpu, - geotiff.tileWidth, - geotiff.tileHeight, + Math.min(geotiff.tileWidth, geotiff.width), + Math.min(geotiff.tileHeight, geotiff.height), matrixWidth, matrixHeight, ), diff --git a/packages/geotiff/tests/integration.test.ts b/packages/geotiff/tests/integration.test.ts index ba7474ed..512fda3a 100644 --- a/packages/geotiff/tests/integration.test.ts +++ b/packages/geotiff/tests/integration.test.ts @@ -9,11 +9,11 @@ * are intentionally omitted here. */ +import type { GeoTIFF } from "@developmentseed/geotiff"; import type { GeoTIFFImage, GeoTIFF as GeotiffJs } from "geotiff"; import { fromFile } from "geotiff"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { toBandSeparate } from "../src/array.js"; -import type { GeoTIFF } from "../src/geotiff.js"; import { fixturePath, loadGeoTIFF } from "./helpers.js"; const FIXTURES = [ @@ -167,3 +167,42 @@ describe("boundless=false edge tile pixel values", () => { } }); }); + +describe("flipped y (positive Y resolution / bottom-left origin)", () => { + let ours: GeoTIFF; + let ref: GeotiffJs; + let refImage: GeoTIFFImage; + + beforeAll(async () => { + ours = await loadGeoTIFF( + "xjejfvrbm1fbu1ecw-0000000000-0000008192", + "source-coop-alpha-earth", + ); + ref = await loadGeoTiffJs( + "xjejfvrbm1fbu1ecw-0000000000-0000008192", + "source-coop-alpha-earth", + ); + refImage = await ref.getImage(); + }); + + afterAll(() => ref.close()); + + it("transform has positive Y resolution (south-up file)", () => { + const [, , , , e] = ours.transform; + expect(e).toBeGreaterThan(0); + }); + + it("bbox matches geotiff.js getBoundingBox", () => { + // geotiff.js getBoundingBox returns [minX, minY, maxX, maxY] + const [minX, minY, maxX, maxY] = refImage.getBoundingBox(); + expect(ours.bbox[0]).toBeCloseTo(minX!, 3); + expect(ours.bbox[1]).toBeCloseTo(minY!, 3); + expect(ours.bbox[2]).toBeCloseTo(maxX!, 3); + expect(ours.bbox[3]).toBeCloseTo(maxY!, 3); + }); + + it("bbox minY < maxY (Y increases upward)", () => { + const [, minY, , maxY] = ours.bbox; + expect(minY).toBeLessThan(maxY); + }); +});