@walkthru-earth/objex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +396 -0
- package/README.md +114 -0
- package/dist/assets/favicon.svg +17 -0
- package/dist/components/CLAUDE.md +44 -0
- package/dist/components/browser/Breadcrumb.svelte +50 -0
- package/dist/components/browser/Breadcrumb.svelte.d.ts +7 -0
- package/dist/components/browser/CreateFolderDialog.svelte +98 -0
- package/dist/components/browser/CreateFolderDialog.svelte.d.ts +6 -0
- package/dist/components/browser/DeleteConfirmDialog.svelte +90 -0
- package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +8 -0
- package/dist/components/browser/DropZone.svelte +83 -0
- package/dist/components/browser/DropZone.svelte.d.ts +7 -0
- package/dist/components/browser/FileBrowser.svelte +229 -0
- package/dist/components/browser/FileBrowser.svelte.d.ts +3 -0
- package/dist/components/browser/FileRow.svelte +112 -0
- package/dist/components/browser/FileRow.svelte.d.ts +9 -0
- package/dist/components/browser/FileTreeSidebar.svelte +559 -0
- package/dist/components/browser/FileTreeSidebar.svelte.d.ts +8 -0
- package/dist/components/browser/RenameDialog.svelte +101 -0
- package/dist/components/browser/RenameDialog.svelte.d.ts +8 -0
- package/dist/components/browser/SearchBar.svelte +40 -0
- package/dist/components/browser/SearchBar.svelte.d.ts +6 -0
- package/dist/components/browser/UploadButton.svelte +65 -0
- package/dist/components/browser/UploadButton.svelte.d.ts +3 -0
- package/dist/components/editor/CodeMirrorEditor.svelte +404 -0
- package/dist/components/editor/CodeMirrorEditor.svelte.d.ts +12 -0
- package/dist/components/editor/MilkdownEditor.svelte +98 -0
- package/dist/components/editor/MilkdownEditor.svelte.d.ts +9 -0
- package/dist/components/editor/SqlEditor.svelte +173 -0
- package/dist/components/editor/SqlEditor.svelte.d.ts +7 -0
- package/dist/components/editor/SqlResultBlock.svelte +199 -0
- package/dist/components/editor/SqlResultBlock.svelte.d.ts +9 -0
- package/dist/components/layout/ConnectionDialog.svelte +439 -0
- package/dist/components/layout/ConnectionDialog.svelte.d.ts +9 -0
- package/dist/components/layout/LocaleToggle.svelte +32 -0
- package/dist/components/layout/LocaleToggle.svelte.d.ts +3 -0
- package/dist/components/layout/SafeLockToggle.svelte +37 -0
- package/dist/components/layout/SafeLockToggle.svelte.d.ts +18 -0
- package/dist/components/layout/Sidebar.svelte +314 -0
- package/dist/components/layout/Sidebar.svelte.d.ts +3 -0
- package/dist/components/layout/StatusBar.svelte +73 -0
- package/dist/components/layout/StatusBar.svelte.d.ts +3 -0
- package/dist/components/layout/TabBar.svelte +102 -0
- package/dist/components/layout/TabBar.svelte.d.ts +7 -0
- package/dist/components/layout/ThemeToggle.svelte +52 -0
- package/dist/components/layout/ThemeToggle.svelte.d.ts +3 -0
- package/dist/components/ui/badge/badge.svelte +49 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +32 -0
- package/dist/components/ui/badge/index.d.ts +1 -0
- package/dist/components/ui/badge/index.js +1 -0
- package/dist/components/ui/button/button.svelte +82 -0
- package/dist/components/ui/button/button.svelte.d.ts +64 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +4 -0
- package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte +40 -0
- package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte.d.ts +9 -0
- package/dist/components/ui/context-menu/context-menu-content.svelte +28 -0
- package/dist/components/ui/context-menu/context-menu-content.svelte.d.ts +10 -0
- package/dist/components/ui/context-menu/context-menu-group-heading.svelte +21 -0
- package/dist/components/ui/context-menu/context-menu-group-heading.svelte.d.ts +7 -0
- package/dist/components/ui/context-menu/context-menu-group.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-group.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-item.svelte +27 -0
- package/dist/components/ui/context-menu/context-menu-item.svelte.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu-label.svelte +24 -0
- package/dist/components/ui/context-menu/context-menu-label.svelte.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu-portal.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-portal.svelte.d.ts +3 -0
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte +16 -0
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-radio-item.svelte +33 -0
- package/dist/components/ui/context-menu/context-menu-radio-item.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-separator.svelte +17 -0
- package/dist/components/ui/context-menu/context-menu-separator.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-shortcut.svelte +20 -0
- package/dist/components/ui/context-menu/context-menu-shortcut.svelte.d.ts +5 -0
- package/dist/components/ui/context-menu/context-menu-sub-content.svelte +20 -0
- package/dist/components/ui/context-menu/context-menu-sub-content.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte +29 -0
- package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu-sub.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-sub.svelte.d.ts +3 -0
- package/dist/components/ui/context-menu/context-menu-trigger.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu.svelte.d.ts +3 -0
- package/dist/components/ui/context-menu/index.d.ts +17 -0
- package/dist/components/ui/context-menu/index.js +19 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +9 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +29 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +10 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +33 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte.d.ts +5 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/index.d.ts +18 -0
- package/dist/components/ui/dropdown-menu/index.js +18 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +52 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/resizable/index.d.ts +4 -0
- package/dist/components/ui/resizable/index.js +6 -0
- package/dist/components/ui/resizable/resizable-handle.svelte +30 -0
- package/dist/components/ui/resizable/resizable-handle.svelte.d.ts +8 -0
- package/dist/components/ui/resizable/resizable-pane-group.svelte +20 -0
- package/dist/components/ui/resizable/resizable-pane-group.svelte.d.ts +7 -0
- package/dist/components/ui/scroll-area/index.d.ts +3 -0
- package/dist/components/ui/scroll-area/index.js +5 -0
- package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte +31 -0
- package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte.d.ts +4 -0
- package/dist/components/ui/scroll-area/scroll-area.svelte +47 -0
- package/dist/components/ui/scroll-area/scroll-area.svelte.d.ts +11 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/index.js +4 -0
- package/dist/components/ui/separator/separator.svelte +21 -0
- package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/index.d.ts +11 -0
- package/dist/components/ui/sheet/index.js +13 -0
- package/dist/components/ui/sheet/sheet-close.svelte +7 -0
- package/dist/components/ui/sheet/sheet-close.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-content.svelte +62 -0
- package/dist/components/ui/sheet/sheet-content.svelte.d.ts +37 -0
- package/dist/components/ui/sheet/sheet-description.svelte +17 -0
- package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-portal.svelte +7 -0
- package/dist/components/ui/sheet/sheet-portal.svelte.d.ts +3 -0
- package/dist/components/ui/sheet/sheet-title.svelte +13 -0
- package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/components/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet.svelte +7 -0
- package/dist/components/ui/sheet/sheet.svelte.d.ts +3 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +28 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +8 -0
- package/dist/components/ui/tabs/index.d.ts +5 -0
- package/dist/components/ui/tabs/index.js +7 -0
- package/dist/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +16 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +20 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs.svelte +19 -0
- package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/index.d.ts +6 -0
- package/dist/components/ui/tooltip/index.js +8 -0
- package/dist/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +11 -0
- package/dist/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-portal.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-provider.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip.svelte.d.ts +4 -0
- package/dist/components/viewers/ArchiveViewer.svelte +586 -0
- package/dist/components/viewers/ArchiveViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/CLAUDE.md +60 -0
- package/dist/components/viewers/CodeViewer.svelte +553 -0
- package/dist/components/viewers/CodeViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/CogViewer.svelte +1345 -0
- package/dist/components/viewers/CogViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/CopcViewer.svelte +25 -0
- package/dist/components/viewers/CopcViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/DatabaseViewer.svelte +169 -0
- package/dist/components/viewers/DatabaseViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/FileInfo.svelte +174 -0
- package/dist/components/viewers/FileInfo.svelte.d.ts +10 -0
- package/dist/components/viewers/FlatGeobufViewer.svelte +755 -0
- package/dist/components/viewers/FlatGeobufViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/GeoParquetMapViewer.svelte +278 -0
- package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +17 -0
- package/dist/components/viewers/ImageViewer.svelte +233 -0
- package/dist/components/viewers/ImageViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/LoadProgress.svelte +93 -0
- package/dist/components/viewers/LoadProgress.svelte.d.ts +15 -0
- package/dist/components/viewers/MapViewer.svelte +234 -0
- package/dist/components/viewers/MapViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/MarkdownViewer.svelte +478 -0
- package/dist/components/viewers/MarkdownViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/MediaViewer.svelte +121 -0
- package/dist/components/viewers/MediaViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/ModelViewer.svelte +164 -0
- package/dist/components/viewers/ModelViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/NotebookViewer.svelte +389 -0
- package/dist/components/viewers/NotebookViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/PdfViewer.svelte +278 -0
- package/dist/components/viewers/PdfViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/PmtilesViewer.svelte +191 -0
- package/dist/components/viewers/PmtilesViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/QueryHistoryPanel.svelte +159 -0
- package/dist/components/viewers/QueryHistoryPanel.svelte.d.ts +8 -0
- package/dist/components/viewers/RawViewer.svelte +117 -0
- package/dist/components/viewers/RawViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/StacMapViewer.svelte +20 -0
- package/dist/components/viewers/StacMapViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/StyleEditorOverlay.svelte +27 -0
- package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +7 -0
- package/dist/components/viewers/TableGrid.svelte +355 -0
- package/dist/components/viewers/TableGrid.svelte.d.ts +12 -0
- package/dist/components/viewers/TableStatusBar.svelte +92 -0
- package/dist/components/viewers/TableStatusBar.svelte.d.ts +11 -0
- package/dist/components/viewers/TableToolbar.svelte +382 -0
- package/dist/components/viewers/TableToolbar.svelte.d.ts +25 -0
- package/dist/components/viewers/TableViewer.svelte +923 -0
- package/dist/components/viewers/TableViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/ViewerRouter.svelte +70 -0
- package/dist/components/viewers/ViewerRouter.svelte.d.ts +7 -0
- package/dist/components/viewers/ZarrMapViewer.svelte +288 -0
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +17 -0
- package/dist/components/viewers/ZarrViewer.svelte +256 -0
- package/dist/components/viewers/ZarrViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/map/AttributeTable.svelte +52 -0
- package/dist/components/viewers/map/AttributeTable.svelte.d.ts +8 -0
- package/dist/components/viewers/map/MapContainer.svelte +158 -0
- package/dist/components/viewers/map/MapContainer.svelte.d.ts +12 -0
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +389 -0
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte.d.ts +10 -0
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +332 -0
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte.d.ts +11 -0
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +373 -0
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte.d.ts +12 -0
- package/dist/components/viewers/pmtiles/SvgTileRenderer.svelte +112 -0
- package/dist/components/viewers/pmtiles/SvgTileRenderer.svelte.d.ts +10 -0
- package/dist/file-icons/CLAUDE.md +21 -0
- package/dist/file-icons/FileTypeIcon.svelte +74 -0
- package/dist/file-icons/FileTypeIcon.svelte.d.ts +9 -0
- package/dist/file-icons/index.d.ts +56 -0
- package/dist/file-icons/index.js +1070 -0
- package/dist/i18n/CLAUDE.md +19 -0
- package/dist/i18n/ar.d.ts +1 -0
- package/dist/i18n/ar.js +404 -0
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.js +404 -0
- package/dist/i18n/index.svelte.d.ts +9 -0
- package/dist/i18n/index.svelte.js +27 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -0
- package/dist/query/CLAUDE.md +22 -0
- package/dist/query/engine.d.ts +56 -0
- package/dist/query/engine.js +6 -0
- package/dist/query/index.d.ts +4 -0
- package/dist/query/index.js +19 -0
- package/dist/query/wasm.d.ts +20 -0
- package/dist/query/wasm.js +890 -0
- package/dist/storage/CLAUDE.md +23 -0
- package/dist/storage/adapter.d.ts +21 -0
- package/dist/storage/adapter.js +1 -0
- package/dist/storage/browser-azure.d.ts +25 -0
- package/dist/storage/browser-azure.js +271 -0
- package/dist/storage/browser-cloud.d.ts +32 -0
- package/dist/storage/browser-cloud.js +293 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +37 -0
- package/dist/storage/url-adapter.d.ts +19 -0
- package/dist/storage/url-adapter.js +51 -0
- package/dist/stores/CLAUDE.md +29 -0
- package/dist/stores/browser.svelte.d.ts +28 -0
- package/dist/stores/browser.svelte.js +160 -0
- package/dist/stores/connections.svelte.d.ts +56 -0
- package/dist/stores/connections.svelte.js +272 -0
- package/dist/stores/credentials.svelte.d.ts +56 -0
- package/dist/stores/credentials.svelte.js +79 -0
- package/dist/stores/files.svelte.d.ts +20 -0
- package/dist/stores/files.svelte.js +76 -0
- package/dist/stores/query-history.svelte.d.ts +16 -0
- package/dist/stores/query-history.svelte.js +57 -0
- package/dist/stores/safelock.svelte.d.ts +8 -0
- package/dist/stores/safelock.svelte.js +52 -0
- package/dist/stores/settings.svelte.d.ts +11 -0
- package/dist/stores/settings.svelte.js +101 -0
- package/dist/stores/tab-resources.svelte.d.ts +25 -0
- package/dist/stores/tab-resources.svelte.js +61 -0
- package/dist/stores/tabs.svelte.d.ts +17 -0
- package/dist/stores/tabs.svelte.js +110 -0
- package/dist/types/notebookjs.d.ts +14 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/dist/utils/CLAUDE.md +54 -0
- package/dist/utils/analytics.d.ts +10 -0
- package/dist/utils/analytics.js +38 -0
- package/dist/utils/archive.d.ts +70 -0
- package/dist/utils/archive.js +333 -0
- package/dist/utils/column-types.d.ts +5 -0
- package/dist/utils/column-types.js +137 -0
- package/dist/utils/deck.d.ts +98 -0
- package/dist/utils/deck.js +208 -0
- package/dist/utils/evidence-context.d.ts +22 -0
- package/dist/utils/evidence-context.js +56 -0
- package/dist/utils/export.d.ts +2 -0
- package/dist/utils/export.js +51 -0
- package/dist/utils/format.d.ts +14 -0
- package/dist/utils/format.js +56 -0
- package/dist/utils/geoarrow.d.ts +32 -0
- package/dist/utils/geoarrow.js +672 -0
- package/dist/utils/hex.d.ts +10 -0
- package/dist/utils/hex.js +27 -0
- package/dist/utils/host-detection.d.ts +23 -0
- package/dist/utils/host-detection.js +289 -0
- package/dist/utils/map-selection.d.ts +12 -0
- package/dist/utils/map-selection.js +45 -0
- package/dist/utils/markdown-sql.d.ts +30 -0
- package/dist/utils/markdown-sql.js +73 -0
- package/dist/utils/markdown.d.ts +18 -0
- package/dist/utils/markdown.js +146 -0
- package/dist/utils/model3d.d.ts +13 -0
- package/dist/utils/model3d.js +62 -0
- package/dist/utils/parquet-metadata.d.ts +58 -0
- package/dist/utils/parquet-metadata.js +228 -0
- package/dist/utils/pdf.d.ts +8 -0
- package/dist/utils/pdf.js +28 -0
- package/dist/utils/pmtiles-tile.d.ts +38 -0
- package/dist/utils/pmtiles-tile.js +64 -0
- package/dist/utils/pmtiles.d.ts +46 -0
- package/dist/utils/pmtiles.js +135 -0
- package/dist/utils/shiki.d.ts +8 -0
- package/dist/utils/shiki.js +98 -0
- package/dist/utils/storage-url.d.ts +64 -0
- package/dist/utils/storage-url.js +374 -0
- package/dist/utils/url-state.d.ts +40 -0
- package/dist/utils/url-state.js +113 -0
- package/dist/utils/url.d.ts +27 -0
- package/dist/utils/url.js +115 -0
- package/dist/utils/wkb.d.ts +43 -0
- package/dist/utils/wkb.js +345 -0
- package/dist/utils/zarr.d.ts +39 -0
- package/dist/utils/zarr.js +204 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +5 -0
- package/package.json +203 -0
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { MapboxOverlay } from '@deck.gl/mapbox';
|
|
3
|
+
import { COGLayer, parseCOGTileMatrixSet, proj } from '@developmentseed/deck.gl-geotiff';
|
|
4
|
+
import { fromUrl } from 'geotiff';
|
|
5
|
+
import { toProj4 } from 'geotiff-geokeys-to-proj4';
|
|
6
|
+
import type maplibregl from 'maplibre-gl';
|
|
7
|
+
import proj4Lib from 'proj4';
|
|
8
|
+
import { onDestroy, untrack } from 'svelte';
|
|
9
|
+
import { t } from '../../i18n/index.svelte.js';
|
|
10
|
+
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
11
|
+
import type { Tab } from '../../types';
|
|
12
|
+
import { buildHttpsUrl } from '../../utils/url.js';
|
|
13
|
+
import MapContainer from './map/MapContainer.svelte';
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const SF_LABELS: Record<number, string> = {
|
|
18
|
+
1: 'uint',
|
|
19
|
+
2: 'int',
|
|
20
|
+
3: 'float',
|
|
21
|
+
4: 'void',
|
|
22
|
+
5: 'complex int',
|
|
23
|
+
6: 'complex float'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* GeoTIFF projection code → proj4 mapping for projections that
|
|
28
|
+
* geotiff-geokeys-to-proj4 doesn't recognise (returns longlat fallback).
|
|
29
|
+
* Key = ProjCoordTransGeoKey value from GeoTIFF spec section 6.3.3.3.
|
|
30
|
+
*/
|
|
31
|
+
const PROJ_CT_FALLBACK: Record<number, string> = {
|
|
32
|
+
11: '+proj=moll', // CT_Mollweide
|
|
33
|
+
12: '+proj=eck4', // CT_EckertIV
|
|
34
|
+
13: '+proj=eck6', // CT_EckertVI
|
|
35
|
+
14: '+proj=vandg', // CT_VanDerGrinten
|
|
36
|
+
15: '+proj=robin', // CT_Robinson
|
|
37
|
+
16: '+proj=sinu' // CT_Sinusoidal
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ESRI PE String PROJECTION name → proj4 +proj parameter.
|
|
42
|
+
* Many GeoTIFFs (especially from ArcGIS/rasterio) embed the projection
|
|
43
|
+
* as an ESRI PE String in PCSCitationGeoKey rather than setting
|
|
44
|
+
* ProjCoordTransGeoKey. This map handles common pseudo-cylindrical
|
|
45
|
+
* and other projections that toProj4 can't parse.
|
|
46
|
+
*/
|
|
47
|
+
const ESRI_PROJ_MAP: Record<string, string> = {
|
|
48
|
+
Mollweide: '+proj=moll',
|
|
49
|
+
Eckert_IV: '+proj=eck4',
|
|
50
|
+
Eckert_VI: '+proj=eck6',
|
|
51
|
+
Van_der_Grinten_I: '+proj=vandg',
|
|
52
|
+
Robinson: '+proj=robin',
|
|
53
|
+
Sinusoidal: '+proj=sinu',
|
|
54
|
+
Goode_Homolosine: '+proj=igh',
|
|
55
|
+
Winkel_Tripel: '+proj=wintri'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Try to extract a proj4 string from ESRI PE String in PCSCitationGeoKey.
|
|
60
|
+
* Returns null if no recognized projection is found.
|
|
61
|
+
*/
|
|
62
|
+
function tryParseEsriCitation(geoKeys: Record<string, unknown>): string | null {
|
|
63
|
+
const citation = geoKeys.PCSCitationGeoKey as string | undefined;
|
|
64
|
+
if (!citation) return null;
|
|
65
|
+
|
|
66
|
+
// Extract PROJECTION["Name"] from the ESRI PE string
|
|
67
|
+
const match = citation.match(/PROJECTION\["([^"]+)"\]/);
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
|
|
70
|
+
const projName = match[1];
|
|
71
|
+
const proj4Proj = ESRI_PROJ_MAP[projName];
|
|
72
|
+
if (!proj4Proj) return null;
|
|
73
|
+
|
|
74
|
+
// Extract parameters
|
|
75
|
+
const params: Record<string, number> = {};
|
|
76
|
+
const paramRe = /PARAMETER\["([^"]+)",([\s\S]*?)\]/g;
|
|
77
|
+
for (const m of citation.matchAll(paramRe)) {
|
|
78
|
+
params[m[1]] = parseFloat(m[2]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lon0 = params.Central_Meridian ?? params.central_meridian ?? 0;
|
|
82
|
+
const fe = params.False_Easting ?? params.false_easting ?? 0;
|
|
83
|
+
const fn = params.False_Northing ?? params.false_northing ?? 0;
|
|
84
|
+
|
|
85
|
+
return `${proj4Proj} +lon_0=${lon0} +x_0=${fe} +y_0=${fn} +datum=WGS84 +units=m +no_defs`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Custom GeoKeys parser using geotiff-geokeys-to-proj4.
|
|
90
|
+
* Bypasses the default proj4 EPSG lookup (which fails for non-standard CRS codes
|
|
91
|
+
* like EPSG:32767) by parsing GeoKeys directly into a proj4 definition string.
|
|
92
|
+
*
|
|
93
|
+
* Falls back to manual proj4 construction for user-defined projections
|
|
94
|
+
* detected via ProjCoordTransGeoKey or ESRI PE String in PCSCitationGeoKey.
|
|
95
|
+
*/
|
|
96
|
+
async function geoKeysParser(
|
|
97
|
+
geoKeys: Record<string, unknown>
|
|
98
|
+
): Promise<proj.ProjectionInfo | null> {
|
|
99
|
+
try {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
const projDef = toProj4(geoKeys as any);
|
|
102
|
+
const def = projDef.proj4 as string;
|
|
103
|
+
|
|
104
|
+
// Detect misdetected CRS: toProj4 returns +proj=longlat for projections
|
|
105
|
+
// it doesn't handle. Check two sources:
|
|
106
|
+
// 1. ProjCoordTransGeoKey (GeoTIFF standard projection codes)
|
|
107
|
+
// 2. ESRI PE String in PCSCitationGeoKey (common in ArcGIS/rasterio output)
|
|
108
|
+
if (def.includes('+proj=longlat')) {
|
|
109
|
+
let corrected: string | null = null;
|
|
110
|
+
|
|
111
|
+
// Try ProjCoordTransGeoKey first
|
|
112
|
+
const ct = geoKeys.ProjCoordTransGeoKey as number | undefined;
|
|
113
|
+
if (ct && PROJ_CT_FALLBACK[ct]) {
|
|
114
|
+
const lon0 = (geoKeys.ProjNatOriginLongGeoKey ??
|
|
115
|
+
geoKeys.ProjCenterLongGeoKey ??
|
|
116
|
+
0) as number;
|
|
117
|
+
const fe = (geoKeys.ProjFalseEastingGeoKey ?? 0) as number;
|
|
118
|
+
const fn = (geoKeys.ProjFalseNorthingGeoKey ?? 0) as number;
|
|
119
|
+
corrected = `${PROJ_CT_FALLBACK[ct]} +lon_0=${lon0} +x_0=${fe} +y_0=${fn} +datum=WGS84 +units=m +no_defs`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Try ESRI PE String in PCSCitationGeoKey
|
|
123
|
+
if (!corrected) {
|
|
124
|
+
corrected = tryParseEsriCitation(geoKeys);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (corrected) {
|
|
128
|
+
console.log(`[COG:geokeys] corrected CRS from longlat → ${corrected}`);
|
|
129
|
+
return {
|
|
130
|
+
def: corrected,
|
|
131
|
+
parsed: proj.parseCrs(corrected),
|
|
132
|
+
coordinatesUnits: 'metre' as proj.SupportedCrsUnit
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
def,
|
|
139
|
+
parsed: proj.parseCrs(def),
|
|
140
|
+
coordinatesUnits: projDef.coordinatesUnits as proj.SupportedCrsUnit
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Query the browser's WebGL MAX_TEXTURE_SIZE from a MapLibre map.
|
|
149
|
+
* This is the hard limit for any single texture upload (canvas, tile, image source).
|
|
150
|
+
* Falls back to 4096 (lowest common denominator for mobile GPUs).
|
|
151
|
+
*/
|
|
152
|
+
function getMaxTextureSize(map: maplibregl.Map): number {
|
|
153
|
+
try {
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
+
const gl = (map as any).painter?.context?.gl as WebGL2RenderingContext | null;
|
|
156
|
+
if (gl) return gl.getParameter(gl.MAX_TEXTURE_SIZE) as number;
|
|
157
|
+
} catch {
|
|
158
|
+
/* fallback */
|
|
159
|
+
}
|
|
160
|
+
return 4096;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Safely clamp a number to a range, treating NaN/Infinity as the fallback. */
|
|
164
|
+
function safeClamp(v: number, lo: number, hi: number, fallback: number): number {
|
|
165
|
+
return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : fallback;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Clamp geographic bounds to valid MapLibre web-Mercator range. */
|
|
169
|
+
function clampBounds(b: { west: number; south: number; east: number; north: number }) {
|
|
170
|
+
return {
|
|
171
|
+
west: safeClamp(b.west, -180, 180, -180),
|
|
172
|
+
south: safeClamp(b.south, -85.051129, 85.051129, -85.051129),
|
|
173
|
+
east: safeClamp(b.east, -180, 180, 180),
|
|
174
|
+
north: safeClamp(b.north, -85.051129, 85.051129, 85.051129)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fit the map to COG bounds with responsive padding.
|
|
180
|
+
* Uses smaller padding on mobile to zoom in closer, ensuring overviews load
|
|
181
|
+
* properly instead of appearing black at very low zoom levels.
|
|
182
|
+
* After fitting, bumps zoom +1 when the viewport settles at a very low level.
|
|
183
|
+
*/
|
|
184
|
+
function fitCogBounds(
|
|
185
|
+
map: maplibregl.Map,
|
|
186
|
+
b: { west: number; south: number; east: number; north: number }
|
|
187
|
+
) {
|
|
188
|
+
const isMobile = window.innerWidth < 640;
|
|
189
|
+
const viewportMin = Math.min(window.innerWidth, window.innerHeight);
|
|
190
|
+
const padding = isMobile ? 5 : Math.max(10, Math.round(viewportMin * 0.04));
|
|
191
|
+
map.fitBounds(
|
|
192
|
+
[
|
|
193
|
+
[b.west, b.south],
|
|
194
|
+
[b.east, b.north]
|
|
195
|
+
],
|
|
196
|
+
{ padding, maxZoom: 18, speed: 1.2, maxDuration: 2000 }
|
|
197
|
+
);
|
|
198
|
+
// On small screens, fitBounds settles at a zoom too low for overviews
|
|
199
|
+
// to render (appears black). Bump zoom so the first overview tile loads.
|
|
200
|
+
// Skip for large-extent COGs — bumping zoom at global scale pushes the
|
|
201
|
+
// TileLayer into its finest overview, requesting hundreds of tiles.
|
|
202
|
+
const lonSpan = b.east - b.west;
|
|
203
|
+
const latSpan = b.north - b.south;
|
|
204
|
+
const isLargeExtent = lonSpan > 90 || latSpan > 45;
|
|
205
|
+
if (!isLargeExtent) {
|
|
206
|
+
map.once('moveend', () => {
|
|
207
|
+
const z = map.getZoom();
|
|
208
|
+
const minZoom = isMobile ? 10 : 8;
|
|
209
|
+
if (z < minZoom) {
|
|
210
|
+
map.zoomTo(z + 2, { duration: 500 });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fix metadata from parseCOGTileMatrixSet for projections where corner
|
|
218
|
+
* reprojection produces NaN/extreme values (Mollweide, global EPSG:4326, etc.).
|
|
219
|
+
* Clamps wgsBounds and wraps projectTo3857/projectToWgs84 with safe fallbacks.
|
|
220
|
+
*/
|
|
221
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
222
|
+
function patchMetadataBounds(metadata: any) {
|
|
223
|
+
// Clamp wgsBounds — corners of projections like Mollweide can be
|
|
224
|
+
// outside the valid domain, producing extreme longitudes (e.g. ±277°)
|
|
225
|
+
// or latitudes slightly outside [-90,90] (e.g. EPSG:4326 at ±90.002°).
|
|
226
|
+
// deck.gl's lngLatToWorld asserts lat ∈ [-90,90].
|
|
227
|
+
const wb = metadata.wgsBounds;
|
|
228
|
+
if (wb) {
|
|
229
|
+
metadata.wgsBounds = {
|
|
230
|
+
lowerLeft: [
|
|
231
|
+
safeClamp(wb.lowerLeft[0], -180, 180, -180),
|
|
232
|
+
safeClamp(wb.lowerLeft[1], -85.051129, 85.051129, -85.051129)
|
|
233
|
+
],
|
|
234
|
+
upperRight: [
|
|
235
|
+
safeClamp(wb.upperRight[0], -180, 180, 180),
|
|
236
|
+
safeClamp(wb.upperRight[1], -85.051129, 85.051129, 85.051129)
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Wrap projectTo3857 — out-of-domain points produce NaN/Infinity in EPSG:3857
|
|
242
|
+
const origTo3857 = metadata.projectTo3857;
|
|
243
|
+
if (origTo3857) {
|
|
244
|
+
metadata.projectTo3857 = (point: [number, number]) => {
|
|
245
|
+
const r = origTo3857(point);
|
|
246
|
+
if (Number.isFinite(r[0]) && Number.isFinite(r[1])) return r;
|
|
247
|
+
// Sign-based edge fallback: map out-of-domain points to the
|
|
248
|
+
// nearest edge of EPSG:3857 instead of the origin, reducing
|
|
249
|
+
// adaptive mesh distortion for edge tiles.
|
|
250
|
+
return [
|
|
251
|
+
point[0] >= 0 ? 20037508.34 : -20037508.34,
|
|
252
|
+
point[1] >= 0 ? 20037508.34 : -20037508.34
|
|
253
|
+
];
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Wrap projectToWgs84 — clamp extreme lon/lat from edge reprojection
|
|
258
|
+
const origToWgs84 = metadata.projectToWgs84;
|
|
259
|
+
if (origToWgs84) {
|
|
260
|
+
metadata.projectToWgs84 = (point: [number, number]) => {
|
|
261
|
+
const r = origToWgs84(point);
|
|
262
|
+
return [safeClamp(r[0], -180, 180, 0), safeClamp(r[1], -85.051129, 85.051129, 0)];
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── Monkey-patch COGLayer._parseGeoTIFF ─────────────────────────
|
|
268
|
+
// The library's inferRenderPipeline throws/hangs for PI=0/1 (Gray) and
|
|
269
|
+
// non-uint SampleFormat. For custom pipelines (getTileData/renderTile),
|
|
270
|
+
// skip _origParse entirely and reconstruct state from our v3 GeoTIFF.
|
|
271
|
+
// For default pipelines, _origParse runs normally with a timeout guard.
|
|
272
|
+
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
274
|
+
let capturedV2Geotiff: any = null;
|
|
275
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
|
+
let currentV3Tiff: any = null;
|
|
277
|
+
|
|
278
|
+
// Guard against HMR re-patching: always reference the true original
|
|
279
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
280
|
+
const _origParse = (COGLayer as any).__origParseGeoTIFF ?? COGLayer.prototype._parseGeoTIFF;
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
282
|
+
(COGLayer as any).__origParseGeoTIFF = _origParse;
|
|
283
|
+
|
|
284
|
+
/** Shared reconstruction: build layer state from a geotiff (v2 or v3). */
|
|
285
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
286
|
+
async function reconstructLayerState(layer: any, geotiff: any) {
|
|
287
|
+
const t0 = performance.now();
|
|
288
|
+
const gkParser = layer.props.geoKeysParser;
|
|
289
|
+
|
|
290
|
+
console.log('[COG:reconstruct] parsing tile matrix set...');
|
|
291
|
+
const metadata = await parseCOGTileMatrixSet(geotiff, gkParser);
|
|
292
|
+
patchMetadataBounds(metadata);
|
|
293
|
+
|
|
294
|
+
const image = await geotiff.getImage();
|
|
295
|
+
const imageCount = await geotiff.getImageCount();
|
|
296
|
+
let images: unknown[] = [];
|
|
297
|
+
for (let i = 0; i < imageCount; i++) {
|
|
298
|
+
images.push(await geotiff.getImage(i));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const allLevels = images.map((img, i) => {
|
|
302
|
+
const im = img as { getWidth(): number; getHeight(): number };
|
|
303
|
+
return ` [${i}] ${im.getWidth()}×${im.getHeight()}`;
|
|
304
|
+
});
|
|
305
|
+
console.log(
|
|
306
|
+
`[COG:reconstruct] ${imageCount} IFDs, ${metadata.tileMatrices.length} tile matrices\n${allLevels.join('\n')}`
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Skip overviews smaller than tile size — their tile bounds span
|
|
310
|
+
// most of the globe and produce NaN when projected to Web Mercator.
|
|
311
|
+
let firstValidZ = 0;
|
|
312
|
+
for (let z = 0; z < metadata.tileMatrices.length; z++) {
|
|
313
|
+
const img = images[images.length - 1 - z] as {
|
|
314
|
+
getWidth(): number;
|
|
315
|
+
getHeight(): number;
|
|
316
|
+
};
|
|
317
|
+
const tm = metadata.tileMatrices[z];
|
|
318
|
+
if (img.getWidth() >= tm.tileWidth && img.getHeight() >= tm.tileHeight) {
|
|
319
|
+
firstValidZ = z;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (firstValidZ > 0) {
|
|
324
|
+
console.log(`[COG:reconstruct] skipping ${firstValidZ} sub-tile overviews`);
|
|
325
|
+
metadata.tileMatrices = metadata.tileMatrices.slice(firstValidZ);
|
|
326
|
+
images = images.slice(0, images.length - firstValidZ);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Cap zoom levels — decompression runs synchronously on the main
|
|
330
|
+
// thread (WASM). Too many fine levels overwhelms the browser.
|
|
331
|
+
// Also cap by image dimension: skip levels where the overview has
|
|
332
|
+
// more than MAX_DIM_PER_LEVEL pixels in any direction (too many tiles
|
|
333
|
+
// would be visible at once, blocking the main thread on decode).
|
|
334
|
+
const MAX_TILE_LEVELS = 12;
|
|
335
|
+
const MAX_DIM_PER_LEVEL = 8_192;
|
|
336
|
+
if (metadata.tileMatrices.length > MAX_TILE_LEVELS) {
|
|
337
|
+
const trimmed = metadata.tileMatrices.length - MAX_TILE_LEVELS;
|
|
338
|
+
console.log(
|
|
339
|
+
`[COG:reconstruct] capping zoom: trimming ${trimmed} levels (>${MAX_TILE_LEVELS} max)`
|
|
340
|
+
);
|
|
341
|
+
metadata.tileMatrices = metadata.tileMatrices.slice(0, MAX_TILE_LEVELS);
|
|
342
|
+
images = images.slice(images.length - MAX_TILE_LEVELS);
|
|
343
|
+
}
|
|
344
|
+
// Trim finest levels whose source images are too large
|
|
345
|
+
let dimTrimCount = 0;
|
|
346
|
+
while (images.length > 1) {
|
|
347
|
+
const finest = images[0] as { getWidth(): number; getHeight(): number };
|
|
348
|
+
if (finest.getWidth() <= MAX_DIM_PER_LEVEL && finest.getHeight() <= MAX_DIM_PER_LEVEL) break;
|
|
349
|
+
dimTrimCount++;
|
|
350
|
+
images.shift();
|
|
351
|
+
metadata.tileMatrices.pop();
|
|
352
|
+
}
|
|
353
|
+
if (dimTrimCount > 0) {
|
|
354
|
+
console.log(
|
|
355
|
+
`[COG:reconstruct] trimmed ${dimTrimCount} finest levels (>${MAX_DIM_PER_LEVEL}px per dim)`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const finalLevels = images.map((img, i) => {
|
|
360
|
+
const im = img as { getWidth(): number; getHeight(): number };
|
|
361
|
+
return ` z${i}: ${im.getWidth()}×${im.getHeight()}`;
|
|
362
|
+
});
|
|
363
|
+
console.log(
|
|
364
|
+
`[COG:reconstruct] final: ${images.length} levels, ${metadata.tileMatrices.length} tile matrices\n${finalLevels.join('\n')}`
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const sourceProjection = await gkParser(image.getGeoKeys());
|
|
368
|
+
if (!sourceProjection) throw new Error('Could not determine source projection');
|
|
369
|
+
console.log(`[COG:reconstruct] CRS: ${sourceProjection.def.substring(0, 80)}...`);
|
|
370
|
+
|
|
371
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
372
|
+
const converter = proj4Lib(sourceProjection.def, 'EPSG:4326') as any;
|
|
373
|
+
const forwardReproject = (x: number, y: number) => {
|
|
374
|
+
const r = converter.forward([x, y], false);
|
|
375
|
+
const lon = Number.isFinite(r[0]) ? Math.max(-180, Math.min(180, r[0])) : x >= 0 ? 180 : -180;
|
|
376
|
+
const lat = Number.isFinite(r[1])
|
|
377
|
+
? Math.max(-85.051129, Math.min(85.051129, r[1]))
|
|
378
|
+
: y >= 0
|
|
379
|
+
? 85.051129
|
|
380
|
+
: -85.051129;
|
|
381
|
+
return [lon, lat];
|
|
382
|
+
};
|
|
383
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
384
|
+
const inverseReproject = (x: number, y: number) => converter.inverse([x, y], false);
|
|
385
|
+
|
|
386
|
+
layer.setState({
|
|
387
|
+
metadata,
|
|
388
|
+
forwardReproject,
|
|
389
|
+
inverseReproject,
|
|
390
|
+
images,
|
|
391
|
+
defaultGetTileData: null,
|
|
392
|
+
defaultRenderTile: null
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Trigger onGeoTIFFLoad so the component updates info panel and fits bounds.
|
|
396
|
+
// When we skip _origParse the library never calls this callback itself.
|
|
397
|
+
if (layer.props.onGeoTIFFLoad && metadata.wgsBounds) {
|
|
398
|
+
const wb = metadata.wgsBounds;
|
|
399
|
+
const geographicBounds = {
|
|
400
|
+
west: wb.lowerLeft[0],
|
|
401
|
+
south: wb.lowerLeft[1],
|
|
402
|
+
east: wb.upperRight[0],
|
|
403
|
+
north: wb.upperRight[1]
|
|
404
|
+
};
|
|
405
|
+
console.log(
|
|
406
|
+
`[COG:reconstruct] triggering onGeoTIFFLoad with bounds: ` +
|
|
407
|
+
`W${geographicBounds.west.toFixed(2)} S${geographicBounds.south.toFixed(2)} ` +
|
|
408
|
+
`E${geographicBounds.east.toFixed(2)} N${geographicBounds.north.toFixed(2)}`
|
|
409
|
+
);
|
|
410
|
+
layer.props.onGeoTIFFLoad(geotiff, { projection: null, geographicBounds });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(`[COG:reconstruct] done in ${(performance.now() - t0).toFixed(0)}ms`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
417
|
+
COGLayer.prototype._parseGeoTIFF = async function (this: any) {
|
|
418
|
+
const hasCustomPipeline = this.props.getTileData && this.props.renderTile;
|
|
419
|
+
console.log(
|
|
420
|
+
`[COG:patch] _parseGeoTIFF called — customPipeline=${hasCustomPipeline}, hasV3Tiff=${!!currentV3Tiff}`
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// ── Custom pipeline (Gray / float / int) ──
|
|
424
|
+
// Skip _origParse entirely — it hangs for Gray/float COGs because
|
|
425
|
+
// inferRenderPipeline fails internally without throwing to our catch.
|
|
426
|
+
// Reconstruct directly from our pre-opened v3 geotiff.
|
|
427
|
+
if (hasCustomPipeline && currentV3Tiff) {
|
|
428
|
+
console.log('[COG:patch] custom pipeline → skipping _origParse, reconstructing from v3 tiff');
|
|
429
|
+
try {
|
|
430
|
+
await reconstructLayerState(this, currentV3Tiff);
|
|
431
|
+
console.log('[COG:patch] custom pipeline reconstruction succeeded');
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.error('[COG:patch] custom pipeline reconstruction failed:', err);
|
|
434
|
+
if (this.props.onError) {
|
|
435
|
+
this.props.onError(err instanceof Error ? err : new Error(String(err)));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Default pipeline (RGB / Palette / CMYK) ──
|
|
442
|
+
// Run the library's original parser with a timeout guard.
|
|
443
|
+
const TIMEOUT_MS = 15_000;
|
|
444
|
+
console.log(`[COG:patch] default pipeline → running _origParse with ${TIMEOUT_MS}ms timeout`);
|
|
445
|
+
const t0 = performance.now();
|
|
446
|
+
try {
|
|
447
|
+
await Promise.race([
|
|
448
|
+
_origParse.call(this),
|
|
449
|
+
new Promise((_, reject) =>
|
|
450
|
+
setTimeout(() => reject(new Error('COG parsing timed out')), TIMEOUT_MS)
|
|
451
|
+
)
|
|
452
|
+
]);
|
|
453
|
+
console.log(`[COG:patch] _origParse completed in ${(performance.now() - t0).toFixed(0)}ms`);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.warn(
|
|
456
|
+
`[COG:patch] _origParse failed after ${(performance.now() - t0).toFixed(0)}ms:`,
|
|
457
|
+
err instanceof Error ? err.message : err
|
|
458
|
+
);
|
|
459
|
+
// Only attempt reconstruction for custom pipeline COGs (Gray/float/int)
|
|
460
|
+
// that have getTileData/renderTile props. Default pipeline COGs need
|
|
461
|
+
// the library's inferRenderPipeline to set defaultGetTileData — without
|
|
462
|
+
// it the library throws "getTileData is not a function".
|
|
463
|
+
const hasCustomPipeline = this.props.getTileData && this.props.renderTile;
|
|
464
|
+
const geotiff = capturedV2Geotiff || currentV3Tiff;
|
|
465
|
+
console.log(
|
|
466
|
+
`[COG:patch] fallback — customPipeline=${!!hasCustomPipeline}, hasV2=${!!capturedV2Geotiff}, hasV3=${!!currentV3Tiff}`
|
|
467
|
+
);
|
|
468
|
+
if (hasCustomPipeline && geotiff) {
|
|
469
|
+
try {
|
|
470
|
+
await reconstructLayerState(this, geotiff);
|
|
471
|
+
console.log('[COG:patch] fallback reconstruction succeeded');
|
|
472
|
+
} catch (reconstructErr) {
|
|
473
|
+
console.error('[COG:patch] fallback reconstruction failed:', reconstructErr);
|
|
474
|
+
if (this.props.onError) {
|
|
475
|
+
this.props.onError(
|
|
476
|
+
reconstructErr instanceof Error ? reconstructErr : new Error(String(reconstructErr))
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} else if (this.props.onError) {
|
|
481
|
+
this.props.onError(err instanceof Error ? err : new Error(String(err)));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// ─── Props & State ───────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
let { tab }: { tab: Tab } = $props();
|
|
489
|
+
|
|
490
|
+
let loading = $state(true);
|
|
491
|
+
let error = $state<string | null>(null);
|
|
492
|
+
let showInfo = $state(false);
|
|
493
|
+
let bounds = $state<[number, number, number, number] | undefined>();
|
|
494
|
+
let cogInfo = $state<{
|
|
495
|
+
width: number;
|
|
496
|
+
height: number;
|
|
497
|
+
bandCount: number;
|
|
498
|
+
dataType: string;
|
|
499
|
+
bounds: { west: number; south: number; east: number; north: number };
|
|
500
|
+
downsampled?: boolean;
|
|
501
|
+
} | null>(null);
|
|
502
|
+
|
|
503
|
+
let abortController = new AbortController();
|
|
504
|
+
let mapRef: maplibregl.Map | null = null;
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
+
let overlayRef: any = null;
|
|
507
|
+
|
|
508
|
+
// Native MapLibre image source for non-tiled GeoTIFFs (bypasses deck.gl)
|
|
509
|
+
const BITMAP_SOURCE = 'geotiff-bitmap-src';
|
|
510
|
+
const BITMAP_LAYER = 'geotiff-bitmap-layer';
|
|
511
|
+
|
|
512
|
+
function cleanupNativeBitmap() {
|
|
513
|
+
if (!mapRef) return;
|
|
514
|
+
try {
|
|
515
|
+
if (mapRef.getLayer(BITMAP_LAYER)) mapRef.removeLayer(BITMAP_LAYER);
|
|
516
|
+
} catch {
|
|
517
|
+
/* already removed */
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
if (mapRef.getSource(BITMAP_SOURCE)) mapRef.removeSource(BITMAP_SOURCE);
|
|
521
|
+
} catch {
|
|
522
|
+
/* already removed */
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─── Tab change reset ───────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
$effect(() => {
|
|
529
|
+
if (!tab) return;
|
|
530
|
+
const _tabId = tab.id;
|
|
531
|
+
untrack(() => {
|
|
532
|
+
abortController.abort();
|
|
533
|
+
abortController = new AbortController();
|
|
534
|
+
// Remove previous overlay/sources from map before loading new COG
|
|
535
|
+
cleanupNativeBitmap();
|
|
536
|
+
if (mapRef && overlayRef) {
|
|
537
|
+
try {
|
|
538
|
+
mapRef.removeControl(overlayRef);
|
|
539
|
+
} catch {
|
|
540
|
+
// map may already be destroyed
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
overlayRef = null;
|
|
544
|
+
loading = true;
|
|
545
|
+
error = null;
|
|
546
|
+
cogInfo = null;
|
|
547
|
+
bounds = undefined;
|
|
548
|
+
capturedV2Geotiff = null;
|
|
549
|
+
currentV3Tiff = null;
|
|
550
|
+
// Re-trigger loading if map is already initialized (tab switch).
|
|
551
|
+
// On first mount mapRef is null — onMapReady will handle it.
|
|
552
|
+
if (mapRef) {
|
|
553
|
+
loadCog(mapRef);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// ─── Map ready ──────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
function onMapReady(map: maplibregl.Map) {
|
|
561
|
+
mapRef = map;
|
|
562
|
+
loadCog(map);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function loadCog(map: maplibregl.Map) {
|
|
566
|
+
const signal = abortController.signal;
|
|
567
|
+
const loadT0 = performance.now();
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const url = buildHttpsUrl(tab);
|
|
571
|
+
console.group(`[COG] loadCog: ${url}`);
|
|
572
|
+
|
|
573
|
+
// Pre-flight: read first IFD with geotiff@3 (single small range request)
|
|
574
|
+
console.log('[COG] opening GeoTIFF via geotiff@3...');
|
|
575
|
+
const tiffT0 = performance.now();
|
|
576
|
+
const tiff = await fromUrl(url, {}, signal);
|
|
577
|
+
currentV3Tiff = tiff; // expose to monkey-patch as fallback
|
|
578
|
+
const firstImage = await tiff.getImage();
|
|
579
|
+
if (signal.aborted) return;
|
|
580
|
+
console.log(`[COG] first IFD loaded in ${(performance.now() - tiffT0).toFixed(0)}ms`);
|
|
581
|
+
|
|
582
|
+
// ─── v3-compatible metadata access ───
|
|
583
|
+
// Load deferred GDAL_NODATA tag (42113) — geotiff v3 defers large/custom
|
|
584
|
+
// TIFF tags and throws if accessed synchronously before loading.
|
|
585
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
586
|
+
await (firstImage as any).fileDirectory?.loadValue?.(42113);
|
|
587
|
+
const isTiled = firstImage.isTiled;
|
|
588
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
589
|
+
const pi = (firstImage as any).fileDirectory?.actualizedFields?.get?.(262) as
|
|
590
|
+
| number
|
|
591
|
+
| undefined;
|
|
592
|
+
const sfVal = firstImage.getSampleFormat();
|
|
593
|
+
const bandCount = firstImage.getSamplesPerPixel();
|
|
594
|
+
const bpsVal = firstImage.getBitsPerSample();
|
|
595
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
596
|
+
const compression = (firstImage as any).fileDirectory?.Compression ?? 'unknown';
|
|
597
|
+
const tileWidth = isTiled ? firstImage.getTileWidth() : 'N/A';
|
|
598
|
+
const tileHeight = isTiled ? firstImage.getTileHeight() : 'N/A';
|
|
599
|
+
const noData = firstImage.getGDALNoData();
|
|
600
|
+
|
|
601
|
+
// Routing: default pipeline = uint (SF=1) + PI >= 2 (RGB/Palette/CMYK/YCbCr/CIELab)
|
|
602
|
+
const isUint = sfVal === 1;
|
|
603
|
+
const isDefaultPipeline = isUint && pi !== undefined && pi >= 2;
|
|
604
|
+
|
|
605
|
+
// Data type label for info panel
|
|
606
|
+
const dataType = `${SF_LABELS[sfVal] ?? `sf${sfVal}`}${bpsVal ?? ''}`;
|
|
607
|
+
|
|
608
|
+
console.log(
|
|
609
|
+
`[COG] pre-flight metadata:\n` +
|
|
610
|
+
` dimensions: ${firstImage.getWidth()}×${firstImage.getHeight()}\n` +
|
|
611
|
+
` tiled: ${isTiled} (${tileWidth}×${tileHeight})\n` +
|
|
612
|
+
` PI: ${pi} (${pi === 0 || pi === 1 ? 'Gray' : pi === 2 ? 'RGB' : pi === 3 ? 'Palette' : `code ${pi}`})\n` +
|
|
613
|
+
` sampleFormat: ${sfVal} (${SF_LABELS[sfVal] ?? 'unknown'})\n` +
|
|
614
|
+
` bitsPerSample: ${bpsVal}\n` +
|
|
615
|
+
` bands: ${bandCount}\n` +
|
|
616
|
+
` compression: ${compression}\n` +
|
|
617
|
+
` noData: ${noData}\n` +
|
|
618
|
+
` pipeline: ${isDefaultPipeline ? 'DEFAULT (library)' : 'CUSTOM (Gray/float/int)'}\n` +
|
|
619
|
+
` route: ${isTiled ? (isDefaultPipeline ? 'tiled-default' : 'tiled-custom') : 'non-tiled-bitmap'}`
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Compute geographic bounds with edge sampling — the library's
|
|
623
|
+
// getGeographicBounds uses only 4 corners, which is inaccurate for
|
|
624
|
+
// projections where edges curve (UTM at high latitudes, Mollweide,
|
|
625
|
+
// sinusoidal). Sampling edge midpoints captures the true extent.
|
|
626
|
+
let preFlightBounds: { west: number; south: number; east: number; north: number } | null = null;
|
|
627
|
+
try {
|
|
628
|
+
const boundsT0 = performance.now();
|
|
629
|
+
const geoKeys = firstImage.getGeoKeys() as Record<string, unknown> | null;
|
|
630
|
+
const projInfo = geoKeys ? await geoKeysParser(geoKeys) : null;
|
|
631
|
+
if (projInfo) {
|
|
632
|
+
const bbox = firstImage.getBoundingBox();
|
|
633
|
+
const [x0, y0, x1, y1] = bbox;
|
|
634
|
+
console.log(
|
|
635
|
+
`[COG] bounds: native bbox=[${x0.toFixed(2)}, ${y0.toFixed(2)}, ${x1.toFixed(2)}, ${y1.toFixed(2)}], ` +
|
|
636
|
+
`CRS=${String(projInfo.def).substring(0, 60)}`
|
|
637
|
+
);
|
|
638
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
639
|
+
const conv = proj4Lib(projInfo.def, 'EPSG:4326') as any;
|
|
640
|
+
const N = 4; // samples per edge (including endpoints = 5 points)
|
|
641
|
+
const pts: [number, number][] = [];
|
|
642
|
+
for (let i = 0; i <= N; i++) {
|
|
643
|
+
const t = i / N;
|
|
644
|
+
pts.push([x0 + t * (x1 - x0), y0]); // bottom edge
|
|
645
|
+
pts.push([x0 + t * (x1 - x0), y1]); // top edge
|
|
646
|
+
pts.push([x0, y0 + t * (y1 - y0)]); // left edge
|
|
647
|
+
pts.push([x1, y0 + t * (y1 - y0)]); // right edge
|
|
648
|
+
}
|
|
649
|
+
let w = 180,
|
|
650
|
+
s = 90,
|
|
651
|
+
e = -180,
|
|
652
|
+
n = -90;
|
|
653
|
+
let validCount = 0;
|
|
654
|
+
let rejectedSamples: string[] = [];
|
|
655
|
+
for (const [px, py] of pts) {
|
|
656
|
+
const r = conv.forward([px, py], false);
|
|
657
|
+
if (
|
|
658
|
+
Number.isFinite(r[0]) &&
|
|
659
|
+
Number.isFinite(r[1]) &&
|
|
660
|
+
Math.abs(r[0]) <= 180 &&
|
|
661
|
+
Math.abs(r[1]) <= 90
|
|
662
|
+
) {
|
|
663
|
+
validCount++;
|
|
664
|
+
w = Math.min(w, r[0]);
|
|
665
|
+
e = Math.max(e, r[0]);
|
|
666
|
+
s = Math.min(s, r[1]);
|
|
667
|
+
n = Math.max(n, r[1]);
|
|
668
|
+
} else if (rejectedSamples.length < 4) {
|
|
669
|
+
rejectedSamples.push(
|
|
670
|
+
`(${px.toFixed(2)},${py.toFixed(2)})→(${r[0]?.toFixed(2)},${r[1]?.toFixed(2)})`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (w < e && s < n) {
|
|
675
|
+
preFlightBounds = clampBounds({ west: w, south: s, east: e, north: n });
|
|
676
|
+
}
|
|
677
|
+
console.log(
|
|
678
|
+
`[COG] bounds computed in ${(performance.now() - boundsT0).toFixed(0)}ms: ` +
|
|
679
|
+
`${validCount}/${pts.length} valid points` +
|
|
680
|
+
(preFlightBounds
|
|
681
|
+
? ` → W${preFlightBounds.west.toFixed(2)} S${preFlightBounds.south.toFixed(2)} E${preFlightBounds.east.toFixed(2)} N${preFlightBounds.north.toFixed(2)}`
|
|
682
|
+
: ' → FAILED') +
|
|
683
|
+
(rejectedSamples.length > 0
|
|
684
|
+
? `\n rejected samples: ${rejectedSamples.join(', ')}`
|
|
685
|
+
: '')
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
} catch (boundsErr) {
|
|
689
|
+
console.warn('[COG] bounds computation failed:', boundsErr);
|
|
690
|
+
}
|
|
691
|
+
if (signal.aborted) return;
|
|
692
|
+
|
|
693
|
+
// Shared onGeoTIFFLoad callback — populates info panel and fits bounds.
|
|
694
|
+
// Also captures the library's internal v2 GeoTIFF for the monkey-patch.
|
|
695
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
696
|
+
const handleGeoTIFFLoad = (
|
|
697
|
+
v2tiff: any,
|
|
698
|
+
{
|
|
699
|
+
geographicBounds
|
|
700
|
+
}: {
|
|
701
|
+
projection: unknown;
|
|
702
|
+
geographicBounds: { west: number; south: number; east: number; north: number };
|
|
703
|
+
}
|
|
704
|
+
) => {
|
|
705
|
+
capturedV2Geotiff = v2tiff;
|
|
706
|
+
const clamped = preFlightBounds || clampBounds(geographicBounds);
|
|
707
|
+
console.log(
|
|
708
|
+
`[COG] onGeoTIFFLoad fired — using ${preFlightBounds ? 'pre-flight' : 'library'} bounds, ` +
|
|
709
|
+
`loading took ${(performance.now() - loadT0).toFixed(0)}ms`
|
|
710
|
+
);
|
|
711
|
+
cogInfo = {
|
|
712
|
+
width: firstImage.getWidth(),
|
|
713
|
+
height: firstImage.getHeight(),
|
|
714
|
+
bandCount,
|
|
715
|
+
dataType,
|
|
716
|
+
bounds: clamped
|
|
717
|
+
};
|
|
718
|
+
bounds = [clamped.west, clamped.south, clamped.east, clamped.north];
|
|
719
|
+
fitCogBounds(map, clamped);
|
|
720
|
+
loading = false;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// Shared error handler
|
|
724
|
+
const handleError = (err: Error) => {
|
|
725
|
+
if (signal.aborted) return true;
|
|
726
|
+
const msg = err?.message || String(err);
|
|
727
|
+
if (
|
|
728
|
+
msg.includes('Request failed') ||
|
|
729
|
+
msg.includes('NetworkError') ||
|
|
730
|
+
msg.includes('Failed to fetch')
|
|
731
|
+
) {
|
|
732
|
+
error = t('map.cogCorsError');
|
|
733
|
+
} else {
|
|
734
|
+
error = msg;
|
|
735
|
+
}
|
|
736
|
+
loading = false;
|
|
737
|
+
return true;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
741
|
+
let layer: any;
|
|
742
|
+
|
|
743
|
+
if (isTiled && isDefaultPipeline) {
|
|
744
|
+
// ── Tiled COG, default pipeline (RGB / Palette / CMYK / YCbCr / CIELab) ──
|
|
745
|
+
console.log('[COG] route: tiled-default → creating COGLayer with library pipeline');
|
|
746
|
+
layer = new COGLayer({
|
|
747
|
+
id: 'cog-layer',
|
|
748
|
+
geotiff: url,
|
|
749
|
+
geoKeysParser,
|
|
750
|
+
onError: handleError,
|
|
751
|
+
onGeoTIFFLoad: handleGeoTIFFLoad
|
|
752
|
+
});
|
|
753
|
+
} else if (isTiled) {
|
|
754
|
+
// ── Tiled COG, custom single-band pipeline (Gray / float / int) ──
|
|
755
|
+
// Render a moderate overview as a single bitmap via MapLibre image
|
|
756
|
+
// source. Per-tile WASM ZSTD/LZW decode on the main thread blocks
|
|
757
|
+
// the UI for 200-1000ms per tile, making the TileLayer approach
|
|
758
|
+
// unusable for large Gray/float COGs (hundreds of visible tiles).
|
|
759
|
+
// A bitmap preview loads one overview in a single async read.
|
|
760
|
+
if (!preFlightBounds) {
|
|
761
|
+
throw new Error('Cannot determine geographic bounds for custom-pipeline COG');
|
|
762
|
+
}
|
|
763
|
+
console.log('[COG] route: tiled-custom → bitmap preview from overview');
|
|
764
|
+
const customT0 = performance.now();
|
|
765
|
+
const maxTexDim = getMaxTextureSize(map);
|
|
766
|
+
// Cap preview at 4096 to balance quality vs ZSTD decode time.
|
|
767
|
+
// A 4096×2000 overview (~8M pixels) decodes in 1-3 seconds.
|
|
768
|
+
const PREVIEW_MAX = Math.min(maxTexDim, 4096);
|
|
769
|
+
|
|
770
|
+
// Find the best overview: largest that fits within PREVIEW_MAX.
|
|
771
|
+
// Iterate from finest (IFD 0) to coarsest, pick first that fits.
|
|
772
|
+
const imageCount = await tiff.getImageCount();
|
|
773
|
+
let previewImage = firstImage;
|
|
774
|
+
let previewIdx = 0;
|
|
775
|
+
for (let i = 0; i < imageCount; i++) {
|
|
776
|
+
const img = await tiff.getImage(i);
|
|
777
|
+
const w = img.getWidth();
|
|
778
|
+
const h = img.getHeight();
|
|
779
|
+
if (w <= PREVIEW_MAX && h <= PREVIEW_MAX) {
|
|
780
|
+
previewImage = img;
|
|
781
|
+
previewIdx = i;
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (signal.aborted) return;
|
|
786
|
+
|
|
787
|
+
const pvW = previewImage.getWidth();
|
|
788
|
+
const pvH = previewImage.getHeight();
|
|
789
|
+
console.log(`[COG] preview: IFD #${previewIdx} ${pvW}×${pvH} (maxTex=${maxTexDim})`);
|
|
790
|
+
|
|
791
|
+
const noData = firstImage.getGDALNoData();
|
|
792
|
+
const readT0 = performance.now();
|
|
793
|
+
const rasters = await previewImage.readRasters({
|
|
794
|
+
samples: [0],
|
|
795
|
+
signal
|
|
796
|
+
});
|
|
797
|
+
if (signal.aborted) return;
|
|
798
|
+
console.log(`[COG] preview: readRasters took ${(performance.now() - readT0).toFixed(0)}ms`);
|
|
799
|
+
|
|
800
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
801
|
+
const band = (rasters as any)[0] as ArrayLike<number>;
|
|
802
|
+
let bMin = Infinity;
|
|
803
|
+
let bMax = -Infinity;
|
|
804
|
+
for (let i = 0; i < band.length; i++) {
|
|
805
|
+
const v = band[i];
|
|
806
|
+
if (noData !== null && v === noData) continue;
|
|
807
|
+
if (!Number.isFinite(v)) continue;
|
|
808
|
+
if (v < bMin) bMin = v;
|
|
809
|
+
if (v > bMax) bMax = v;
|
|
810
|
+
}
|
|
811
|
+
if (!Number.isFinite(bMin)) {
|
|
812
|
+
bMin = 0;
|
|
813
|
+
bMax = 1;
|
|
814
|
+
}
|
|
815
|
+
const bRange = bMax - bMin || 1;
|
|
816
|
+
console.log(`[COG] preview band stats: min=${bMin}, max=${bMax}, noData=${noData}`);
|
|
817
|
+
|
|
818
|
+
const rgba = new Uint8ClampedArray(pvW * pvH * 4);
|
|
819
|
+
for (let i = 0; i < band.length; i++) {
|
|
820
|
+
const v = band[i];
|
|
821
|
+
const isND = (noData !== null && v === noData) || !Number.isFinite(v);
|
|
822
|
+
const g = isND ? 0 : Math.round(((v - bMin) / bRange) * 255);
|
|
823
|
+
const idx = i * 4;
|
|
824
|
+
rgba[idx] = g;
|
|
825
|
+
rgba[idx + 1] = g;
|
|
826
|
+
rgba[idx + 2] = g;
|
|
827
|
+
rgba[idx + 3] = isND ? 0 : 255;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const canvas = document.createElement('canvas');
|
|
831
|
+
canvas.width = pvW;
|
|
832
|
+
canvas.height = pvH;
|
|
833
|
+
const ctx = canvas.getContext('2d')!;
|
|
834
|
+
ctx.putImageData(new ImageData(rgba, pvW, pvH), 0, 0);
|
|
835
|
+
const dataUrl = canvas.toDataURL();
|
|
836
|
+
canvas.width = 0;
|
|
837
|
+
canvas.height = 0;
|
|
838
|
+
|
|
839
|
+
const clamped = preFlightBounds;
|
|
840
|
+
cogInfo = {
|
|
841
|
+
width: firstImage.getWidth(),
|
|
842
|
+
height: firstImage.getHeight(),
|
|
843
|
+
bandCount,
|
|
844
|
+
dataType,
|
|
845
|
+
bounds: clamped,
|
|
846
|
+
downsampled: true
|
|
847
|
+
};
|
|
848
|
+
bounds = [clamped.west, clamped.south, clamped.east, clamped.north];
|
|
849
|
+
fitCogBounds(map, clamped);
|
|
850
|
+
|
|
851
|
+
cleanupNativeBitmap();
|
|
852
|
+
map.addSource(BITMAP_SOURCE, {
|
|
853
|
+
type: 'image',
|
|
854
|
+
url: dataUrl,
|
|
855
|
+
coordinates: [
|
|
856
|
+
[clamped.west, clamped.north],
|
|
857
|
+
[clamped.east, clamped.north],
|
|
858
|
+
[clamped.east, clamped.south],
|
|
859
|
+
[clamped.west, clamped.south]
|
|
860
|
+
]
|
|
861
|
+
});
|
|
862
|
+
map.addLayer({
|
|
863
|
+
id: BITMAP_LAYER,
|
|
864
|
+
source: BITMAP_SOURCE,
|
|
865
|
+
type: 'raster',
|
|
866
|
+
paint: { 'raster-opacity': 1 }
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
loading = false;
|
|
870
|
+
layer = null;
|
|
871
|
+
console.log(`[COG] preview rendered in ${(performance.now() - customT0).toFixed(0)}ms`);
|
|
872
|
+
} else {
|
|
873
|
+
// ── Non-tiled TIFF — render as bitmap ──
|
|
874
|
+
console.log('[COG] route: non-tiled-bitmap');
|
|
875
|
+
// GeoTIFFLayer is broken: it passes `texture` to RasterLayer but
|
|
876
|
+
// RasterLayer expects `renderPipeline`, causing a Symbol.iterator
|
|
877
|
+
// crash in MeshTextureLayer. Read the raster ourselves instead.
|
|
878
|
+
if (!preFlightBounds) {
|
|
879
|
+
throw new Error('Cannot determine geographic bounds for non-tiled GeoTIFF');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const imgW = firstImage.getWidth();
|
|
883
|
+
const imgH = firstImage.getHeight();
|
|
884
|
+
const totalPixels = imgW * imgH;
|
|
885
|
+
|
|
886
|
+
// Size gates — non-tiled TIFFs are read as a single strip-based
|
|
887
|
+
// blob (no random tile access). Protect against OOM / browser hang.
|
|
888
|
+
const MAX_NONTILED_PIXELS = 100_000_000; // 100M — refuse above
|
|
889
|
+
// Use actual GPU texture limit instead of a hardcoded value.
|
|
890
|
+
// High-end desktop GPUs: 16384, mobile/integrated: 4096–8192.
|
|
891
|
+
const maxTexDim = getMaxTextureSize(map);
|
|
892
|
+
|
|
893
|
+
console.log(
|
|
894
|
+
`[COG] non-tiled: ${imgW}×${imgH} = ${(totalPixels / 1e6).toFixed(1)}M px, maxTex=${maxTexDim}`
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
if (totalPixels > MAX_NONTILED_PIXELS) {
|
|
898
|
+
console.warn(
|
|
899
|
+
`[COG] non-tiled: REFUSED — ${(totalPixels / 1e6).toFixed(0)}M px > ${MAX_NONTILED_PIXELS / 1e6}M limit`
|
|
900
|
+
);
|
|
901
|
+
const clamped = preFlightBounds;
|
|
902
|
+
cogInfo = { width: imgW, height: imgH, bandCount, dataType, bounds: clamped };
|
|
903
|
+
bounds = [clamped.west, clamped.south, clamped.east, clamped.north];
|
|
904
|
+
fitCogBounds(map, clamped);
|
|
905
|
+
throw new Error(
|
|
906
|
+
`Non-tiled GeoTIFF too large (${imgW.toLocaleString()} × ${imgH.toLocaleString()} = ` +
|
|
907
|
+
`${(totalPixels / 1e6).toFixed(0)}M pixels). Convert to COG: ` +
|
|
908
|
+
`gdal_translate -of COG input.tif output.tif`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Cap output to GPU texture limit — keeps RGBA array + canvas within
|
|
913
|
+
// what this browser/device can actually upload as a single texture.
|
|
914
|
+
const needsDownsample = imgW > maxTexDim || imgH > maxTexDim;
|
|
915
|
+
let readW = imgW;
|
|
916
|
+
let readH = imgH;
|
|
917
|
+
if (needsDownsample) {
|
|
918
|
+
const scale = Math.min(maxTexDim / imgW, maxTexDim / imgH);
|
|
919
|
+
readW = Math.max(1, Math.round(imgW * scale));
|
|
920
|
+
readH = Math.max(1, Math.round(imgH * scale));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (needsDownsample) {
|
|
924
|
+
console.log(`[COG] non-tiled: downsampling ${imgW}×${imgH} → ${readW}×${readH}`);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const readT0 = performance.now();
|
|
928
|
+
const noData = firstImage.getGDALNoData();
|
|
929
|
+
const rasters = await firstImage.readRasters({
|
|
930
|
+
samples: [0],
|
|
931
|
+
signal,
|
|
932
|
+
...(needsDownsample ? { width: readW, height: readH } : {})
|
|
933
|
+
});
|
|
934
|
+
if (signal.aborted) return;
|
|
935
|
+
console.log(`[COG] non-tiled: readRasters took ${(performance.now() - readT0).toFixed(0)}ms`);
|
|
936
|
+
|
|
937
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
938
|
+
const band = (rasters as any)[0] as ArrayLike<number>;
|
|
939
|
+
let bMin = Infinity;
|
|
940
|
+
let bMax = -Infinity;
|
|
941
|
+
for (let i = 0; i < band.length; i++) {
|
|
942
|
+
const v = band[i];
|
|
943
|
+
if (noData !== null && v === noData) continue;
|
|
944
|
+
if (!Number.isFinite(v)) continue;
|
|
945
|
+
if (v < bMin) bMin = v;
|
|
946
|
+
if (v > bMax) bMax = v;
|
|
947
|
+
}
|
|
948
|
+
if (!Number.isFinite(bMin)) {
|
|
949
|
+
bMin = 0;
|
|
950
|
+
bMax = 1;
|
|
951
|
+
}
|
|
952
|
+
const bRange = bMax - bMin || 1;
|
|
953
|
+
console.log('[COG] non-tiled band 0 stats:', {
|
|
954
|
+
bMin,
|
|
955
|
+
bMax,
|
|
956
|
+
bRange,
|
|
957
|
+
noData,
|
|
958
|
+
len: band.length,
|
|
959
|
+
readW,
|
|
960
|
+
readH,
|
|
961
|
+
maxTexDim,
|
|
962
|
+
downsampled: needsDownsample
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const rgba = new Uint8ClampedArray(readW * readH * 4);
|
|
966
|
+
for (let i = 0; i < band.length; i++) {
|
|
967
|
+
const v = band[i];
|
|
968
|
+
const isND = (noData !== null && v === noData) || !Number.isFinite(v);
|
|
969
|
+
const g = isND ? 0 : Math.round(((v - bMin) / bRange) * 255);
|
|
970
|
+
const idx = i * 4;
|
|
971
|
+
rgba[idx] = g;
|
|
972
|
+
rgba[idx + 1] = g;
|
|
973
|
+
rgba[idx + 2] = g;
|
|
974
|
+
rgba[idx + 3] = isND ? 0 : 255;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Render via MapLibre native image source — bypasses deck.gl
|
|
978
|
+
// entirely, avoiding WebGL texture upload issues in Firefox.
|
|
979
|
+
const canvas = document.createElement('canvas');
|
|
980
|
+
canvas.width = readW;
|
|
981
|
+
canvas.height = readH;
|
|
982
|
+
const ctx = canvas.getContext('2d')!;
|
|
983
|
+
ctx.putImageData(new ImageData(rgba, readW, readH), 0, 0);
|
|
984
|
+
const dataUrl = canvas.toDataURL();
|
|
985
|
+
canvas.width = 0;
|
|
986
|
+
canvas.height = 0;
|
|
987
|
+
|
|
988
|
+
const clamped = preFlightBounds;
|
|
989
|
+
cogInfo = {
|
|
990
|
+
width: imgW,
|
|
991
|
+
height: imgH,
|
|
992
|
+
bandCount,
|
|
993
|
+
dataType,
|
|
994
|
+
bounds: clamped,
|
|
995
|
+
downsampled: needsDownsample
|
|
996
|
+
};
|
|
997
|
+
bounds = [clamped.west, clamped.south, clamped.east, clamped.north];
|
|
998
|
+
fitCogBounds(map, clamped);
|
|
999
|
+
|
|
1000
|
+
cleanupNativeBitmap();
|
|
1001
|
+
map.addSource(BITMAP_SOURCE, {
|
|
1002
|
+
type: 'image',
|
|
1003
|
+
url: dataUrl,
|
|
1004
|
+
coordinates: [
|
|
1005
|
+
[clamped.west, clamped.north], // top-left
|
|
1006
|
+
[clamped.east, clamped.north], // top-right
|
|
1007
|
+
[clamped.east, clamped.south], // bottom-right
|
|
1008
|
+
[clamped.west, clamped.south] // bottom-left
|
|
1009
|
+
]
|
|
1010
|
+
});
|
|
1011
|
+
map.addLayer({
|
|
1012
|
+
id: BITMAP_LAYER,
|
|
1013
|
+
source: BITMAP_SOURCE,
|
|
1014
|
+
type: 'raster',
|
|
1015
|
+
paint: { 'raster-opacity': 1 }
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
loading = false;
|
|
1019
|
+
layer = null; // no deck.gl layer needed
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Attach deck.gl overlay to the map (skip for native bitmap path)
|
|
1023
|
+
if (layer) {
|
|
1024
|
+
console.log('[COG] attaching deck.gl overlay to map...');
|
|
1025
|
+
const overlay = new MapboxOverlay({
|
|
1026
|
+
interleaved: false,
|
|
1027
|
+
layers: [layer],
|
|
1028
|
+
onError: (err: Error) => {
|
|
1029
|
+
if (!error && !signal.aborted) {
|
|
1030
|
+
error = err?.message || String(err);
|
|
1031
|
+
loading = false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
overlayRef = overlay;
|
|
1036
|
+
|
|
1037
|
+
if (map.loaded()) {
|
|
1038
|
+
map.addControl(overlay as unknown as maplibregl.IControl);
|
|
1039
|
+
} else {
|
|
1040
|
+
map.once('load', () => map.addControl(overlay as unknown as maplibregl.IControl));
|
|
1041
|
+
}
|
|
1042
|
+
console.log('[COG] overlay attached');
|
|
1043
|
+
}
|
|
1044
|
+
console.log(`[COG] loadCog completed in ${(performance.now() - loadT0).toFixed(0)}ms`);
|
|
1045
|
+
console.groupEnd();
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
if (signal.aborted) return;
|
|
1048
|
+
console.error(`[COG] loadCog failed after ${(performance.now() - loadT0).toFixed(0)}ms:`, err);
|
|
1049
|
+
console.groupEnd();
|
|
1050
|
+
error = err instanceof Error ? err.message : String(err);
|
|
1051
|
+
loading = false;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ─── Custom single-band COGLayer builder ────────────────────────
|
|
1056
|
+
// Uses our geotiff@3 pre-flight to compute min/max stats for normalization,
|
|
1057
|
+
// then passes the URL to COGLayer so the library opens its own v2 GeoTIFF.
|
|
1058
|
+
// The monkey-patched _parseGeoTIFF catches inferRenderPipeline's throw
|
|
1059
|
+
// and reconstructs state from the captured v2 GeoTIFF.
|
|
1060
|
+
|
|
1061
|
+
async function buildCustomCogLayer(
|
|
1062
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1063
|
+
tiff: any,
|
|
1064
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1065
|
+
firstImage: any,
|
|
1066
|
+
url: string,
|
|
1067
|
+
signal: AbortSignal,
|
|
1068
|
+
onError: (err: Error) => boolean,
|
|
1069
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1070
|
+
onGeoTIFFLoad: any
|
|
1071
|
+
) {
|
|
1072
|
+
// Compute global min/max from a small overview for normalization
|
|
1073
|
+
const imageCount = await tiff.getImageCount();
|
|
1074
|
+
console.log(`[COG:custom] ${imageCount} IFDs, searching for stats overview (64–1024px wide)...`);
|
|
1075
|
+
let statsImage = firstImage;
|
|
1076
|
+
let statsIfdIdx = 0;
|
|
1077
|
+
for (let i = imageCount - 1; i >= 1; i--) {
|
|
1078
|
+
const img = await tiff.getImage(i);
|
|
1079
|
+
const w = img.getWidth();
|
|
1080
|
+
if (w >= 64 && w <= 1024) {
|
|
1081
|
+
statsImage = img;
|
|
1082
|
+
statsIfdIdx = i;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
console.log(
|
|
1087
|
+
`[COG:custom] stats from IFD #${statsIfdIdx}: ${statsImage.getWidth()}×${statsImage.getHeight()}`
|
|
1088
|
+
);
|
|
1089
|
+
if (signal.aborted) return null;
|
|
1090
|
+
|
|
1091
|
+
const noData = firstImage.getGDALNoData();
|
|
1092
|
+
|
|
1093
|
+
// For large images without a suitable overview, sample a center crop
|
|
1094
|
+
let statsWindow: [number, number, number, number] | undefined;
|
|
1095
|
+
if (statsImage.getWidth() > 1024) {
|
|
1096
|
+
const cx = Math.floor(statsImage.getWidth() / 2);
|
|
1097
|
+
const cy = Math.floor(statsImage.getHeight() / 2);
|
|
1098
|
+
const half = 512;
|
|
1099
|
+
statsWindow = [
|
|
1100
|
+
Math.max(0, cx - half),
|
|
1101
|
+
Math.max(0, cy - half),
|
|
1102
|
+
Math.min(statsImage.getWidth(), cx + half),
|
|
1103
|
+
Math.min(statsImage.getHeight(), cy + half)
|
|
1104
|
+
];
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const rasters = await statsImage.readRasters({
|
|
1108
|
+
samples: [0],
|
|
1109
|
+
window: statsWindow,
|
|
1110
|
+
signal
|
|
1111
|
+
});
|
|
1112
|
+
if (signal.aborted) return null;
|
|
1113
|
+
|
|
1114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1115
|
+
const statsBand = (rasters as any)[0] as ArrayLike<number>;
|
|
1116
|
+
let min = Infinity;
|
|
1117
|
+
let max = -Infinity;
|
|
1118
|
+
for (let i = 0; i < statsBand.length; i++) {
|
|
1119
|
+
const v = statsBand[i];
|
|
1120
|
+
if (noData !== null && v === noData) continue;
|
|
1121
|
+
if (!Number.isFinite(v)) continue;
|
|
1122
|
+
if (v < min) min = v;
|
|
1123
|
+
if (v > max) max = v;
|
|
1124
|
+
}
|
|
1125
|
+
if (!Number.isFinite(min)) {
|
|
1126
|
+
min = 0;
|
|
1127
|
+
max = 1;
|
|
1128
|
+
}
|
|
1129
|
+
const range = max - min || 1;
|
|
1130
|
+
console.log(`[COG:custom] band stats: min=${min}, max=${max}, range=${range}, noData=${noData}`);
|
|
1131
|
+
|
|
1132
|
+
// Concurrency limiter — ZSTD/LZW decode is synchronous WASM on the
|
|
1133
|
+
// main thread. Without a cap, deck.gl fires dozens of tile requests
|
|
1134
|
+
// at once, each blocking for 100-300ms, completely freezing the UI.
|
|
1135
|
+
const MAX_CONCURRENT_TILES = 1;
|
|
1136
|
+
let activeTiles = 0;
|
|
1137
|
+
const tileQueue: (() => void)[] = [];
|
|
1138
|
+
function acquireTileSlot(): Promise<void> {
|
|
1139
|
+
if (activeTiles < MAX_CONCURRENT_TILES) {
|
|
1140
|
+
activeTiles++;
|
|
1141
|
+
return Promise.resolve();
|
|
1142
|
+
}
|
|
1143
|
+
return new Promise((resolve) => tileQueue.push(resolve));
|
|
1144
|
+
}
|
|
1145
|
+
function releaseTileSlot() {
|
|
1146
|
+
const next = tileQueue.shift();
|
|
1147
|
+
if (next) {
|
|
1148
|
+
next(); // keep activeTiles the same — slot transfers
|
|
1149
|
+
} else {
|
|
1150
|
+
activeTiles--;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Lazy cache: v3 images are loaded on-demand per zoom level.
|
|
1155
|
+
// geotiff v3 supports modern codecs (Zstandard 50000, WebP 50001) that
|
|
1156
|
+
// the library's bundled v2 does not. No Pool — avoids worker module
|
|
1157
|
+
// resolution failures in Vite dev that cause the browser to hang.
|
|
1158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1159
|
+
const v3ImageCache = new Map<string, any>();
|
|
1160
|
+
|
|
1161
|
+
// Pass URL so the library opens its own v2 GeoTIFF for metadata/tile-matrix.
|
|
1162
|
+
// Custom getTileData uses v3 images (which support more compression methods).
|
|
1163
|
+
return new COGLayer({
|
|
1164
|
+
id: 'cog-layer',
|
|
1165
|
+
geotiff: url,
|
|
1166
|
+
geoKeysParser,
|
|
1167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1168
|
+
getTileData: async (image: any, options: any) => {
|
|
1169
|
+
const { window: win, signal: tileSig } = options;
|
|
1170
|
+
const tileT0 = performance.now();
|
|
1171
|
+
const winLabel = win ? `[${win[0]},${win[1]}→${win[2]},${win[3]}]` : 'full';
|
|
1172
|
+
const key = `${image.getWidth()}x${image.getHeight()}`;
|
|
1173
|
+
|
|
1174
|
+
// Wait for a decode slot — limits main-thread blocking
|
|
1175
|
+
await acquireTileSlot();
|
|
1176
|
+
try {
|
|
1177
|
+
// Yield two animation frames before each tile decompression.
|
|
1178
|
+
// ZSTD/LZW decode via geotiff is synchronous WASM on the main
|
|
1179
|
+
// thread. Two rAF cycles guarantee the browser paints at least
|
|
1180
|
+
// one frame and processes input between tile decodes.
|
|
1181
|
+
await new Promise<void>((r) =>
|
|
1182
|
+
requestAnimationFrame(() => requestAnimationFrame(() => r()))
|
|
1183
|
+
);
|
|
1184
|
+
if (tileSig?.aborted) return null;
|
|
1185
|
+
|
|
1186
|
+
// Lazily find/cache the matching v3 image by dimensions
|
|
1187
|
+
let v3Img = v3ImageCache.get(key);
|
|
1188
|
+
if (!v3Img) {
|
|
1189
|
+
const count = await tiff.getImageCount();
|
|
1190
|
+
for (let i = 0; i < count; i++) {
|
|
1191
|
+
const img = await tiff.getImage(i); // cached by geotiff.js
|
|
1192
|
+
const k = `${img.getWidth()}x${img.getHeight()}`;
|
|
1193
|
+
v3ImageCache.set(k, img);
|
|
1194
|
+
if (k === key) {
|
|
1195
|
+
v3Img = img;
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Read band 0 — no Pool (main-thread async decode avoids worker hangs)
|
|
1202
|
+
const readT0 = performance.now();
|
|
1203
|
+
const r = await (v3Img || image).readRasters({
|
|
1204
|
+
samples: [0],
|
|
1205
|
+
window: win,
|
|
1206
|
+
signal: tileSig,
|
|
1207
|
+
interleave: false,
|
|
1208
|
+
// Use v2 pool only when falling back to v2 image
|
|
1209
|
+
...(v3Img ? {} : { pool: options.pool })
|
|
1210
|
+
});
|
|
1211
|
+
const readMs = performance.now() - readT0;
|
|
1212
|
+
|
|
1213
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1214
|
+
const band = (r as any)[0] as ArrayLike<number>;
|
|
1215
|
+
const w = win ? win[2] - win[0] : image.getWidth();
|
|
1216
|
+
const h = win ? win[3] - win[1] : image.getHeight();
|
|
1217
|
+
const rgba = new Uint8ClampedArray(w * h * 4);
|
|
1218
|
+
|
|
1219
|
+
const rgbaT0 = performance.now();
|
|
1220
|
+
for (let i = 0; i < band.length; i++) {
|
|
1221
|
+
const v = band[i];
|
|
1222
|
+
const isND = (noData !== null && v === noData) || !Number.isFinite(v);
|
|
1223
|
+
const g = isND ? 0 : Math.round(((v - min) / range) * 255);
|
|
1224
|
+
const idx = i * 4;
|
|
1225
|
+
rgba[idx] = g;
|
|
1226
|
+
rgba[idx + 1] = g;
|
|
1227
|
+
rgba[idx + 2] = g;
|
|
1228
|
+
rgba[idx + 3] = isND ? 0 : 255;
|
|
1229
|
+
}
|
|
1230
|
+
const rgbaMs = performance.now() - rgbaT0;
|
|
1231
|
+
|
|
1232
|
+
const totalMs = performance.now() - tileT0;
|
|
1233
|
+
console.log(
|
|
1234
|
+
`[COG:tile] ${key} ${winLabel} ${w}×${h} — ` +
|
|
1235
|
+
`read=${readMs.toFixed(0)}ms rgba=${rgbaMs.toFixed(0)}ms total=${totalMs.toFixed(0)}ms ` +
|
|
1236
|
+
`queue=${tileQueue.length} active=${activeTiles}` +
|
|
1237
|
+
(v3Img ? ' (v3)' : ' (v2-fallback)')
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
return { texture: new ImageData(rgba, w, h), width: w, height: h };
|
|
1241
|
+
} finally {
|
|
1242
|
+
releaseTileSlot();
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1246
|
+
renderTile: (data: any) => data.texture,
|
|
1247
|
+
onError,
|
|
1248
|
+
onGeoTIFFLoad
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// ─── Cleanup ────────────────────────────────────────────────────
|
|
1253
|
+
|
|
1254
|
+
function cleanup() {
|
|
1255
|
+
abortController.abort();
|
|
1256
|
+
cleanupNativeBitmap();
|
|
1257
|
+
if (mapRef && overlayRef) {
|
|
1258
|
+
try {
|
|
1259
|
+
mapRef.removeControl(overlayRef);
|
|
1260
|
+
} catch {
|
|
1261
|
+
// map may already be destroyed
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
mapRef = null;
|
|
1265
|
+
overlayRef = null;
|
|
1266
|
+
capturedV2Geotiff = null;
|
|
1267
|
+
currentV3Tiff = null;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
$effect(() => {
|
|
1271
|
+
const id = tab.id;
|
|
1272
|
+
const unregister = tabResources.register(id, cleanup);
|
|
1273
|
+
return unregister;
|
|
1274
|
+
});
|
|
1275
|
+
onDestroy(cleanup);
|
|
1276
|
+
</script>
|
|
1277
|
+
|
|
1278
|
+
<div class="relative flex h-full overflow-hidden">
|
|
1279
|
+
<div class="flex-1">
|
|
1280
|
+
<MapContainer {onMapReady} {bounds} />
|
|
1281
|
+
</div>
|
|
1282
|
+
|
|
1283
|
+
<div class="pointer-events-none absolute left-2 top-2 z-10 flex flex-col gap-1">
|
|
1284
|
+
{#if loading}
|
|
1285
|
+
<div
|
|
1286
|
+
class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm"
|
|
1287
|
+
>
|
|
1288
|
+
{t('map.loadingCog')}
|
|
1289
|
+
</div>
|
|
1290
|
+
{/if}
|
|
1291
|
+
|
|
1292
|
+
{#if cogInfo}
|
|
1293
|
+
<div
|
|
1294
|
+
class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm"
|
|
1295
|
+
>
|
|
1296
|
+
COG {cogInfo.width}×{cogInfo.height}, {cogInfo.bandCount}
|
|
1297
|
+
band{cogInfo.bandCount !== 1 ? 's' : ''}, {cogInfo.dataType}
|
|
1298
|
+
{#if cogInfo.downsampled}
|
|
1299
|
+
<span class="text-amber-400">— downsampled preview</span>
|
|
1300
|
+
{/if}
|
|
1301
|
+
</div>
|
|
1302
|
+
{/if}
|
|
1303
|
+
|
|
1304
|
+
{#if error}
|
|
1305
|
+
<div
|
|
1306
|
+
class="pointer-events-auto max-w-sm rounded bg-red-900/80 px-2 py-1 text-xs text-red-200"
|
|
1307
|
+
>
|
|
1308
|
+
{error}
|
|
1309
|
+
</div>
|
|
1310
|
+
{/if}
|
|
1311
|
+
</div>
|
|
1312
|
+
|
|
1313
|
+
{#if cogInfo}
|
|
1314
|
+
|
|
1315
|
+
<div class="absolute right-2 top-2 z-10 flex gap-1">
|
|
1316
|
+
<button
|
|
1317
|
+
class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm hover:bg-card"
|
|
1318
|
+
class:ring-1={showInfo}
|
|
1319
|
+
class:ring-primary={showInfo}
|
|
1320
|
+
onclick={() => (showInfo = !showInfo)}
|
|
1321
|
+
>
|
|
1322
|
+
{t('map.info')}
|
|
1323
|
+
</button>
|
|
1324
|
+
</div>
|
|
1325
|
+
|
|
1326
|
+
{#if showInfo}
|
|
1327
|
+
<div
|
|
1328
|
+
class="absolute right-2 top-10 z-10 max-h-[70vh] w-64 overflow-auto rounded bg-card/90 p-3 text-xs text-card-foreground backdrop-blur-sm"
|
|
1329
|
+
>
|
|
1330
|
+
<h3 class="mb-2 font-medium">{t('map.cogInfo')}</h3>
|
|
1331
|
+
<dl class="space-y-1.5">
|
|
1332
|
+
<dt class="text-muted-foreground">{t('mapInfo.size')}</dt>
|
|
1333
|
+
<dd>{cogInfo.width} × {cogInfo.height}</dd>
|
|
1334
|
+
<dt class="text-muted-foreground">{t('mapInfo.bands')}</dt>
|
|
1335
|
+
<dd>{cogInfo.bandCount} ({cogInfo.dataType})</dd>
|
|
1336
|
+
<dt class="text-muted-foreground">{t('mapInfo.bounds')}</dt>
|
|
1337
|
+
<dd>
|
|
1338
|
+
W {cogInfo.bounds.west.toFixed(4)}, S {cogInfo.bounds.south.toFixed(4)}<br />
|
|
1339
|
+
E {cogInfo.bounds.east.toFixed(4)}, N {cogInfo.bounds.north.toFixed(4)}
|
|
1340
|
+
</dd>
|
|
1341
|
+
</dl>
|
|
1342
|
+
</div>
|
|
1343
|
+
{/if}
|
|
1344
|
+
{/if}
|
|
1345
|
+
</div>
|