diff --git a/cog-tile-testing.ipynb b/cog-tile-testing.ipynb new file mode 100644 index 00000000..8fd31fc1 --- /dev/null +++ b/cog-tile-testing.ipynb @@ -0,0 +1,166 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 8, + "id": "66285659-d958-4be6-ae62-b3b6efdad127", + "metadata": {}, + "outputs": [], + "source": [ + "from lonboard import Map\n", + "from lonboard.experimental._surface import COGTileLayer" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "49ea9e5f-ee08-4924-b968-2e18c058c876", + "metadata": {}, + "outputs": [], + "source": [ + "url = \"https://ds-wheels.s3.us-east-1.amazonaws.com/m_4007307_sw_18_060_20220803.tif\"\n", + "layer = COGTileLayer(data=url)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b890fe4a-6291-4dd6-b62a-20581e22404d", + "metadata": {}, + "outputs": [], + "source": [ + "view_state = {'longitude': -73.20972420038129,\n", + " 'latitude': 40.90307721701271,\n", + " 'zoom': 10.98785761041711}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "21aa3c2f-e8d3-4a92-acf1-2c1af8451de7", + "metadata": {}, + "outputs": [], + "source": [ + "m = Map(layer, view_state=view_state)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cf976d1f-4264-44b5-bed6-fbd1ae8c9811", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "087f29c18787446591cd6ec85ef49a59", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "Map(basemap=MaplibreBasemap(), controls=(FullscreenControl(), NavigationControl(), ScaleControl()), custom_att…" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5f559dda-d832-40d5-a746-340944ef64e3", + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import asdict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9787bfa0-0f7f-4ee4-a6ff-c674acdb9451", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "7eb7fc24-ef48-43de-a4dd-7686dfeab320", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'longitude': -73.20972420038129,\n", + " 'latitude': 40.90307721701271,\n", + " 'zoom': 10.98785761041711,\n", + " 'pitch': 0,\n", + " 'bearing': 0,\n", + " 'max_zoom': 20,\n", + " 'min_zoom': 0,\n", + " 'max_pitch': 60,\n", + " 'min_pitch': 0}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "asdict(m.view_state)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f07a3d8-e045-42ba-840e-58ae844d643f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5349bab1-6542-41ce-88fc-413e559253f6", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f7961e7-8bdd-429c-9bb9-9de33e03405a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "lonboard", + "language": "python", + "name": "lonboard" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lonboard/experimental/_surface.py b/lonboard/experimental/_surface.py index c744926b..978eb2cc 100644 --- a/lonboard/experimental/_surface.py +++ b/lonboard/experimental/_surface.py @@ -228,3 +228,11 @@ def from_rasterio( - Type: `bool`, optional - Default: `False` """ + + +class COGTileLayer(BaseLayer): + """COGTileLayer.""" + + _layer_type = t.Unicode("cog-tile").tag(sync=True) + + data = t.Unicode().tag(sync=True) diff --git a/package-lock.json b/package-lock.json index a4f364ff..be9a4e7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,13 @@ "apache-arrow": "^21.1.0", "esbuild-sass-plugin": "^3.3.1", "framer-motion": "^12.23.19", + "geotiff": "^2.1.4-beta.0", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "maplibre-gl": "^5.9.0", "memoize-one": "^6.0.0", "parquet-wasm": "0.7.1", + "proj4": "^2.19.10", "react": "^19.2.0", "react-dom": "^19.2.0", "react-map-gl": "^8.1.0", @@ -104,8 +106,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz", "integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "2.0.5", @@ -219,6 +220,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -242,6 +244,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1356,6 +1359,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.2.tgz", "integrity": "sha512-ZvCV8kcC730t62Q+iiCn8SOPgDCEyvV6i/GvIXf59Rnyl8pRvo7wcbl5zorbIEFng+a+EcYv/tEAZcAkwe1oEA==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/core": "^4.2.0", "@loaders.gl/images": "^4.2.0", @@ -1381,6 +1385,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.2.tgz", "integrity": "sha512-DwbUai9Gm3wdKUj7rflMeOTsBHXlvm5Zah10qNHehv4TNgt8iZ1FFvbZoTi5ocj8s5wo0JdyLQuT98r5LGplrw==", "license": "MIT", + "peer": true, "dependencies": { "@luma.gl/constants": "^9.2.2", "@luma.gl/shadertools": "^9.2.2", @@ -1432,6 +1437,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/layers/-/layers-9.2.2.tgz", "integrity": "sha512-O1tmE6I7KQs3Wv2W+KkLo3mOW6CqWn92jwkCq7i5b7OZwxNfzdEQFK3bDpQTfDEp1BTwR6bMP1TkhGBVksQJ2Q==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/images": "^4.2.0", "@loaders.gl/schema": "^4.2.0", @@ -1470,6 +1476,7 @@ "resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.2.tgz", "integrity": "sha512-OhH11zy2yyETXY6rwinJnaKk3eTzAk4BHH31ZltK+DI4XHBUVPpXHIyI9soPsI06TObfdx54LgLxdA/PIDdscg==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/gltf": "^4.2.0", "@loaders.gl/schema": "^4.2.0", @@ -2460,7 +2467,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/3d-tiles/-/3d-tiles-4.3.4.tgz", "integrity": "sha512-JQ3y3p/KlZP7lfobwON5t7H9WinXEYTvuo3SRQM8TBKhM+koEYZhvI2GwzoXx54MbBbY+s3fm1dq5UAAmaTsZw==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/compression": "4.3.4", "@loaders.gl/crypto": "4.3.4", @@ -2485,15 +2491,13 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@loaders.gl/compression": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/compression/-/compression-4.3.4.tgz", "integrity": "sha512-+o+5JqL9Sx8UCwdc2MTtjQiUHYQGJALHbYY/3CT+b9g/Emzwzez2Ggk9U9waRfdHiBCzEgRBivpWZEOAtkimXQ==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/worker-utils": "4.3.4", @@ -2518,6 +2522,7 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz", "integrity": "sha512-cG0C5fMZ1jyW6WCsf4LoHGvaIAJCEVA/ioqKoYRwoSfXkOf+17KupK1OUQyUCw5XoRn+oWA1FulJQOYlXnb9Gw==", "license": "MIT", + "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -2530,7 +2535,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/crypto/-/crypto-4.3.4.tgz", "integrity": "sha512-3VS5FgB44nLOlAB9Q82VOQnT1IltwfRa1miE0mpHCe1prYu1M/dMnEyynusbrsp+eDs3EKbxpguIS9HUsFu5dQ==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/worker-utils": "4.3.4", @@ -2560,7 +2564,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/gis/-/gis-4.3.4.tgz", "integrity": "sha512-8xub38lSWW7+ZXWuUcggk7agRHJUy6RdipLNKZ90eE0ZzLNGDstGD1qiBwkvqH0AkG+uz4B7Kkiptyl7w2Oa6g==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -2621,7 +2624,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/math/-/math-4.3.4.tgz", "integrity": "sha512-UJrlHys1fp9EUO4UMnqTCqvKvUjJVCbYZ2qAKD7tdGzHJYT8w/nsP7f/ZOYFc//JlfC3nq+5ogvmdpq2pyu3TA==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2636,7 +2638,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/mvt/-/mvt-4.3.4.tgz", "integrity": "sha512-9DrJX8RQf14htNtxsPIYvTso5dUce9WaJCWCIY/79KYE80Be6dhcEYMknxBS4w3+PAuImaAe66S5xo9B7Erm5A==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/gis": "4.3.4", "@loaders.gl/images": "4.3.4", @@ -2667,7 +2668,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/terrain/-/terrain-4.3.4.tgz", "integrity": "sha512-JszbRJGnxL5Fh82uA2U8HgjlsIpzYoCNNjy3cFsgCaxi4/dvjz3BkLlBilR7JlbX8Ka+zlb4GAbDDChiXLMJ/g==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2701,7 +2701,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/tiles/-/tiles-4.3.4.tgz", "integrity": "sha512-oC0zJfyvGox6Ag9ABF8fxOkx9yEFVyzTa9ryHXl2BqLiQoR1v3p+0tIJcEbh5cnzHfoTZzUis1TEAZluPRsHBQ==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/math": "4.3.4", @@ -2720,7 +2719,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/wms/-/wms-4.3.4.tgz", "integrity": "sha512-yXF0wuYzJUdzAJQrhLIua6DnjOiBJusaY1j8gpvuH1VYs3mzvWlIRuZKeUd9mduQZKK88H2IzHZbj2RGOauq4w==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/images": "4.3.4", "@loaders.gl/loader-utils": "4.3.4", @@ -2747,7 +2745,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/xml/-/xml-4.3.4.tgz", "integrity": "sha512-p+y/KskajsvyM3a01BwUgjons/j/dUhniqd5y1p6keLOuwoHlY/TfTKd+XluqfyP14vFrdAHCZTnFCWLblN10w==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/loader-utils": "4.3.4", "@loaders.gl/schema": "4.3.4", @@ -2762,7 +2759,6 @@ "resolved": "https://registry.npmjs.org/@loaders.gl/zip/-/zip-4.3.4.tgz", "integrity": "sha512-bHY4XdKYJm3vl9087GMoxnUqSURwTxPPh6DlAGOmz6X9Mp3JyWuA2gk3tQ1UIuInfjXKph3WAUfGe6XRIs1sfw==", "license": "MIT", - "peer": true, "dependencies": { "@loaders.gl/compression": "4.3.4", "@loaders.gl/crypto": "4.3.4", @@ -2778,13 +2774,15 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.2.tgz", "integrity": "sha512-XURMF0gSh0ImZltYa/PCe9KgmopQJiOA6y1m1PxDxJY8OCLma7ZJyvomLn7TQBvPtWTYZsibTW7blu7RwThsaQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@luma.gl/core": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.2.2.tgz", "integrity": "sha512-X63BnXXDlC9AmoG4sUVsfxLn+DoNovbX/z5ZXxnhpxx47536Ss/SLzwnLvm/ZoDhK9/s5qdI95mSZKuqzKCkjw==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/types": "^4.1.0", "@probe.gl/env": "^4.0.8", @@ -2798,6 +2796,7 @@ "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.2.2.tgz", "integrity": "sha512-Upq/jVPvgi/rjwgSGYyW+jobJBotKR/aNTDwyHAubx6wXWluZqnR0ZBwctiO9i7w2RIzZGboMYs4dIgVw0ULaQ==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", @@ -2832,6 +2831,7 @@ "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.2.2.tgz", "integrity": "sha512-ChskCXE8Q+5/rC8zPR7pHBSfERGRui5qvw6bZnOZMVwTvGbW5tI5od5Wu9ytGi45kWus66M+M/o5LpP3hfc4Hg==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/core": "^4.1.0", "@math.gl/types": "^4.1.0", @@ -3029,15 +3029,13 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@mapbox/martini/-/martini-0.2.0.tgz", "integrity": "sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/@mapbox/tiny-sdf": { "version": "2.0.7", @@ -3056,7 +3054,6 @@ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/point-geometry": "~0.1.0" } @@ -3148,7 +3145,6 @@ "resolved": "https://registry.npmjs.org/@math.gl/culling/-/culling-4.1.0.tgz", "integrity": "sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg==", "license": "MIT", - "peer": true, "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" @@ -3159,7 +3155,6 @@ "resolved": "https://registry.npmjs.org/@math.gl/geospatial/-/geospatial-4.1.0.tgz", "integrity": "sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg==", "license": "MIT", - "peer": true, "dependencies": { "@math.gl/core": "4.1.0", "@math.gl/types": "4.1.0" @@ -3170,6 +3165,7 @@ "resolved": "https://registry.npmjs.org/@math.gl/polygon/-/polygon-4.1.0.tgz", "integrity": "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==", "license": "MIT", + "peer": true, "dependencies": { "@math.gl/core": "4.1.0" } @@ -4327,6 +4323,7 @@ "resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.4.6.tgz", "integrity": "sha512-6ujAriBZMfQ16n6M6Ad9g32KJUa1CzqIVaHN/tymadr/3m8hrr7xDw6z50pVjpCRq2PaaA1hT8Hx7EFU3f2z3Q==", "license": "MIT", + "peer": true, "dependencies": { "@internationalized/date": "3.6.0", "@nextui-org/react-utils": "2.1.3", @@ -4421,6 +4418,7 @@ "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.4.5.tgz", "integrity": "sha512-c7Y17n+hBGiFedxMKfg7Qyv93iY5MteamLXV4Po4c1VF1qZJI6I+IKULFh3FxPWzAoz96r6NdYT7OLFjrAJdWg==", "license": "MIT", + "peer": true, "dependencies": { "@nextui-org/shared-utils": "2.1.2", "clsx": "^1.2.1", @@ -5079,6 +5077,12 @@ "node": ">=0.10" } }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7386,7 +7390,6 @@ "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-5.1.5.tgz", "integrity": "sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA==", "license": "MIT", - "peer": true, "dependencies": { "@turf/helpers": "^5.1.5", "@turf/invariant": "^5.1.5" @@ -7397,7 +7400,6 @@ "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-5.1.5.tgz", "integrity": "sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg==", "license": "MIT", - "peer": true, "dependencies": { "@turf/helpers": "^5.1.5" } @@ -7406,15 +7408,13 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@turf/invariant": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", "integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==", "license": "MIT", - "peer": true, "dependencies": { "@turf/helpers": "^5.1.5" } @@ -7424,7 +7424,6 @@ "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", "integrity": "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==", "license": "MIT", - "peer": true, "dependencies": { "@turf/helpers": "^5.1.5" } @@ -7434,7 +7433,6 @@ "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-5.1.5.tgz", "integrity": "sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw==", "license": "MIT", - "peer": true, "dependencies": { "@turf/boolean-clockwise": "^5.1.5", "@turf/clone": "^5.1.5", @@ -7459,7 +7457,6 @@ "resolved": "https://registry.npmjs.org/@types/brotli/-/brotli-1.3.4.tgz", "integrity": "sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -7480,8 +7477,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", @@ -7573,14 +7569,14 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7664,6 +7660,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -7973,7 +7970,6 @@ "resolved": "https://registry.npmjs.org/a5-js/-/a5-js-0.5.0.tgz", "integrity": "sha512-VAw19sWdYadhdovb0ViOIi1SdKx6H6LwcGMRFKwMfgL5gcmL/1fKJHfgsNgNaJ7xC/eEyjs6VK+VVd4N0a+peg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "gl-matrix": "^3.4.3" } @@ -7984,6 +7980,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8069,6 +8066,7 @@ "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.1.0.tgz", "integrity": "sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", @@ -8383,8 +8381,7 @@ } ], "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/baseline-browser-mapping": { "version": "2.8.20", @@ -8437,7 +8434,6 @@ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "base64-js": "^1.1.2" } @@ -8462,6 +8458,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -8481,7 +8478,6 @@ "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", "integrity": "sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8490,8 +8486,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", - "license": "MIT/X11", - "peer": true + "license": "MIT/X11" }, "node_modules/bytewise": { "version": "1.1.0", @@ -8637,7 +8632,6 @@ "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": "*" } @@ -8738,8 +8732,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/command-line-args": { "version": "6.0.1", @@ -8845,7 +8838,6 @@ "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", "integrity": "sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw==", "license": "MIT", - "peer": true, "dependencies": { "buf-compare": "^1.0.0", "is-error": "^2.2.0" @@ -8858,8 +8850,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -8880,7 +8871,6 @@ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": "*" } @@ -9001,8 +8991,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", "integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -9093,7 +9082,6 @@ "resolved": "https://registry.npmjs.org/deep-strict-equal/-/deep-strict-equal-0.2.0.tgz", "integrity": "sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA==", "license": "MIT", - "peer": true, "dependencies": { "core-assert": "^0.2.0" }, @@ -9437,6 +9425,7 @@ "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9516,6 +9505,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9968,7 +9958,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "strnum": "^1.1.1" }, @@ -9989,8 +9978,7 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -10138,6 +10126,7 @@ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", "license": "MIT", + "peer": true, "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", @@ -10240,6 +10229,31 @@ "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", "license": "ISC" }, + "node_modules/geotiff": { + "version": "2.1.4-beta.0", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.4-beta.0.tgz", + "integrity": "sha512-jb6SYvHMyiCqwqgGGLDAxtig9h1g6O+n1wEyNEE4QgVEXOItYaWrEgPg9SAnwdoZm2yx6DpFtilbGG65hvZgpQ==", + "license": "MIT", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.5.0", + "xml-utils": "^1.10.2", + "zstddec": "^0.2.0-alpha.3" + }, + "engines": { + "node": ">=10.19" + } + }, + "node_modules/geotiff/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -10442,7 +10456,6 @@ "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.3.0.tgz", "integrity": "sha512-zgvyHZz5bEKeuyYGh0bF9/kYSxJ2SqroopkXHqKnD3lfjaZawcxulcI9nWbNC54gakl/2eObRLHWueTf1iLSaA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=4", "npm": ">=3", @@ -10572,8 +10585,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -10608,8 +10620,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/immutable": { "version": "5.1.4", @@ -10648,8 +10659,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/input-otp": { "version": "1.4.1", @@ -10781,8 +10791,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-callable": { "version": "1.2.7", @@ -10851,8 +10860,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz", "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-extendable": { "version": "0.1.1", @@ -11167,8 +11175,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -11380,7 +11387,6 @@ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "license": "(MIT OR GPL-3.0-or-later)", - "peer": true, "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -11410,6 +11416,12 @@ "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==", "license": "MIT" }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11451,7 +11463,6 @@ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", - "peer": true, "dependencies": { "immediate": "~3.0.5" } @@ -11805,7 +11816,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=0.6" } @@ -11834,15 +11844,13 @@ "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==", "license": "ISC", - "optional": true, - "peer": true + "optional": true }, "node_modules/lzo-wasm": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/lzo-wasm/-/lzo-wasm-0.0.4.tgz", "integrity": "sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/magic-string": { "version": "0.30.21", @@ -11941,7 +11949,6 @@ "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -12395,8 +12402,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)", - "peer": true + "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", @@ -12417,6 +12423,12 @@ "integrity": "sha512-fjEGpMApzt3mpI2pUxdRgQGu5G+s4nr0vm5xn43JO7jxdYzzu2fHrVrTHtfeEhtB6vfvTzJBz0WydDYzLWvszQ==", "license": "MIT OR Apache-2.0" }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, "node_modules/partysocket": { "version": "0.0.25", "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-0.0.25.tgz", @@ -12480,7 +12492,6 @@ "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -12554,6 +12565,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13477,6 +13489,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13502,7 +13515,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -13538,8 +13550,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/proj4": { "version": "2.19.10", @@ -13623,6 +13634,18 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/quickselect": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", @@ -13634,6 +13657,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13643,6 +13667,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13712,7 +13737,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -13879,7 +13903,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13915,8 +13938,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safe-identifier": { "version": "0.4.2", @@ -14001,7 +14023,6 @@ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.2.tgz", "integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==", "license": "MIT", - "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", @@ -14051,7 +14072,6 @@ ], "license": "MIT", "optional": true, - "peer": true, "dependencies": { "sass": "1.93.2" } @@ -14068,7 +14088,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14085,7 +14104,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14102,7 +14120,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14119,7 +14136,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14136,7 +14152,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14153,7 +14168,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14170,7 +14184,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14187,7 +14200,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14204,7 +14216,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14221,7 +14232,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14238,7 +14248,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14255,7 +14264,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14272,7 +14280,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14289,7 +14296,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14306,7 +14312,6 @@ "!linux", "!win32" ], - "peer": true, "dependencies": { "sass": "1.93.2" } @@ -14323,7 +14328,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14340,7 +14344,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=14.0.0" } @@ -14350,7 +14353,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14482,8 +14484,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -14633,8 +14634,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.6.1.tgz", "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/sort-asc": { "version": "0.2.0", @@ -14742,7 +14742,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -14981,8 +14980,7 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/sucrase": { "version": "3.35.0", @@ -15057,7 +15055,6 @@ "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", "license": "MIT", - "peer": true, "dependencies": { "sync-message-port": "^1.0.0" }, @@ -15070,7 +15067,6 @@ "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.0.0" } @@ -15444,6 +15440,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15721,8 +15718,13 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" }, "node_modules/wgsl_reflect": { "version": "1.2.3", @@ -15960,6 +15962,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -15976,11 +15979,18 @@ } } }, + "node_modules/xml-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", + "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==", + "license": "CC0-1.0" + }, "node_modules/xstate": { "version": "5.23.0", "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.23.0.tgz", "integrity": "sha512-jo126xWXkU6ySQ91n51+H2xcgnMuZcCQpQoD3FQ79d32a6RQvryRh8rrDHnH4WDdN/yg5xNjlIRol9ispMvzeg==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" @@ -16013,6 +16023,7 @@ "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lib0": "^0.2.99" }, @@ -16043,8 +16054,13 @@ "resolved": "https://registry.npmjs.org/zstd-codec/-/zstd-codec-0.1.5.tgz", "integrity": "sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==", "license": "MIT", - "optional": true, - "peer": true + "optional": true + }, + "node_modules/zstddec": { + "version": "0.2.0-alpha.3", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0-alpha.3.tgz", + "integrity": "sha512-uHyE3TN8jRFOaMVwdhERfrcaabyoUUawIRDKXE6x0nCU7mzyIZO0LndJ3AtVUiKLF0lC+8F5bMSySWEF586PSA==", + "license": "MIT AND BSD-3-Clause" } } } diff --git a/package.json b/package.json index 380a2e05..3963dc62 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "apache-arrow": "^21.1.0", "esbuild-sass-plugin": "^3.3.1", "framer-motion": "^12.23.19", + "geotiff": "^2.1.4-beta.0", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "maplibre-gl": "^5.9.0", "memoize-one": "^6.0.0", "parquet-wasm": "0.7.1", + "proj4": "^2.19.10", "react": "^19.2.0", "react-dom": "^19.2.0", "react-map-gl": "^8.1.0", diff --git a/src/cog-tileset/claude-tileset-2d-improved.ts b/src/cog-tileset/claude-tileset-2d-improved.ts new file mode 100644 index 00000000..fdbcd2e7 --- /dev/null +++ b/src/cog-tileset/claude-tileset-2d-improved.ts @@ -0,0 +1,391 @@ +/** + * COGTileset2D - Improved Implementation with Frustum Culling + * + * This version properly implements frustum culling and bounding volume calculations + * following the pattern from deck.gl's OSM tile indexing. + */ + +import { Viewport, WebMercatorViewport } from "@deck.gl/core"; +import { _Tileset2D as Tileset2D } from "@deck.gl/geo-layers"; +import type { Tileset2DProps } from "@deck.gl/geo-layers/dist/tileset-2d"; +import type { ZRange } from "@deck.gl/geo-layers/dist/tileset-2d/types"; +import { Matrix4 } from "@math.gl/core"; +import { GeoTIFF, GeoTIFFImage } from "geotiff"; +import proj4 from "proj4"; + +import { getTileIndices } from "./cog-tile-2d-traversal"; +import type { COGMetadata, COGTileIndex, COGOverview, Bounds } from "./types"; + +const OGC_84 = { + $schema: "https://proj.org/schemas/v0.7/projjson.schema.json", + type: "GeographicCRS", + name: "WGS 84 (CRS84)", + datum_ensemble: { + name: "World Geodetic System 1984 ensemble", + members: [ + { + name: "World Geodetic System 1984 (Transit)", + id: { authority: "EPSG", code: 1166 }, + }, + { + name: "World Geodetic System 1984 (G730)", + id: { authority: "EPSG", code: 1152 }, + }, + { + name: "World Geodetic System 1984 (G873)", + id: { authority: "EPSG", code: 1153 }, + }, + { + name: "World Geodetic System 1984 (G1150)", + id: { authority: "EPSG", code: 1154 }, + }, + { + name: "World Geodetic System 1984 (G1674)", + id: { authority: "EPSG", code: 1155 }, + }, + { + name: "World Geodetic System 1984 (G1762)", + id: { authority: "EPSG", code: 1156 }, + }, + { + name: "World Geodetic System 1984 (G2139)", + id: { authority: "EPSG", code: 1309 }, + }, + ], + ellipsoid: { + name: "WGS 84", + semi_major_axis: 6378137, + inverse_flattening: 298.257223563, + }, + accuracy: "2.0", + id: { authority: "EPSG", code: 6326 }, + }, + coordinate_system: { + subtype: "ellipsoidal", + axis: [ + { + name: "Geodetic longitude", + abbreviation: "Lon", + direction: "east", + unit: "degree", + }, + { + name: "Geodetic latitude", + abbreviation: "Lat", + direction: "north", + unit: "degree", + }, + ], + }, + scope: "Not known.", + area: "World.", + bbox: { + south_latitude: -90, + west_longitude: -180, + north_latitude: 90, + east_longitude: 180, + }, + id: { authority: "OGC", code: "CRS84" }, +}; + +/** + * Extract affine geotransform from a GeoTIFF image. + * + * Returns a 6-element array in Python `affine` package ordering: + * [a, b, c, d, e, f] where: + * - x_geo = a * col + b * row + c + * - y_geo = d * col + e * row + f + * + * This is NOT GDAL ordering, which is [c, a, b, f, d, e]. + */ +function extractGeotransform( + image: GeoTIFFImage, +): [number, number, number, number, number, number] { + const origin = image.getOrigin(); + const resolution = image.getResolution(); + + // origin: [x, y, z] + // resolution: [x_res, y_res, z_res] + + // Check for rotation/skew in the file directory + const fileDirectory = image.getFileDirectory(); + const modelTransformation = fileDirectory.ModelTransformation; + + let b = 0; // row rotation + let d = 0; // column rotation + + if (modelTransformation && modelTransformation.length >= 16) { + // ModelTransformation is a 4x4 matrix in row-major order + // [0 1 2 3 ] [a b 0 c] + // [4 5 6 7 ] = [d e 0 f] + // [8 9 10 11] [0 0 1 0] + // [12 13 14 15] [0 0 0 1] + b = modelTransformation[1]; + d = modelTransformation[4]; + } + + // Return in affine package ordering: [a, b, c, d, e, f] + return [ + resolution[0], // a: pixel width + b, // b: row rotation + origin[0], // c: x origin + d, // d: column rotation + resolution[1], // e: pixel height (often negative) + origin[1], // f: y origin + ]; +} + +/** + * Extract COG metadata + */ +export async function extractCOGMetadata(tiff: GeoTIFF): Promise { + const image = await tiff.getImage(); + + const width = image.getWidth(); + const height = image.getHeight(); + const tileWidth = image.getTileWidth(); + const tileHeight = image.getTileHeight(); + + const tilesX = Math.ceil(width / tileWidth); + const tilesY = Math.ceil(height / tileHeight); + + const bbox = image.getBoundingBox(); + const geoKeys = image.getGeoKeys(); + const projectionCode: number | null = + geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey || null; + const projection = projectionCode ? `EPSG:${projectionCode}` : null; + + // Extract geotransform from full-resolution image + // Only the top-level IFD has geo keys, so we'll derive overviews from this + const baseGeotransform = extractGeotransform(image); + + // Overviews **in COG order**, from finest to coarsest (we'll reverse the + // array later) + const overviews: COGOverview[] = []; + const imageCount = await tiff.getImageCount(); + + // Full resolution image (GeoTIFF index 0) + overviews.push({ + geoTiffIndex: 0, + width, + height, + tilesX, + tilesY, + scaleFactor: 1, + geotransform: baseGeotransform, + // TODO: combine these two properties into one + level: imageCount - 1, // Coarsest level number + z: imageCount - 1, + }); + + for (let i = 1; i < imageCount; i++) { + const overview = await tiff.getImage(i); + const overviewWidth = overview.getWidth(); + const overviewHeight = overview.getHeight(); + const overviewTileWidth = overview.getTileWidth(); + const overviewTileHeight = overview.getTileHeight(); + + const scaleFactor = Math.round(width / overviewWidth); + + // Derive geotransform for this overview by scaling pixel size + // [a, b, c, d, e, f] where a and e are pixel dimensions + const overviewGeotransform: [ + number, + number, + number, + number, + number, + number, + ] = [ + baseGeotransform[0] * scaleFactor, // a: scaled pixel width + baseGeotransform[1] * scaleFactor, // b: scaled row rotation + baseGeotransform[2], // c: same x origin + baseGeotransform[3] * scaleFactor, // d: scaled column rotation + baseGeotransform[4] * scaleFactor, // e: scaled pixel height (typically negative) + baseGeotransform[5], // f: same y origin + ]; + + overviews.push({ + geoTiffIndex: i, + width: overviewWidth, + height: overviewHeight, + tilesX: Math.ceil(overviewWidth / overviewTileWidth), + tilesY: Math.ceil(overviewHeight / overviewTileHeight), + scaleFactor, + geotransform: overviewGeotransform, + // TODO: combine these two properties into one + level: imageCount - 1 - i, + z: imageCount - 1 - i, + }); + } + + // Reverse to TileMatrixSet order: coarsest (0) → finest (n) + overviews.reverse(); + + const sourceProjection = await getProjjson(projectionCode); + const projectToWgs84 = proj4(sourceProjection, OGC_84); + const projectTo3857 = proj4(sourceProjection, "EPSG:3857"); + + return { + width, + height, + tileWidth, + tileHeight, + tilesX, + tilesY, + bbox: [bbox[0], bbox[1], bbox[2], bbox[3]], + projection, + projectToWgs84, + projectTo3857, + overviews, + image: tiff, + }; +} + +async function getProjjson(projectionCode: number | null) { + const url = `https://epsg.io/${projectionCode}.json`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch projection data from ${url}`); + } + const data = await response.json(); + return data; +} + +const viewport = new WebMercatorViewport({ + height: 500, + width: 845, + latitude: 40.88775942857086, + longitude: -73.20197979318772, + zoom: 11.294596276534985, +}); + +/** + * COGTileset2D with proper frustum culling + */ +export class COGTileset2D extends Tileset2D { + private cogMetadata: COGMetadata; + + constructor(cogMetadata: COGMetadata, opts: Tileset2DProps) { + super(opts); + this.cogMetadata = cogMetadata; + } + + /** + * Get tile indices visible in viewport + * Uses frustum culling similar to OSM implementation + * + * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer + */ + getTileIndices(opts: { + viewport: Viewport; + maxZoom?: number; + minZoom?: number; + zRange: ZRange | null; + modelMatrix?: Matrix4; + modelMatrixInverse?: Matrix4; + }): COGTileIndex[] { + console.log("Called getTileIndices", opts); + const tileIndices = getTileIndices(this.cogMetadata, opts); + console.log("Visible tile indices:", tileIndices); + + // return [ + // { x: 0, y: 0, z: 0 }, + // { x: 0, y: 0, z: 1 }, + // { x: 1, y: 1, z: 2 }, + // { x: 1, y: 2, z: 3 }, + // { x: 2, y: 1, z: 3 }, + // { x: 2, y: 2, z: 3 }, + // { x: 3, y: 1, z: 3 }, + // ]; // Temporary override for testing + return tileIndices; + } + + getTileId(index: COGTileIndex): string { + return `${index.x}-${index.y}-${index.z}`; + } + + getParentIndex(index: COGTileIndex): COGTileIndex { + if (index.z === 0) { + // Already at coarsest level + return index; + } + + const currentOverview = this.cogMetadata.overviews[index.z]; + const parentOverview = this.cogMetadata.overviews[index.z - 1]; + + const scaleFactor = + currentOverview.scaleFactor / parentOverview.scaleFactor; + + return { + x: Math.floor(index.x / scaleFactor), + y: Math.floor(index.y / scaleFactor), + z: index.z - 1, + }; + } + + getTileZoom(index: COGTileIndex): number { + return index.z; + } + + getTileMetadata(index: COGTileIndex): Record { + const { x, y, z } = index; + const { overviews, tileWidth, tileHeight } = this.cogMetadata; + const overview = overviews[z]; + + // Use geotransform to calculate tile bounds + // geotransform: [a, b, c, d, e, f] where: + // x_geo = a * col + b * row + c + // y_geo = d * col + e * row + f + const [a, b, c, d, e, f] = overview.geotransform; + + // Calculate pixel coordinates for this tile's extent + const pixelMinCol = x * tileWidth; + const pixelMinRow = y * tileHeight; + const pixelMaxCol = (x + 1) * tileWidth; + const pixelMaxRow = (y + 1) * tileHeight; + + // Calculate the four corners of the tile in geographic coordinates + const topLeft = [ + a * pixelMinCol + b * pixelMinRow + c, + d * pixelMinCol + e * pixelMinRow + f, + ]; + const topRight = [ + a * pixelMaxCol + b * pixelMinRow + c, + d * pixelMaxCol + e * pixelMinRow + f, + ]; + const bottomLeft = [ + a * pixelMinCol + b * pixelMaxRow + c, + d * pixelMinCol + e * pixelMaxRow + f, + ]; + const bottomRight = [ + a * pixelMaxCol + b * pixelMaxRow + c, + d * pixelMaxCol + e * pixelMaxRow + f, + ]; + + // Return the projected bounds as four corners + // This preserves rotation/skew information + const projectedBounds = { + topLeft, + topRight, + bottomLeft, + bottomRight, + }; + + // Also compute axis-aligned bounding box for compatibility + const bounds: Bounds = [ + Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), + Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), + Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), + Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), + ]; + + return { + bounds, + projectedBounds, + tileWidth, + tileHeight, + overview, + }; + } +} diff --git a/src/cog-tileset/cog-tile-2d-traversal.ts b/src/cog-tileset/cog-tile-2d-traversal.ts new file mode 100644 index 00000000..7194622a --- /dev/null +++ b/src/cog-tileset/cog-tile-2d-traversal.ts @@ -0,0 +1,519 @@ +/** + * This file implements tile traversal for generic 2D tilesets defined by COG + * tile layouts. + * + * The main algorithm works as follows: + * 1. Start at the root tile(s) (z=0, covers the entire image, but not + * necessarily the whole world) + * 2. Test if each tile is visible using viewport frustum culling + * 3. For visible tiles, compute distance-based LOD (Level of Detail) + * 4. If LOD is insufficient, recursively subdivide into 4 child tiles + * 5. Select tiles at appropriate zoom levels based on distance from camera + * + * The result is a set of tiles at varying zoom levels that efficiently + * cover the visible area with appropriate detail. + */ + +import { + _GlobeViewport, + assert, + Viewport, + WebMercatorViewport, +} from "@deck.gl/core"; +import { + CullingVolume, + Plane, + makeOrientedBoundingBoxFromPoints, +} from "@math.gl/culling"; + +import type { COGMetadata, COGOverview, COGTileIndex, ZRange } from "./types"; + +/** + * The size of the entire world in deck.gl's common coordinate space. + * + * The world always spans [0, 512] in both X and Y in Web Mercator common space. + * + * The origin (0,0) is at the top-left corner, and (512,512) is at the + * bottom-right. + */ +const WORLD_SIZE = 512; + +// Reference points used to sample tile boundaries for bounding volume +// calculation. +// +// In upstream deck.gl code, such reference points are only used in non-Web +// Mercator projections because the OSM tiling scheme is designed for Web +// Mercator and the OSM tile extents are already in Web Mercator projection. So +// using Axis-Aligned bounding boxes based on tile extents is sufficient for +// frustum culling in Web Mercator viewports. +// +// In upstream code these reference points are used for Globe View where the OSM +// tile indices _projected into longitude-latitude bounds in Globe View space_ +// are no longer axis-aligned, and oriented bounding boxes must be used instead. +// +// In the context of generic tiling grids which are often not in Web Mercator +// projection, we must use the reference points approach because the grid tiles +// will never be exact axis aligned boxes in Web Mercator space. + +// For most tiles: sample 4 corners and center (5 points total) +const REF_POINTS_5 = [ + [0.5, 0.5], // center + [0, 0], // top-left + [0, 1], // bottom-left + [1, 0], // top-right + [1, 1], // bottom-right +]; + +// For higher detail: add 4 edge midpoints (9 points total) +const REF_POINTS_9 = REF_POINTS_5.concat([ + [0, 0.5], // left edge + [0.5, 0], // top edge + [1, 0.5], // right edge + [0.5, 1], // bottom edge +]); + +/** + * COG Tile Node - similar to OSMNode but for COG's tile structure. + * + * Represents a single tile in the COG internal tiling pyramid. + * + * COG tile nodes use the following coordinate system: + * + * - x: tile column (0 to COGOverview.tilesX, left to right) + * - y: tile row (0 to COGOverview.tilesY, top to bottom) + * - z: overview level. This uses TileMatrixSet ordering where: 0 = coarsest, higher = finer + */ +export class COGTileNode { + /** Index across a row */ + x: number; + /** Index down a column */ + y: number; + /** TileMatrixSet-style zoom index (higher = finer detail) */ + z: number; + + private cogMetadata: COGMetadata; + + /** + * Flag indicating whether any descendant of this tile is visible. + * + * Used to prevent loading parent tiles when children are visible (avoids + * overdraw). + */ + private childVisible?: boolean; + + /** + * Flag indicating this tile should be rendered + * + * Set to `true` when this is the appropriate LOD for its distance from camera. + */ + private selected?: boolean; + + /** A cache of the children of this node. */ + private _children?: COGTileNode[]; + + constructor(x: number, y: number, z: number, cogMetadata: COGMetadata) { + this.x = x; + this.y = y; + this.z = z; + this.cogMetadata = cogMetadata; + } + + /** Get overview info for this tile's z level */ + get overview(): COGOverview { + return this.cogMetadata.overviews[this.z]; + } + + /** Get the children of this node. */ + get children(): COGTileNode[] { + if (!this._children) { + const maxZ = this.cogMetadata.overviews.length - 1; + if (this.z >= maxZ) { + // Already at finest resolution, no children + return []; + } + + // In TileMatrixSet ordering: refine to z + 1 (finer detail) + const childZ = this.z + 1; + const parentOverview = this.overview; + const childOverview = this.cogMetadata.overviews[childZ]; + + // Calculate scale factor between levels + const scaleFactor = + parentOverview.scaleFactor / childOverview.scaleFactor; + + // Generate child tiles + this._children = []; + for (let dy = 0; dy < scaleFactor; dy++) { + for (let dx = 0; dx < scaleFactor; dx++) { + const childX = this.x * scaleFactor + dx; + const childY = this.y * scaleFactor + dy; + + // Only create child if it's within bounds + // Some tiles on the edges might not need to be created at higher + // resolutions (higher map zoom level) + if (childX < childOverview.tilesX && childY < childOverview.tilesY) { + this._children.push( + new COGTileNode(childX, childY, childZ, this.cogMetadata), + ); + } + } + } + } + return this._children; + } + + /** + * Update tile visibility using frustum culling + * This follows the pattern from OSMNode + */ + update(params: { + viewport: Viewport; + project: ((xyz: number[]) => number[]) | null; + cullingVolume: CullingVolume; + elevationBounds: ZRange; + /** Minimum (coarsest) COG overview level */ + minZ: number; + /** Maximum (finest) COG overview level */ + maxZ?: number; + }): boolean { + const { + viewport, + cullingVolume, + elevationBounds, + minZ, + maxZ = this.cogMetadata.overviews.length - 1, + project, + } = params; + + // Get bounding volume for this tile + const boundingVolume = this.getBoundingVolume(elevationBounds, project); + + // Note: this is a part of the upstream code because they have _generic_ + // tiling systems, where the client doesn't know whether a given xyz tile + // actually exists. So the idea of `bounds` is to avoid even trying to fetch + // tiles that the user doesn't care about (think oceans) + // + // But in our case, we have known bounds from the COG metadata. So the tiles + // are explicitly constructed to match only tiles that exist. + + // Check if tile is within user-specified bounds + // if (bounds && !this.insideBounds(bounds)) { + // return false; + // } + + console.log("=== FRUSTUM CULLING DEBUG ==="); + console.log(`Tile: ${this.x}, ${this.y}, ${this.z}`); + console.log("Bounding volume center:", boundingVolume.center); + console.log("Bounding volume halfSize:", boundingVolume.halfSize); + console.log("Viewport cameraPosition:", viewport.cameraPosition); + console.log( + "Viewport pitch:", + viewport instanceof WebMercatorViewport ? viewport.pitch : "N/A", + ); + + for (let i = 0; i < cullingVolume.planes.length; i++) { + const plane = cullingVolume.planes[i]; + const result = boundingVolume.intersectPlane(plane); + const planeNames = ["left", "right", "bottom", "top", "near", "far"]; + + // Calculate signed distance from OBB center to plane + const centerDist = + plane.normal.x * boundingVolume.center.x + + plane.normal.y * boundingVolume.center.y + + plane.normal.z * boundingVolume.center.z + + plane.distance; + + console.log( + `Plane ${i} (${planeNames[i]}): normal=[${plane.normal.x.toFixed(3)}, ${plane.normal.y.toFixed(3)}, ${plane.normal.z.toFixed(3)}], ` + + `distance=${plane.distance.toFixed(3)}, centerDist=${centerDist.toFixed(3)}, result=${result} (${result === 1 ? "INSIDE" : result === 0 ? "INTERSECT" : "OUTSIDE"})`, + ); + } + console.log("=== END FRUSTUM DEBUG ==="); + + // Frustum culling + // Test if tile's bounding volume intersects the camera frustum + // Returns: <0 if outside, 0 if intersecting, >0 if fully inside + const isInside = cullingVolume.computeVisibility(boundingVolume); + console.log( + `Tile ${this.x},${this.y},${this.z} frustum check: ${isInside} (${isInside < 0 ? "CULLED" : "VISIBLE"})`, + ); + if (isInside < 0) { + return false; + } + + // LOD (Level of Detail) selection + // Only select this tile if no child is visible (prevents overlapping tiles) + if (!this.childVisible) { + let { z } = this; + + if (z < maxZ && z >= minZ) { + // Compute distance-based LOD adjustment + // Tiles farther from camera can use lower zoom levels (larger tiles) + // Distance is normalized by viewport height to be resolution-independent + const distance = + (boundingVolume.distanceTo(viewport.cameraPosition) * + viewport.scale) / + viewport.height; + // Increase effective zoom level based on log2(distance) + // e.g., if distance=4, accept tiles 2 levels lower than maxZ + z += Math.floor(Math.log2(distance)); + } + + if (z >= maxZ) { + // This tile's LOD is sufficient for its distance - select it for rendering + this.selected = true; + return true; + } + } + + // LOD is not enough, recursively test child tiles + this.selected = false; + this.childVisible = true; + + for (const child of this.children) { + child.update(params); + } + + // // NOTE: this deviates from upstream; we could move to the upstream code if + // // we pass in maxZ correctly I think + // if (children.length === 0) { + // // No children available (at finest resolution), select this tile + // this.selected = true; + // return true; + // } + + // for (const child of children) { + // child.update(params); + // } + return true; + } + + /** + * Collect all tiles marked as selected in the tree. + * Recursively traverses the entire tree and gathers tiles where selected=true. + * + * @param result - Accumulator array for selected tiles + * @returns Array of selected OSMNode tiles + */ + getSelected(result: COGTileNode[] = []): COGTileNode[] { + if (this.selected) { + result.push(this); + } + if (this._children) { + for (const node of this._children) { + node.getSelected(result); + } + } + return result; + } + + /** + * Calculate the 3D bounding volume for this tile in deck.gl's common + * coordinate space for frustum culling. + * + */ + getBoundingVolume( + zRange: ZRange, + project: ((xyz: number[]) => number[]) | null, + ) { + const overview = this.overview; + const { tileWidth, tileHeight } = this.cogMetadata; + + // Use geotransform to calculate tile bounds + // geotransform: [a, b, c, d, e, f] where: + // x_geo = a * col + b * row + c + // y_geo = d * col + e * row + f + const [a, b, c, d, e, f] = overview.geotransform; + + // Calculate pixel coordinates for this tile's extent + const pixelMinCol = this.x * tileWidth; + const pixelMinRow = this.y * tileHeight; + const pixelMaxCol = (this.x + 1) * tileWidth; + const pixelMaxRow = (this.y + 1) * tileHeight; + + // Sample reference points across the tile surface + const refPoints = REF_POINTS_9; + + console.log("refPoints", refPoints); + + /** Reference points positions in image CRS */ + const refPointPositionsImage: number[][] = []; + + for (const [pX, pY] of refPoints) { + // pX, pY are in [0, 1] range + // Interpolate pixel coordinates within the tile + const col = pixelMinCol + pX * (pixelMaxCol - pixelMinCol); + const row = pixelMinRow + pY * (pixelMaxRow - pixelMinRow); + + // Convert pixel coordinates to geographic coordinates using geotransform + const geoX = a * col + b * row + c; + const geoY = d * col + e * row + f; + + refPointPositionsImage.push([geoX, geoY]); + } + + console.log("refPointPositionsImage (image CRS):", refPointPositionsImage); + console.log("Geotransform [a,b,c,d,e,f]:", [a, b, c, d, e, f]); + + if (project) { + assert( + false, + "TODO: implement bounding volume implementation in Globe view", + ); + // Reproject positions to wgs84 instead, then pass them into `project` + // return makeOrientedBoundingBoxFromPoints(refPointPositions); + } + + /** Reference points positions in EPSG 3857 */ + const refPointPositionsProjected: number[][] = []; + + for (const [pX, pY] of refPointPositionsImage) { + // Reproject to Web Mercator (EPSG 3857) + const projected = this.cogMetadata.projectTo3857.forward([pX, pY]); + refPointPositionsProjected.push(projected); + + // Also log WGS84 for comparison + const wgs84 = this.cogMetadata.projectToWgs84.forward([pX, pY]); + console.log(`Image [${pX.toFixed(2)}, ${pY.toFixed(2)}] -> WGS84 [${wgs84[0].toFixed(6)}, ${wgs84[1].toFixed(6)}] -> WebMerc [${projected[0].toFixed(2)}, ${projected[1].toFixed(2)}]`); + } + + console.log("refPointPositionsProjected (EPSG:3857):", refPointPositionsProjected); + + // Convert from Web Mercator meters to deck.gl's common space (world units) + // Web Mercator range: [-20037508.34, 20037508.34] meters + // deck.gl world space: [0, 512] + const WEB_MERCATOR_MAX = 20037508.342789244; // Half Earth circumference + + /** Reference points positions in deck.gl world space */ + const refPointPositionsWorld: number[][] = []; + + for (const [mercX, mercY] of refPointPositionsProjected) { + // X: offset from [-20M, 20M] to [0, 40M], then normalize to [0, 512] + const worldX = + ((mercX + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + + // Y: same transformation WITHOUT flip + // Testing hypothesis: Y-flip might be incorrect since geotransform already handles orientation + const worldY = + ((mercY + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + + console.log(`WebMerc [${mercX.toFixed(2)}, ${mercY.toFixed(2)}] -> World [${worldX.toFixed(4)}, ${worldY.toFixed(4)}]`); + + // Add z-range minimum + refPointPositionsWorld.push([worldX, worldY, zRange[0]]); + } + + // Add top z-range if elevation varies + if (zRange[0] !== zRange[1]) { + for (const [mercX, mercY] of refPointPositionsProjected) { + const worldX = + ((mercX + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + const worldY = + WORLD_SIZE - + ((mercY + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + + refPointPositionsWorld.push([worldX, worldY, zRange[1]]); + } + } + + console.log("refPointPositionsWorld", refPointPositionsWorld); + console.log("zRange used:", zRange); + + const obb = makeOrientedBoundingBoxFromPoints(refPointPositionsWorld); + console.log("Created OBB center:", obb.center); + console.log("Created OBB halfAxes:", obb.halfAxes); + + return obb; + } + + /** + * Convert COG coordinates to lng/lat + * This is a placeholder - needs proper projection library (proj4js) + */ + private cogCoordsToLngLat([x, y]: [number, number]): number[] { + const [lng, lat] = this.cogMetadata.projectToWgs84.forward([x, y]); + return [lng, lat, 0]; + } +} + +/** + * Get tile indices visible in viewport + * Uses frustum culling similar to OSM implementation + * + * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer + */ +export function getTileIndices( + cogMetadata: COGMetadata, + opts: { + viewport: Viewport; + maxZ: number; + zRange: ZRange | null; + }, +): COGTileIndex[] { + const { viewport, maxZ, zRange } = opts; + + // console.log("=== getTileIndices called ==="); + // console.log("Viewport:", viewport); + // console.log("maxZ:", maxZ); + // console.log("COG metadata overviews count:", cogMetadata.overviews.length); + // console.log("COG bbox:", cogMetadata.bbox); + + const project: ((xyz: number[]) => number[]) | null = + viewport instanceof _GlobeViewport && viewport.resolution + ? viewport.projectPosition + : null; + + // Get the culling volume of the current camera + const planes: Plane[] = Object.values(viewport.getFrustumPlanes()).map( + ({ normal, distance }) => new Plane(normal.clone().negate(), distance), + ); + const cullingVolume = new CullingVolume(planes); + + // Project zRange from meters to common space + const unitsPerMeter = viewport.distanceScales.unitsPerMeter[2]; + const elevationMin = (zRange && zRange[0] * unitsPerMeter) || 0; + const elevationMax = (zRange && zRange[1] * unitsPerMeter) || 0; + + // Optimization: For low-pitch views, only consider tiles at maxZ level + // At low pitch (top-down view), all tiles are roughly the same distance, + // so we don't need the LOD pyramid - just use the finest level + const minZ = + viewport instanceof WebMercatorViewport && viewport.pitch <= 60 ? maxZ : 0; + + // Start from coarsest overview + const coarsestOverview = cogMetadata.overviews[0]; + + // Create root tiles at coarsest level + // In contrary to OSM tiling, we might have more than one tile at the + // coarsest level (z=0) + const roots: COGTileNode[] = []; + for (let y = 0; y < coarsestOverview.tilesY; y++) { + for (let x = 0; x < coarsestOverview.tilesX; x++) { + roots.push(new COGTileNode(x, y, 0, cogMetadata)); + } + } + + // Traverse and update visibility + const traversalParams = { + viewport, + project, + cullingVolume, + elevationBounds: [elevationMin, elevationMax] as ZRange, + minZ, + maxZ, + }; + console.log("Traversal params:", traversalParams); + + for (const root of roots) { + root.update(traversalParams); + } + console.log("roots", roots); + + // Collect selected tiles + const selectedNodes: COGTileNode[] = []; + for (const root of roots) { + root.getSelected(selectedNodes); + } + + return selectedNodes; +} diff --git a/src/cog-tileset/index.ts b/src/cog-tileset/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/cog-tileset/tile-matrix-set-schema.d.ts b/src/cog-tileset/tile-matrix-set-schema.d.ts new file mode 100644 index 00000000..56828ad8 --- /dev/null +++ b/src/cog-tileset/tile-matrix-set-schema.d.ts @@ -0,0 +1,912 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + * + * This was created via + * ``` + * git clone https://github.com/opengeospatial/2D-Tile-Matrix-Set + * git checkout a1179bb + * cd 2D-Tile-Matrix-Set/schemas/tms/2.0/json + * npx json-schema-to-typescript tileMatrixSet.json > tile-matrix-set-schema.d.ts + * ``` + */ + +export type CRS = + | string + | ( + | { + /** + * Reference to one coordinate reference system (CRS) + */ + uri: string; + [k: string]: unknown; + } + | { + wkt: { + [k: string]: unknown; + } & HttpsProjOrgSchemasV02ProjjsonSchemaJson; + [k: string]: unknown; + } + | { + /** + * A reference system data structure as defined in the MD_ReferenceSystem of the ISO 19115 + */ + referenceSystem: { + [k: string]: unknown; + }; + [k: string]: unknown; + } + ); +/** + * Schema for PROJJSON (v0.2.1) + */ +export type HttpsProjOrgSchemasV02ProjjsonSchemaJson = + | ( + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs + ) + | ( + | GeodeticReferenceFrame + | VerticalReferenceFrame + | DynamicGeodeticReferenceFrame + | DynamicVerticalReferenceFrame + | TemporalDatum + | ParametricDatum + | EngineeringDatum + ) + | DatumEnsemble + | Ellipsoid + | PrimeMeridian + | (Conversion | Transformation) + | ConcatenatedOperation; +export type AbridgedTransformation = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "AbridgedTransformation"; + name: string; + method: Method; + parameters: ParameterValue[]; + id?: Id; + ids?: Ids; +}; +export type Method = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "OperationMethod"; + name: string; + id?: Id; + ids?: Ids; +}; +export type Ids = Id[]; +export type ParameterValue = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "ParameterValue"; + name: string; + value: string | number; + unit?: + | ("metre" | "degree" | "unity") + | { + [k: string]: unknown; + }; + id?: Id; + ids?: Ids; +}; +export type CompoundCrs = { + [k: string]: unknown; +} & { + type?: "CompoundCRS"; + name: string; + components: ( + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs + )[]; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type DerivedEngineeringCrs = { + [k: string]: unknown; +} & { + type?: "DerivedEngineeringCRS"; + name: string; + base_crs: EngineeringCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type EngineeringCrs = { + [k: string]: unknown; +} & { + type?: "EngineeringCRS"; + name: string; + datum: EngineeringDatum; + coordinate_system?: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type EngineeringDatum = { + [k: string]: unknown; +} & { + type?: "EngineeringDatum"; + name: string; + anchor?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type CoordinateSystem = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "CoordinateSystem"; + name?: string; + subtype: + | "Cartesian" + | "spherical" + | "ellipsoidal" + | "vertical" + | "ordinal" + | "parametric" + | "TemporalDateTime" + | "TemporalCount" + | "TemporalMeasure"; + axis: Axis[]; + id?: Id; + ids?: Ids; +}; +export type Axis = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "Axis"; + name: string; + abbreviation: string; + direction: + | "north" + | "northNorthEast" + | "northEast" + | "eastNorthEast" + | "east" + | "eastSouthEast" + | "southEast" + | "southSouthEast" + | "south" + | "southSouthWest" + | "southWest" + | "westSouthWest" + | "west" + | "westNorthWest" + | "northWest" + | "northNorthWest" + | "up" + | "down" + | "geocentricX" + | "geocentricY" + | "geocentricZ" + | "columnPositive" + | "columnNegative" + | "rowPositive" + | "rowNegative" + | "displayRight" + | "displayLeft" + | "displayUp" + | "displayDown" + | "forward" + | "aft" + | "port" + | "starboard" + | "clockwise" + | "counterClockwise" + | "towards" + | "awayFrom" + | "future" + | "past" + | "unspecified"; + unit?: + | ("metre" | "degree" | "unity") + | { + [k: string]: unknown; + }; + id?: Id; + ids?: Ids; +}; +export type Conversion = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "Conversion"; + name: string; + method: Method; + parameters?: ParameterValue[]; + id?: Id; + ids?: Ids; +}; +export type DerivedGeodeticCrs = { + [k: string]: unknown; +} & { + type?: "DerivedGeodeticCRS" | "DerivedGeographicCRS"; + name: string; + base_crs: GeodeticCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +/** + * One and only one of datum and datum_ensemble must be provided + */ +export type GeodeticCrs = { + [k: string]: unknown; +} & { + type?: "GeodeticCRS" | "GeographicCRS"; + name: string; + datum?: GeodeticReferenceFrame | DynamicGeodeticReferenceFrame; + datum_ensemble?: DatumEnsemble; + coordinate_system?: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type GeodeticReferenceFrame = { + [k: string]: unknown; +} & { + type?: "GeodeticReferenceFrame"; + name: string; + anchor?: string; + ellipsoid: Ellipsoid; + prime_meridian?: PrimeMeridian; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type Ellipsoid = { + [k: string]: unknown; +} & ( + | { + $schema?: string; + type?: "Ellipsoid"; + name: string; + semi_major_axis: number | ValueAndUnit; + semi_minor_axis: number | ValueAndUnit; + id?: Id; + ids?: Ids; + } + | { + $schema?: string; + type?: "Ellipsoid"; + name: string; + semi_major_axis: number | ValueAndUnit; + inverse_flattening: number; + id?: Id; + ids?: Ids; + } + | { + $schema?: string; + type?: "Ellipsoid"; + name: string; + radius: number | ValueAndUnit; + id?: Id; + ids?: Ids; + } +); +export type PrimeMeridian = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "PrimeMeridian"; + name: string; + longitude?: number | ValueAndUnit; + id?: Id; + ids?: Ids; +}; +export type DynamicGeodeticReferenceFrame = GeodeticReferenceFrame & { + type?: "DynamicGeodeticReferenceFrame"; + name: unknown; + anchor?: unknown; + ellipsoid: unknown; + prime_meridian?: unknown; + frame_reference_epoch: number; + deformation_model?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type DatumEnsemble = { + [k: string]: unknown; +} & { + $schema?: string; + type?: "DatumEnsemble"; + name: string; + members: { + [k: string]: unknown; + }[]; + ellipsoid?: Ellipsoid; + accuracy: string; + id?: Id; + ids?: Ids; +}; +export type DerivedParametricCrs = { + [k: string]: unknown; +} & { + type?: "DerivedParametricCRS"; + name: string; + base_crs: ParametricCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type ParametricCrs = { + [k: string]: unknown; +} & { + type?: "ParametricCRS"; + name: string; + datum: ParametricDatum; + coordinate_system?: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type ParametricDatum = { + [k: string]: unknown; +} & { + type?: "ParametricDatum"; + name: string; + anchor?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type DerivedProjectedCrs = { + [k: string]: unknown; +} & { + type?: "DerivedProjectedCRS"; + name: string; + base_crs: ProjectedCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type ProjectedCrs = { + [k: string]: unknown; +} & { + type?: "ProjectedCRS"; + name: string; + base_crs: GeodeticCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type DerivedTemporalCrs = { + [k: string]: unknown; +} & { + type?: "DerivedTemporalCRS"; + name: string; + base_crs: TemporalCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type TemporalCrs = { + [k: string]: unknown; +} & { + type?: "TemporalCRS"; + name: string; + datum: TemporalDatum; + coordinate_system?: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type TemporalDatum = { + [k: string]: unknown; +} & { + type?: "TemporalDatum"; + name: string; + calendar: string; + time_origin?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type DerivedVerticalCrs = { + [k: string]: unknown; +} & { + type?: "DerivedVerticalCRS"; + name: string; + base_crs: VerticalCrs; + conversion: Conversion; + coordinate_system: CoordinateSystem; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +/** + * One and only one of datum and datum_ensemble must be provided + */ +export type VerticalCrs = { + [k: string]: unknown; +} & { + type?: "VerticalCRS"; + name: string; + datum?: VerticalReferenceFrame | DynamicVerticalReferenceFrame; + datum_ensemble?: DatumEnsemble; + coordinate_system?: CoordinateSystem; + geoid_model?: { + name: string; + interpolation_crs?: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + id?: Id; + }; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type VerticalReferenceFrame = { + [k: string]: unknown; +} & { + type?: "VerticalReferenceFrame"; + name: string; + anchor?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type DynamicVerticalReferenceFrame = VerticalReferenceFrame & { + type?: "DynamicVerticalReferenceFrame"; + name: unknown; + anchor?: unknown; + frame_reference_epoch: number; + deformation_model?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type Transformation = { + [k: string]: unknown; +} & { + type?: "Transformation"; + name: string; + source_crs: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + target_crs: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + interpolation_crs?: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + method: Method; + parameters: ParameterValue[]; + accuracy?: string; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +export type ConcatenatedOperation = { + [k: string]: unknown; +} & { + type?: "ConcatenatedOperation"; + name: string; + source_crs: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + target_crs: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + steps: (Conversion | Transformation)[]; + $schema?: unknown; + scope?: unknown; + area?: unknown; + bbox?: unknown; + usages?: unknown; + remarks?: unknown; + id?: unknown; + ids?: unknown; +}; +/** + * A 2D Point in the CRS indicated elsewhere + * + * @minItems 2 + * @maxItems 2 + */ +export type DPoint = [number, number]; + +/** + * A definition of a tile matrix set following the Tile Matrix Set standard. For tileset metadata, such a description (in `tileMatrixSet` property) is only required for offline use, as an alternative to a link with a `http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme` relation type. + */ +export interface TileMatrixSetDefinition { + /** + * Title of this tile matrix set, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + /** + * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set + */ + keywords?: string[]; + /** + * Tile matrix set identifier. Implementation of 'identifier' + */ + id?: string; + /** + * Reference to an official source for this tileMatrixSet + */ + uri?: string; + /** + * @minItems 1 + */ + orderedAxes?: [string, ...string[]]; + crs: { + [k: string]: unknown; + } & CRS; + /** + * Reference to a well-known scale set + */ + wellKnownScaleSet?: string; + boundingBox?: { + [k: string]: unknown; + } & DBoundingBox; + /** + * Describes scale levels and its tile matrices + */ + tileMatrices: TileMatrix[]; + [k: string]: unknown; +} +export interface BoundCrs { + $schema?: string; + type?: "BoundCRS"; + source_crs: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + target_crs: + | BoundCrs + | CompoundCrs + | DerivedEngineeringCrs + | DerivedGeodeticCrs + | DerivedParametricCrs + | DerivedProjectedCrs + | DerivedTemporalCrs + | DerivedVerticalCrs + | EngineeringCrs + | GeodeticCrs + | ParametricCrs + | ProjectedCrs + | TemporalCrs + | VerticalCrs; + transformation: AbridgedTransformation; +} +export interface Id { + authority: string; + code: string | number; +} +export interface ValueAndUnit { + value: number; + unit: + | ("metre" | "degree" | "unity") + | { + [k: string]: unknown; + }; +} +/** + * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere + */ +export interface DBoundingBox { + lowerLeft: DPoint; + upperRight: DPoint; + crs?: CRS; + /** + * @minItems 2 + * @maxItems 2 + */ + orderedAxes?: [string, string]; + [k: string]: unknown; +} +/** + * A tile matrix, usually corresponding to a particular zoom level of a TileMatrixSet. + */ +export interface TileMatrix { + /** + * Title of this tile matrix, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + /** + * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this dataset + */ + keywords?: string[]; + /** + * Identifier selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile. Implementation of 'identifier' + */ + id: string; + /** + * Scale denominator of this tile matrix + */ + scaleDenominator: number; + /** + * Cell size of this tile matrix + */ + cellSize: number; + /** + * The corner of the tile matrix (_topLeft_ or _bottomLeft_) used as the origin for numbering tile rows and columns. This corner is also a corner of the (0, 0) tile. + */ + cornerOfOrigin?: "topLeft" | "bottomLeft"; + pointOfOrigin: { + [k: string]: unknown; + } & DPoint; + /** + * Width of each tile of this tile matrix in pixels + */ + tileWidth: number; + /** + * Height of each tile of this tile matrix in pixels + */ + tileHeight: number; + /** + * Width of the matrix (number of tiles in width) + */ + matrixHeight: number; + /** + * Height of the matrix (number of tiles in height) + */ + matrixWidth: number; + /** + * Describes the rows that has variable matrix width + */ + variableMatrixWidths?: VariableMatrixWidth[]; + [k: string]: unknown; +} +/** + * Variable Matrix Width data structure + */ +export interface VariableMatrixWidth { + /** + * Number of tiles in width that coalesce in a single tile for these rows + */ + coalesce: number; + /** + * First tile row where the coalescence factor applies for this tilematrix + */ + minTileRow: number; + /** + * Last tile row where the coalescence factor applies for this tilematrix + */ + maxTileRow: number; + [k: string]: unknown; +} diff --git a/src/cog-tileset/types.ts b/src/cog-tileset/types.ts new file mode 100644 index 00000000..9c752a4c --- /dev/null +++ b/src/cog-tileset/types.ts @@ -0,0 +1,343 @@ +import type { GeoTIFF } from "geotiff"; +import type { Converter } from "proj4"; + +export type ZRange = [minZ: number, maxZ: number]; + +export type Bounds = [minX: number, minY: number, maxX: number, maxY: number]; + +export type GeoBoundingBox = { + west: number; + north: number; + east: number; + south: number; +}; +export type NonGeoBoundingBox = { + left: number; + top: number; + right: number; + bottom: number; +}; + +export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox; + +export type TileIndex = { x: number; y: number; z: number }; + +export type TileLoadProps = { + index: TileIndex; + id: string; + bbox: TileBoundingBox; + url?: string | null; + signal?: AbortSignal; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + userData?: Record; + zoom?: number; +}; + +//////////////// +// Claude-generated metadata +//////////////// + +/** + * Represents a single resolution level in a Cloud Optimized GeoTIFF. + * + * COGs contain multiple resolution levels (overviews) for efficient + * visualization at different zoom levels. + * + * IMPORTANT: Overviews are ordered according to TileMatrixSet specification: + * - Index 0: Coarsest resolution (most zoomed out) + * - Index N: Finest resolution (most zoomed in) + * + * This matches the natural ordering where z increases with detail. + */ +export type COGOverview = { + /** + * Overview index in the TileMatrixSet ordering. + * - Index 0: Coarsest resolution (most zoomed out) + * - Higher indices: Progressively finer resolution + * + * This is the index in the COGMetadata.overviews array and represents + * the natural ordering from coarse to fine. + * + * Note: This is different from GeoTIFF's internal level numbering where + * level 0 is the full resolution image. + * + * @example + * // For a COG with 4 resolutions: + * index: 0 // Coarsest: 1250x1000 pixels (8x downsampled) + * index: 1 // Medium: 2500x2000 pixels (4x downsampled) + * index: 2 // Fine: 5000x4000 pixels (2x downsampled) + * index: 3 // Finest: 10000x8000 pixels (full resolution) + */ + level: number; + + /** + * Zoom index (OSM convention). + * Defined as: maxLevel - currentLevel + * + * This makes the code compatible with OSM tile indexing where: + * - Higher z = finer detail (opposite of COG level) + * - Lower z = coarser detail + * + * In TileMatrixSet ordering: z === level (both increase with detail) + */ + z: number; + + /** + * Width of the entire image at this overview level, in pixels. + */ + width: number; + /** + * Height of the entire image at this overview level, in pixels. + */ + height: number; + + /** + * Number of tiles in the X (horizontal) direction at this overview level. + * + * Calculated as: Math.ceil(width / tileWidth) + * + * @example + * // If tileWidth = 512: + * tilesX: 3 // z=0: ceil(1250 / 512) + * tilesX: 5 // z=1: ceil(2500 / 512) + * tilesX: 10 // z=2: ceil(5000 / 512) + * tilesX: 20 // z=3: ceil(10000 / 512) + */ + tilesX: number; + + /** + * Number of tiles in the Y (vertical) direction at this overview level. + * + * Calculated as: Math.ceil(height / tileHeight) + * + * @example + * // If tileHeight = 512: + * tilesY: 2 // z=0: ceil(1000 / 512) + * tilesY: 4 // z=1: ceil(2000 / 512) + * tilesY: 8 // z=2: ceil(4000 / 512) + * tilesY: 16 // z=3: ceil(8000 / 512) + */ + tilesY: number; + + /** + * Downsampling scale factor relative to full resolution (finest level). + * + * Indicates how much this overview is downsampled compared to the finest resolution. + * - Scale factor of 1: Full resolution (finest level) + * - Scale factor of 2: Half resolution + * - Scale factor of 4: Quarter resolution + * - Scale factor of 8: Eighth resolution (coarsest in this example) + * + * Common pattern: Each overview is 2x downsampled from the next finer level, + * so scale factors are powers of 2: 8, 4, 2, 1 (from coarsest to finest) + * + * @example + * scaleFactor: 8 // z=0: 1250x1000 (8x downsampled from finest) + * scaleFactor: 4 // z=1: 2500x2000 (4x downsampled) + * scaleFactor: 2 // z=2: 5000x4000 (2x downsampled) + * scaleFactor: 1 // z=3: 10000x8000 (full resolution) + */ + scaleFactor: number; + + /** + * Index in the original GeoTIFF file. + * + * GeoTIFF stores: image 0 = full resolution, image 1+ = overviews (progressively coarser) + * This index is needed to read the correct image from the GeoTIFF file. + * + * Note: This may differ from `level` since we reorder overviews to TileMatrixSet order. + * + * @example + * // TileMatrixSet order (our array): + * level: 0, geoTiffIndex: 3 // Coarsest (GeoTIFF overview 3) + * level: 1, geoTiffIndex: 2 // Medium (GeoTIFF overview 2) + * level: 2, geoTiffIndex: 1 // Fine (GeoTIFF overview 1) + * level: 3, geoTiffIndex: 0 // Finest (GeoTIFF main image) + */ + geoTiffIndex: number; + + /** + * Affine geotransform for this overview level. + * + * Uses Python `affine` package ordering (NOT GDAL ordering): + * [a, b, c, d, e, f] where: + * - x_geo = a * col + b * row + c + * - y_geo = d * col + e * row + f + * + * Parameters: + * - a: pixel width (x resolution) + * - b: row rotation (typically 0) + * - c: x-coordinate of upper-left corner of the upper-left pixel + * - d: column rotation (typically 0) + * - e: pixel height (y resolution, typically negative) + * - f: y-coordinate of upper-left corner of the upper-left pixel + * + * @example + * // For a UTM image with 30m pixels: + * [30, 0, 440720, 0, -30, 3751320] + * // x_geo = 30 * col + 440720 + * // y_geo = -30 * row + 3751320 + */ + geotransform: [number, number, number, number, number, number]; +}; + +/** + * COG Metadata extracted from GeoTIFF + */ +export type COGMetadata = { + width: number; + height: number; + /** Number of pixels wide for each tile */ + tileWidth: number; + tileHeight: number; + tilesX: number; + tilesY: number; + bbox: Bounds; // COG's CRS + projection: string | null; + overviews: COGOverview[]; + image: GeoTIFF; // GeoTIFF reference + projectToWgs84: Converter; + projectTo3857: Converter; +}; + +/** + * COG Tile Index + * + * In TileMatrixSet ordering: level === z (both 0 = coarsest, higher = finer) + */ +export type COGTileIndex = { + x: number; + y: number; + z: number; // TileMatrixSet/OSM zoom (0 = coarsest, higher = finer) +}; + +//////////////// +// TileMatrixSet +//////////////// + +// type CRS = string | { [k: string]: unknown }; +export type TMSCrs = unknown; + +/** + * A 2D Point in the CRS indicated elsewhere + * + * @minItems 2 + * @maxItems 2 + */ +export type TMSPoint = [number, number]; + +/** + * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere + */ +export interface TMSBoundingBox { + lowerLeft: TMSPoint; + upperRight: TMSPoint; + crs?: TMSCrs; + /** + * @minItems 2 + * @maxItems 2 + */ + orderedAxes?: [string, string]; + [k: string]: unknown; +} + +/** + * A definition of a tile matrix set following the Tile Matrix Set standard. For tileset metadata, such a description (in `tileMatrixSet` property) is only required for offline use, as an alternative to a link with a `http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme` relation type. + */ +export type TileMatrixSetDefinition = { + /** + * Title of this tile matrix set, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + /** + * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set + */ + keywords?: string[]; + /** + * Tile matrix set identifier. Implementation of 'identifier' + */ + id?: string; + /** + * Reference to an official source for this tileMatrixSet + */ + uri?: string; + /** + * @minItems 1 + */ + orderedAxes?: [string, ...string[]]; + crs: TMSCrs; + /** + * Reference to a well-known scale set + */ + wellKnownScaleSet?: string; + boundingBox?: { + [k: string]: unknown; + } & TMSBoundingBox; + /** + * Describes scale levels and its tile matrices + */ + tileMatrices: TMSTileMatrix[]; + [k: string]: unknown; +}; + +/** + * A tile matrix, usually corresponding to a particular zoom level of a TileMatrixSet. + */ +export interface TMSTileMatrix { + /** + * Title of this tile matrix, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + /** + * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this dataset + */ + keywords?: string[]; + /** + * Identifier selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile. Implementation of 'identifier' + */ + id: string; + /** + * Scale denominator of this tile matrix + */ + scaleDenominator: number; + /** + * Cell size of this tile matrix + */ + cellSize: number; + /** + * The corner of the tile matrix (_topLeft_ or _bottomLeft_) used as the origin for numbering tile rows and columns. This corner is also a corner of the (0, 0) tile. + */ + cornerOfOrigin?: "topLeft" | "bottomLeft"; + pointOfOrigin: { + [k: string]: unknown; + } & TMSPoint; + /** + * Width of each tile of this tile matrix in pixels + */ + tileWidth: number; + /** + * Height of each tile of this tile matrix in pixels + */ + tileHeight: number; + /** + * Width of the matrix (number of tiles in width) + */ + matrixHeight: number; + /** + * Height of the matrix (number of tiles in height) + */ + matrixWidth: number; + /** + * Describes the rows that has variable matrix width + */ + variableMatrixWidths?: object[]; + [k: string]: unknown; +} diff --git a/src/cog-tileset/utils.ts b/src/cog-tileset/utils.ts new file mode 100644 index 00000000..4d31a595 --- /dev/null +++ b/src/cog-tileset/utils.ts @@ -0,0 +1,357 @@ +import { Viewport } from "@deck.gl/core"; +import { Matrix4 } from "@math.gl/core"; + +import { getOSMTileIndices } from "./tile-2d-traversal"; +import { + Bounds, + GeoBoundingBox, + TileBoundingBox, + TileIndex, + ZRange, +} from "./types.js"; + +const TILE_SIZE = 512; +const DEFAULT_EXTENT: Bounds = [-Infinity, -Infinity, Infinity, Infinity]; + +export type URLTemplate = string | string[] | null; + +export const urlType = { + type: "object" as const, + value: null as URLTemplate, + validate: (value, propType) => + (propType.optional && value === null) || + typeof value === "string" || + (Array.isArray(value) && value.every((url) => typeof url === "string")), + equal: (value1, value2) => { + if (value1 === value2) { + return true; + } + if (!Array.isArray(value1) || !Array.isArray(value2)) { + return false; + } + const len = value1.length; + if (len !== value2.length) { + return false; + } + for (let i = 0; i < len; i++) { + if (value1[i] !== value2[i]) { + return false; + } + } + return true; + }, +}; + +export function transformBox(bbox: Bounds, modelMatrix: Matrix4): Bounds { + const transformedCoords = [ + // top-left + modelMatrix.transformAsPoint([bbox[0], bbox[1]]), + // top-right + modelMatrix.transformAsPoint([bbox[2], bbox[1]]), + // bottom-left + modelMatrix.transformAsPoint([bbox[0], bbox[3]]), + // bottom-right + modelMatrix.transformAsPoint([bbox[2], bbox[3]]), + ]; + const transformedBox: Bounds = [ + // Minimum x coord + Math.min(...transformedCoords.map((i) => i[0])), + // Minimum y coord + Math.min(...transformedCoords.map((i) => i[1])), + // Max x coord + Math.max(...transformedCoords.map((i) => i[0])), + // Max y coord + Math.max(...transformedCoords.map((i) => i[1])), + ]; + return transformedBox; +} + +function stringHash(s: string): number { + return Math.abs( + s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0), + ); +} + +export function getURLFromTemplate( + template: URLTemplate, + tile: { + index: TileIndex; + id: string; + }, +): string | null { + if (!template || !template.length) { + return null; + } + const { index, id } = tile; + + if (Array.isArray(template)) { + const i = stringHash(id) % template.length; + template = template[i]; + } + + let url = template; + for (const key of Object.keys(index)) { + const regex = new RegExp(`{${key}}`, "g"); + url = url.replace(regex, String(index[key])); + } + + // Back-compatible support for {-y} + if (Number.isInteger(index.y) && Number.isInteger(index.z)) { + url = url.replace(/\{-y\}/g, String(Math.pow(2, index.z) - index.y - 1)); + } + return url; +} + +/** + * gets the bounding box of a viewport + */ +function getBoundingBox( + viewport: Viewport, + zRange: number[] | null, + extent: Bounds, +): Bounds { + let bounds; + if (zRange && zRange.length === 2) { + const [minZ, maxZ] = zRange; + const bounds0 = viewport.getBounds({ z: minZ }); + const bounds1 = viewport.getBounds({ z: maxZ }); + bounds = [ + Math.min(bounds0[0], bounds1[0]), + Math.min(bounds0[1], bounds1[1]), + Math.max(bounds0[2], bounds1[2]), + Math.max(bounds0[3], bounds1[3]), + ]; + } else { + bounds = viewport.getBounds(); + } + if (!viewport.isGeospatial) { + return [ + // Top corner should not be more then bottom corner in either direction + Math.max(Math.min(bounds[0], extent[2]), extent[0]), + Math.max(Math.min(bounds[1], extent[3]), extent[1]), + // Bottom corner should not be less then top corner in either direction + Math.min(Math.max(bounds[2], extent[0]), extent[2]), + Math.min(Math.max(bounds[3], extent[1]), extent[3]), + ]; + } + return [ + Math.max(bounds[0], extent[0]), + Math.max(bounds[1], extent[1]), + Math.min(bounds[2], extent[2]), + Math.min(bounds[3], extent[3]), + ]; +} + +/** Get culling bounds in world space */ +export function getCullBounds({ + viewport, + z, + cullRect, +}: { + /** Current viewport */ + viewport: Viewport; + /** Current z range */ + z: ZRange | number | null; + /** Culling rectangle in screen space */ + cullRect: { x: number; y: number; width: number; height: number }; +}): [number, number, number, number][] { + const subViewports = viewport.subViewports || [viewport]; + return subViewports.map((v) => getCullBoundsInViewport(v, z || 0, cullRect)); +} + +function getCullBoundsInViewport( + /** Current viewport */ + viewport: Viewport, + /** At altitude */ + z: ZRange | number, + /** Culling rectangle in screen space */ + cullRect: { x: number; y: number; width: number; height: number }, +): [number, number, number, number] { + if (!Array.isArray(z)) { + const x = cullRect.x - viewport.x; + const y = cullRect.y - viewport.y; + const { width, height } = cullRect; + + const unprojectOption = { targetZ: z }; + + const topLeft = viewport.unproject([x, y], unprojectOption); + const topRight = viewport.unproject([x + width, y], unprojectOption); + const bottomLeft = viewport.unproject([x, y + height], unprojectOption); + const bottomRight = viewport.unproject( + [x + width, y + height], + unprojectOption, + ); + + return [ + Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), + Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), + Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), + Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), + ]; + } + + const bounds0 = getCullBoundsInViewport(viewport, z[0], cullRect); + const bounds1 = getCullBoundsInViewport(viewport, z[1], cullRect); + + return [ + Math.min(bounds0[0], bounds1[0]), + Math.min(bounds0[1], bounds1[1]), + Math.max(bounds0[2], bounds1[2]), + Math.max(bounds0[3], bounds1[3]), + ]; +} + +function getIndexingCoords( + bbox: Bounds, + scale: number, + modelMatrixInverse?: Matrix4, +): Bounds { + if (modelMatrixInverse) { + const transformedTileIndex = transformBox(bbox, modelMatrixInverse).map( + (i) => (i * scale) / TILE_SIZE, + ); + return transformedTileIndex as Bounds; + } + return bbox.map((i) => (i * scale) / TILE_SIZE) as Bounds; +} + +function getScale(z: number, tileSize: number): number { + return (Math.pow(2, z) * TILE_SIZE) / tileSize; +} + +// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_2 +export function osmTile2lngLat( + x: number, + y: number, + z: number, +): [number, number] { + const scale = getScale(z, TILE_SIZE); + const lng = (x / scale) * 360 - 180; + const n = Math.PI - (2 * Math.PI * y) / scale; + const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); + return [lng, lat]; +} + +function tile2XY( + x: number, + y: number, + z: number, + tileSize: number, +): [number, number] { + const scale = getScale(z, tileSize); + return [(x / scale) * TILE_SIZE, (y / scale) * TILE_SIZE]; +} +export function tileToBoundingBox( + viewport: Viewport, + x: number, + y: number, + z: number, + tileSize: number = TILE_SIZE, +): TileBoundingBox { + if (viewport.isGeospatial) { + const [west, north] = osmTile2lngLat(x, y, z); + const [east, south] = osmTile2lngLat(x + 1, y + 1, z); + return { west, north, east, south }; + } + const [left, top] = tile2XY(x, y, z, tileSize); + const [right, bottom] = tile2XY(x + 1, y + 1, z, tileSize); + return { left, top, right, bottom }; +} + +function getIdentityTileIndices( + viewport: Viewport, + z: number, + tileSize: number, + extent: Bounds, + modelMatrixInverse?: Matrix4, +) { + const bbox = getBoundingBox(viewport, null, extent); + const scale = getScale(z, tileSize); + const [minX, minY, maxX, maxY] = getIndexingCoords( + bbox, + scale, + modelMatrixInverse, + ); + const indices: TileIndex[] = []; + + /* + | TILE | TILE | TILE | + |(minX) |(maxX) + */ + for (let x = Math.floor(minX); x < maxX; x++) { + for (let y = Math.floor(minY); y < maxY; y++) { + indices.push({ x, y, z }); + } + } + return indices; +} + +/** + * Returns all tile indices in the current viewport. If the current zoom level is smaller + * than minZoom, return an empty array. If the current zoom level is greater than maxZoom, + * return tiles that are on maxZoom. + */ + +export function getTileIndices({ + viewport, + maxZoom, + minZoom, + zRange, + extent, + tileSize = TILE_SIZE, + modelMatrix, + modelMatrixInverse, + zoomOffset = 0, +}: { + viewport: Viewport; + maxZoom?: number; + minZoom?: number; + zRange: ZRange | null; + extent?: Bounds; + tileSize?: number; + modelMatrix?: Matrix4; + modelMatrixInverse?: Matrix4; + zoomOffset?: number; +}) { + let z = viewport.isGeospatial + ? Math.round(viewport.zoom + Math.log2(TILE_SIZE / tileSize)) + zoomOffset + : Math.ceil(viewport.zoom) + zoomOffset; + if (typeof minZoom === "number" && Number.isFinite(minZoom) && z < minZoom) { + if (!extent) { + return []; + } + z = minZoom; + } + if (typeof maxZoom === "number" && Number.isFinite(maxZoom) && z > maxZoom) { + z = maxZoom; + } + let transformedExtent = extent; + if (modelMatrix && modelMatrixInverse && extent && !viewport.isGeospatial) { + transformedExtent = transformBox(extent, modelMatrix); + } + return viewport.isGeospatial + ? getOSMTileIndices(viewport, z, zRange, extent) + : getIdentityTileIndices( + viewport, + z, + tileSize, + transformedExtent || DEFAULT_EXTENT, + modelMatrixInverse, + ); +} + +/** + * Returns true if s is a valid URL template + */ +export function isURLTemplate(s: string): boolean { + return /(?=.*{z})(?=.*{x})(?=.*({y}|{-y}))/.test(s); +} + +export function isGeoBoundingBox(v: any): v is GeoBoundingBox { + return ( + Number.isFinite(v.west) && + Number.isFinite(v.north) && + Number.isFinite(v.east) && + Number.isFinite(v.south) + ); +} diff --git a/src/model/layer/index.ts b/src/model/layer/index.ts index 0c5c62c9..d2d7adc5 100644 --- a/src/model/layer/index.ts +++ b/src/model/layer/index.ts @@ -16,7 +16,7 @@ import { SolidPolygonModel, } from "./polygon.js"; import { ScatterplotModel } from "./scatterplot.js"; -import { SurfaceModel } from "./surface.js"; +import { COGTileModel, SurfaceModel } from "./surface.js"; import { TextModel } from "./text.js"; import { TripsModel } from "./trips.js"; @@ -33,7 +33,7 @@ export { SolidPolygonModel, } from "./polygon.js"; export { ScatterplotModel } from "./scatterplot.js"; -export { SurfaceModel } from "./surface.js"; +export { COGTileModel, SurfaceModel } from "./surface.js"; export { TextModel } from "./text.js"; export { TripsModel } from "./trips.js"; @@ -60,6 +60,10 @@ export async function initializeLayer( layerModel = new BitmapTileModel(model, updateStateCallback); break; + case COGTileModel.layerType: + layerModel = new COGTileModel(model, updateStateCallback); + break; + case ColumnModel.layerType: layerModel = new ColumnModel(model, updateStateCallback); break; diff --git a/src/model/layer/surface.ts b/src/model/layer/surface.ts index d95ea135..3922c87a 100644 --- a/src/model/layer/surface.ts +++ b/src/model/layer/surface.ts @@ -1,8 +1,17 @@ +import { TileLayer, TileLayerProps } from "@deck.gl/geo-layers"; +import type { Tileset2DProps } from "@deck.gl/geo-layers/dist/tileset-2d"; +import { PathLayer } from "@deck.gl/layers"; import { SimpleMeshLayer, SimpleMeshLayerProps } from "@deck.gl/mesh-layers"; import type { WidgetModel } from "@jupyter-widgets/base"; import * as arrow from "apache-arrow"; +import GeoTIFF, { fromUrl } from "geotiff"; import { BaseLayerModel } from "./base.js"; +import { + COGTileset2D, + extractCOGMetadata, +} from "../../cog-tileset/claude-tileset-2d-improved.js"; +import { COGMetadata } from "../../cog-tileset/types.js"; import { isDefined } from "../../util.js"; export class SurfaceModel extends BaseLayerModel { @@ -105,3 +114,142 @@ export class SurfaceModel extends BaseLayerModel { }); } } + +export class COGTileModel extends BaseLayerModel { + static layerType = "cog-tile"; + + protected data!: string; + protected tileSize: TileLayerProps["tileSize"]; + protected zoomOffset: TileLayerProps["zoomOffset"]; + protected maxZoom: TileLayerProps["maxZoom"]; + protected minZoom: TileLayerProps["minZoom"]; + protected extent: TileLayerProps["extent"]; + protected maxCacheSize: TileLayerProps["maxCacheSize"]; + protected maxCacheByteSize: TileLayerProps["maxCacheByteSize"]; + protected refinementStrategy: TileLayerProps["refinementStrategy"]; + protected maxRequests: TileLayerProps["maxRequests"]; + + protected tiff?: GeoTIFF; + protected cogMetadata?: COGMetadata; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("data", "data"); + + this.initRegularAttribute("tile_size", "tileSize"); + this.initRegularAttribute("zoom_offset", "zoomOffset"); + this.initRegularAttribute("max_zoom", "maxZoom"); + this.initRegularAttribute("min_zoom", "minZoom"); + this.initRegularAttribute("extent", "extent"); + this.initRegularAttribute("max_cache_size", "maxCacheSize"); + this.initRegularAttribute("max_cache_byte_size", "maxCacheByteSize"); + this.initRegularAttribute("refinement_strategy", "refinementStrategy"); + this.initRegularAttribute("max_requests", "maxRequests"); + } + + async asyncInit() { + console.log("Loading COG from URL:", this.data); + const tiff = await fromUrl(this.data); + const metadata = await extractCOGMetadata(tiff); + + this.tiff = tiff; + this.cogMetadata = metadata; + } + + async loadSubModels() { + await this.asyncInit(); + } + + layerProps(): TileLayerProps { + // Create a factory class that wraps COGTileset2D with the metadata + if (!this.cogMetadata) { + throw new Error("COG metadata not loaded. Call asyncInit first."); + } + + // Capture cogMetadata in closure with proper type + const cogMetadata: COGMetadata = this.cogMetadata; + + class COGTilesetWrapper extends COGTileset2D { + constructor(opts: Tileset2DProps) { + super(cogMetadata, opts); + } + } + + return { + id: this.model.model_id, + data: this.data, + TilesetClass: COGTilesetWrapper, + ...(isDefined(this.tileSize) && { tileSize: this.tileSize }), + ...(isDefined(this.zoomOffset) && { zoomOffset: this.zoomOffset }), + ...(isDefined(this.maxZoom) && { maxZoom: this.maxZoom }), + ...(isDefined(this.minZoom) && { minZoom: this.minZoom }), + ...(isDefined(this.extent) && { extent: this.extent }), + ...(isDefined(this.maxCacheSize) && { maxCacheSize: this.maxCacheSize }), + ...(isDefined(this.maxCacheByteSize) && { + maxCacheByteSize: this.maxCacheByteSize, + }), + ...(isDefined(this.refinementStrategy) && { + refinementStrategy: this.refinementStrategy, + }), + ...(isDefined(this.maxRequests) && { maxRequests: this.maxRequests }), + }; + } + + render(): TileLayer[] { + // Capture cogMetadata in closure + const metadata = this.cogMetadata; + + const layer = new TileLayer({ + ...this.baseLayerProps(), + ...this.layerProps(), + + renderSubLayers: (props) => { + const { tile } = props; + console.log("Rendering COG tile with props:"); + console.log(props); + + // Get projected bounds from tile data + // getTileMetadata returns data that includes projectedBounds + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const projectedBounds = (tile as any)?.projectedBounds; + + if (!projectedBounds || !metadata) { + return []; + } + + // Project bounds from image CRS to WGS84 + const { topLeft, topRight, bottomLeft, bottomRight } = projectedBounds; + + const topLeftWgs84 = metadata.projectToWgs84.forward(topLeft); + const topRightWgs84 = metadata.projectToWgs84.forward(topRight); + const bottomRightWgs84 = metadata.projectToWgs84.forward(bottomRight); + const bottomLeftWgs84 = metadata.projectToWgs84.forward(bottomLeft); + + // Create a closed path around the tile bounds + const path = [ + topLeftWgs84, + topRightWgs84, + bottomRightWgs84, + bottomLeftWgs84, + topLeftWgs84, // Close the path + ]; + + console.log("Tile bounds path (WGS84):", path); + + return [ + new PathLayer({ + id: `${tile.id}-bounds`, + data: [{ path }], + getPath: (d) => d.path, + getColor: [255, 0, 0, 255], // Red + getWidth: 2, + widthUnits: "pixels", + pickable: false, + }), + ]; + }, + }); + return [layer]; + } +}