diff --git a/examples/cog-basic/src/App.tsx b/examples/cog-basic/src/App.tsx index ffaa06c..6a1a532 100644 --- a/examples/cog-basic/src/App.tsx +++ b/examples/cog-basic/src/App.tsx @@ -1,12 +1,11 @@ import type { DeckProps } from "@deck.gl/core"; import { MapboxOverlay } from "@deck.gl/mapbox"; import { COGLayer, proj } from "@developmentseed/deck.gl-geotiff"; -import type { GeoTIFF } from "geotiff"; -import { fromUrl, Pool } from "geotiff"; +import { Pool } from "geotiff"; import { toProj4 } from "geotiff-geokeys-to-proj4"; import "maplibre-gl/dist/maplibre-gl.css"; import proj4 from "proj4"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { Map, useControl, type MapRef } from "react-map-gl/maplibre"; window.proj4 = proj4; @@ -30,44 +29,6 @@ async function geoKeysParser( }; } -/** - * Calculate the WGS84 bounding box of a GeoTIFF image - */ -async function getCogBounds( - tiff: GeoTIFF, -): Promise<[[number, number], [number, number]]> { - const image = await tiff.getImage(); - const projectedBbox = image.getBoundingBox(); - const projDefinition = await geoKeysParser(image.getGeoKeys()); - - // Reproject to WGS84 (EPSG:4326) - const converter = proj4(projDefinition.def, "EPSG:4326"); - - // Reproject all four corners to handle rotation/skew - const [minX, minY, maxX, maxY] = projectedBbox; - const corners = [ - converter.forward([minX, minY]), // bottom-left - converter.forward([maxX, minY]), // bottom-right - converter.forward([maxX, maxY]), // top-right - converter.forward([minX, maxY]), // top-left - ]; - - // Find the bounding box that encompasses all reprojected corners - const lons = corners.map((c) => c[0]); - const lats = corners.map((c) => c[1]); - - const west = Math.min(...lons); - const south = Math.min(...lats); - const east = Math.max(...lons); - const north = Math.max(...lats); - - // Return bounds in MapLibre format: [[west, south], [east, north]] - return [ - [west, south], - [east, north], - ]; -} - // const COG_URL = // "https://nz-imagery.s3-ap-southeast-2.amazonaws.com/new-zealand/new-zealand_2024-2025_10m/rgb/2193/CC11.tiff"; @@ -76,65 +37,33 @@ const COG_URL = export default function App() { const mapRef = useRef(null); - const [geotiff, setGeotiff] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); const [debug, setDebug] = useState(false); const [debugOpacity, setDebugOpacity] = useState(0.25); const [pool] = useState(new Pool()); - useEffect(() => { - let mounted = true; - - async function loadGeoTIFF() { - try { - setLoading(true); - setError(null); - - const tiff = await fromUrl(COG_URL); - (window as any).tiff = tiff; - - if (mounted) { - setGeotiff(tiff); - - // Calculate bounds and fit to them - const bounds = await getCogBounds(tiff); - if (mapRef.current) { - mapRef.current.fitBounds(bounds, { - padding: 40, - duration: 1000, - }); - } - - setLoading(false); - } - } catch (err) { - if (mounted) { - setError( - err instanceof Error ? err.message : "Failed to load GeoTIFF", - ); - setLoading(false); - } - } - } - - loadGeoTIFF(); - - return () => { - mounted = false; - }; - }, []); - - const layers = geotiff + const layers = true ? [ new COGLayer({ id: "cog-layer", - geotiff, + geotiff: COG_URL, maxError: 0.125, debug, debugOpacity, geoKeysParser, pool, + onGeoTIFFLoad: (_tiff, options) => { + const { west, south, east, north } = options.geographicBounds; + mapRef.current?.fitBounds( + [ + [west, south], + [east, north], + ], + { + padding: 40, + duration: 1000, + }, + ); + }, beforeId: "aeroway-runway", }), ] @@ -168,43 +97,6 @@ export default function App() { zIndex: 1000, }} > - {loading && ( -
- Loading GeoTIFF... -
- )} - - {error && ( -
- Error: {error} -
- )} -
)}
- - {/*
-
Max Error: 0.125 pixels
-
Source: LINZ
-
*/} diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index 52499d6..c5345ac 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -24,7 +24,12 @@ import type { BaseClient, GeoTIFF, GeoTIFFImage, Pool } from "geotiff"; import proj4 from "proj4"; import { parseCOGTileMatrixSet } from "./cog-tile-matrix-set.js"; import { fromGeoTransform } from "./geotiff-reprojection.js"; -import { defaultPool, fetchGeoTIFF, loadRgbImage } from "./geotiff.js"; +import { + defaultPool, + fetchGeoTIFF, + getGeographicBounds, + loadRgbImage, +} from "./geotiff.js"; import type { GeoKeysParser, ProjectionInfo } from "./proj.js"; import { epsgIoGeoKeyParser } from "./proj.js"; @@ -144,7 +149,22 @@ export interface COGLayerProps< * @param {GeoTIFF} geotiff * @param {ProjectionInfo} projection */ - onGeoTIFFLoad?: (geotiff: GeoTIFF, projection: ProjectionInfo) => void; + onGeoTIFFLoad?: ( + geotiff: GeoTIFF, + options: { + projection: ProjectionInfo; + /** + * Bounds of the image in geographic coordinates (WGS84) [minLon, minLat, + * maxLon, maxLat] + */ + geographicBounds: { + west: number; + south: number; + east: number; + north: number; + }; + }, + ) => void; } const defaultProps: Partial = { @@ -209,14 +229,18 @@ export class COGLayer< ); } - this.props.onGeoTIFFLoad?.(geotiff, sourceProjection); - const converter = proj4(sourceProjection.def, "EPSG:4326"); const forwardReproject = (x: number, y: number) => converter.forward<[number, number]>([x, y], false); const inverseReproject = (x: number, y: number) => converter.inverse<[number, number]>([x, y], false); + const geographicBounds = getGeographicBounds(image, converter); + this.props.onGeoTIFFLoad?.(geotiff, { + projection: sourceProjection, + geographicBounds, + }); + this.setState({ metadata, forwardReproject, diff --git a/packages/deck.gl-geotiff/src/geotiff.ts b/packages/deck.gl-geotiff/src/geotiff.ts index b489aa2..d123a75 100644 --- a/packages/deck.gl-geotiff/src/geotiff.ts +++ b/packages/deck.gl-geotiff/src/geotiff.ts @@ -9,6 +9,7 @@ import { fromUrl, Pool, } from "geotiff"; +import { Converter } from "proj4"; /** * Options that may be passed when reading image data from geotiff.js @@ -158,3 +159,39 @@ export async function fetchGeoTIFF( return input; } + +/** + * Calculate the WGS84 bounding box of a GeoTIFF image + */ +export function getGeographicBounds( + image: GeoTIFFImage, + converter: Converter, +): { west: number; south: number; east: number; north: number } { + const projectedBbox = image.getBoundingBox() as [ + number, + number, + number, + number, + ]; + + // Reproject all four corners to handle rotation/skew + const [minX, minY, maxX, maxY] = projectedBbox; + const corners: [number, number][] = [ + converter.forward([minX, minY]), // bottom-left + converter.forward([maxX, minY]), // bottom-right + converter.forward([maxX, maxY]), // top-right + converter.forward([minX, maxY]), // top-left + ]; + + // Find the bounding box that encompasses all reprojected corners + const lons = corners.map((c) => c[0]); + const lats = corners.map((c) => c[1]); + + const west = Math.min(...lons); + const south = Math.min(...lats); + const east = Math.max(...lons); + const north = Math.max(...lats); + + // Return bounds in MapLibre format: [[west, south], [east, north]] + return { west, south, east, north }; +}