@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.
Files changed (367) hide show
  1. package/LICENSE +396 -0
  2. package/README.md +114 -0
  3. package/dist/assets/favicon.svg +17 -0
  4. package/dist/components/CLAUDE.md +44 -0
  5. package/dist/components/browser/Breadcrumb.svelte +50 -0
  6. package/dist/components/browser/Breadcrumb.svelte.d.ts +7 -0
  7. package/dist/components/browser/CreateFolderDialog.svelte +98 -0
  8. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +6 -0
  9. package/dist/components/browser/DeleteConfirmDialog.svelte +90 -0
  10. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +8 -0
  11. package/dist/components/browser/DropZone.svelte +83 -0
  12. package/dist/components/browser/DropZone.svelte.d.ts +7 -0
  13. package/dist/components/browser/FileBrowser.svelte +229 -0
  14. package/dist/components/browser/FileBrowser.svelte.d.ts +3 -0
  15. package/dist/components/browser/FileRow.svelte +112 -0
  16. package/dist/components/browser/FileRow.svelte.d.ts +9 -0
  17. package/dist/components/browser/FileTreeSidebar.svelte +559 -0
  18. package/dist/components/browser/FileTreeSidebar.svelte.d.ts +8 -0
  19. package/dist/components/browser/RenameDialog.svelte +101 -0
  20. package/dist/components/browser/RenameDialog.svelte.d.ts +8 -0
  21. package/dist/components/browser/SearchBar.svelte +40 -0
  22. package/dist/components/browser/SearchBar.svelte.d.ts +6 -0
  23. package/dist/components/browser/UploadButton.svelte +65 -0
  24. package/dist/components/browser/UploadButton.svelte.d.ts +3 -0
  25. package/dist/components/editor/CodeMirrorEditor.svelte +404 -0
  26. package/dist/components/editor/CodeMirrorEditor.svelte.d.ts +12 -0
  27. package/dist/components/editor/MilkdownEditor.svelte +98 -0
  28. package/dist/components/editor/MilkdownEditor.svelte.d.ts +9 -0
  29. package/dist/components/editor/SqlEditor.svelte +173 -0
  30. package/dist/components/editor/SqlEditor.svelte.d.ts +7 -0
  31. package/dist/components/editor/SqlResultBlock.svelte +199 -0
  32. package/dist/components/editor/SqlResultBlock.svelte.d.ts +9 -0
  33. package/dist/components/layout/ConnectionDialog.svelte +439 -0
  34. package/dist/components/layout/ConnectionDialog.svelte.d.ts +9 -0
  35. package/dist/components/layout/LocaleToggle.svelte +32 -0
  36. package/dist/components/layout/LocaleToggle.svelte.d.ts +3 -0
  37. package/dist/components/layout/SafeLockToggle.svelte +37 -0
  38. package/dist/components/layout/SafeLockToggle.svelte.d.ts +18 -0
  39. package/dist/components/layout/Sidebar.svelte +314 -0
  40. package/dist/components/layout/Sidebar.svelte.d.ts +3 -0
  41. package/dist/components/layout/StatusBar.svelte +73 -0
  42. package/dist/components/layout/StatusBar.svelte.d.ts +3 -0
  43. package/dist/components/layout/TabBar.svelte +102 -0
  44. package/dist/components/layout/TabBar.svelte.d.ts +7 -0
  45. package/dist/components/layout/ThemeToggle.svelte +52 -0
  46. package/dist/components/layout/ThemeToggle.svelte.d.ts +3 -0
  47. package/dist/components/ui/badge/badge.svelte +49 -0
  48. package/dist/components/ui/badge/badge.svelte.d.ts +32 -0
  49. package/dist/components/ui/badge/index.d.ts +1 -0
  50. package/dist/components/ui/badge/index.js +1 -0
  51. package/dist/components/ui/button/button.svelte +82 -0
  52. package/dist/components/ui/button/button.svelte.d.ts +64 -0
  53. package/dist/components/ui/button/index.d.ts +2 -0
  54. package/dist/components/ui/button/index.js +4 -0
  55. package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte +40 -0
  56. package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte.d.ts +9 -0
  57. package/dist/components/ui/context-menu/context-menu-content.svelte +28 -0
  58. package/dist/components/ui/context-menu/context-menu-content.svelte.d.ts +10 -0
  59. package/dist/components/ui/context-menu/context-menu-group-heading.svelte +21 -0
  60. package/dist/components/ui/context-menu/context-menu-group-heading.svelte.d.ts +7 -0
  61. package/dist/components/ui/context-menu/context-menu-group.svelte +7 -0
  62. package/dist/components/ui/context-menu/context-menu-group.svelte.d.ts +4 -0
  63. package/dist/components/ui/context-menu/context-menu-item.svelte +27 -0
  64. package/dist/components/ui/context-menu/context-menu-item.svelte.d.ts +8 -0
  65. package/dist/components/ui/context-menu/context-menu-label.svelte +24 -0
  66. package/dist/components/ui/context-menu/context-menu-label.svelte.d.ts +8 -0
  67. package/dist/components/ui/context-menu/context-menu-portal.svelte +7 -0
  68. package/dist/components/ui/context-menu/context-menu-portal.svelte.d.ts +3 -0
  69. package/dist/components/ui/context-menu/context-menu-radio-group.svelte +16 -0
  70. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +4 -0
  71. package/dist/components/ui/context-menu/context-menu-radio-item.svelte +33 -0
  72. package/dist/components/ui/context-menu/context-menu-radio-item.svelte.d.ts +4 -0
  73. package/dist/components/ui/context-menu/context-menu-separator.svelte +17 -0
  74. package/dist/components/ui/context-menu/context-menu-separator.svelte.d.ts +4 -0
  75. package/dist/components/ui/context-menu/context-menu-shortcut.svelte +20 -0
  76. package/dist/components/ui/context-menu/context-menu-shortcut.svelte.d.ts +5 -0
  77. package/dist/components/ui/context-menu/context-menu-sub-content.svelte +20 -0
  78. package/dist/components/ui/context-menu/context-menu-sub-content.svelte.d.ts +4 -0
  79. package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte +29 -0
  80. package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte.d.ts +8 -0
  81. package/dist/components/ui/context-menu/context-menu-sub.svelte +7 -0
  82. package/dist/components/ui/context-menu/context-menu-sub.svelte.d.ts +3 -0
  83. package/dist/components/ui/context-menu/context-menu-trigger.svelte +7 -0
  84. package/dist/components/ui/context-menu/context-menu-trigger.svelte.d.ts +4 -0
  85. package/dist/components/ui/context-menu/context-menu.svelte +7 -0
  86. package/dist/components/ui/context-menu/context-menu.svelte.d.ts +3 -0
  87. package/dist/components/ui/context-menu/index.d.ts +17 -0
  88. package/dist/components/ui/context-menu/index.js +19 -0
  89. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  90. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +4 -0
  91. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
  92. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +9 -0
  93. package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +29 -0
  94. package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +10 -0
  95. package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  96. package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +8 -0
  97. package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  98. package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte.d.ts +4 -0
  99. package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  100. package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte.d.ts +8 -0
  101. package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  102. package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte.d.ts +8 -0
  103. package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  104. package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte.d.ts +3 -0
  105. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
  106. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +4 -0
  107. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +33 -0
  108. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
  109. package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  110. package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
  111. package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  112. package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte.d.ts +5 -0
  113. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
  114. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +4 -0
  115. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  116. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +7 -0
  117. package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  118. package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte.d.ts +3 -0
  119. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  120. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +4 -0
  121. package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  122. package/dist/components/ui/dropdown-menu/dropdown-menu.svelte.d.ts +3 -0
  123. package/dist/components/ui/dropdown-menu/index.d.ts +18 -0
  124. package/dist/components/ui/dropdown-menu/index.js +18 -0
  125. package/dist/components/ui/input/index.d.ts +2 -0
  126. package/dist/components/ui/input/index.js +4 -0
  127. package/dist/components/ui/input/input.svelte +52 -0
  128. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  129. package/dist/components/ui/resizable/index.d.ts +4 -0
  130. package/dist/components/ui/resizable/index.js +6 -0
  131. package/dist/components/ui/resizable/resizable-handle.svelte +30 -0
  132. package/dist/components/ui/resizable/resizable-handle.svelte.d.ts +8 -0
  133. package/dist/components/ui/resizable/resizable-pane-group.svelte +20 -0
  134. package/dist/components/ui/resizable/resizable-pane-group.svelte.d.ts +7 -0
  135. package/dist/components/ui/scroll-area/index.d.ts +3 -0
  136. package/dist/components/ui/scroll-area/index.js +5 -0
  137. package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte +31 -0
  138. package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte.d.ts +4 -0
  139. package/dist/components/ui/scroll-area/scroll-area.svelte +47 -0
  140. package/dist/components/ui/scroll-area/scroll-area.svelte.d.ts +11 -0
  141. package/dist/components/ui/separator/index.d.ts +2 -0
  142. package/dist/components/ui/separator/index.js +4 -0
  143. package/dist/components/ui/separator/separator.svelte +21 -0
  144. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  145. package/dist/components/ui/sheet/index.d.ts +11 -0
  146. package/dist/components/ui/sheet/index.js +13 -0
  147. package/dist/components/ui/sheet/sheet-close.svelte +7 -0
  148. package/dist/components/ui/sheet/sheet-close.svelte.d.ts +4 -0
  149. package/dist/components/ui/sheet/sheet-content.svelte +62 -0
  150. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +37 -0
  151. package/dist/components/ui/sheet/sheet-description.svelte +17 -0
  152. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  153. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  154. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  155. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  156. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  157. package/dist/components/ui/sheet/sheet-overlay.svelte +20 -0
  158. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  159. package/dist/components/ui/sheet/sheet-portal.svelte +7 -0
  160. package/dist/components/ui/sheet/sheet-portal.svelte.d.ts +3 -0
  161. package/dist/components/ui/sheet/sheet-title.svelte +13 -0
  162. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  163. package/dist/components/ui/sheet/sheet-trigger.svelte +7 -0
  164. package/dist/components/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  165. package/dist/components/ui/sheet/sheet.svelte +7 -0
  166. package/dist/components/ui/sheet/sheet.svelte.d.ts +3 -0
  167. package/dist/components/ui/switch/index.d.ts +2 -0
  168. package/dist/components/ui/switch/index.js +4 -0
  169. package/dist/components/ui/switch/switch.svelte +28 -0
  170. package/dist/components/ui/switch/switch.svelte.d.ts +8 -0
  171. package/dist/components/ui/tabs/index.d.ts +5 -0
  172. package/dist/components/ui/tabs/index.js +7 -0
  173. package/dist/components/ui/tabs/tabs-content.svelte +17 -0
  174. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  175. package/dist/components/ui/tabs/tabs-list.svelte +16 -0
  176. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  177. package/dist/components/ui/tabs/tabs-trigger.svelte +20 -0
  178. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  179. package/dist/components/ui/tabs/tabs.svelte +19 -0
  180. package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
  181. package/dist/components/ui/tooltip/index.d.ts +6 -0
  182. package/dist/components/ui/tooltip/index.js +8 -0
  183. package/dist/components/ui/tooltip/tooltip-content.svelte +52 -0
  184. package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +11 -0
  185. package/dist/components/ui/tooltip/tooltip-portal.svelte +7 -0
  186. package/dist/components/ui/tooltip/tooltip-portal.svelte.d.ts +4 -0
  187. package/dist/components/ui/tooltip/tooltip-provider.svelte +7 -0
  188. package/dist/components/ui/tooltip/tooltip-provider.svelte.d.ts +4 -0
  189. package/dist/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  190. package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
  191. package/dist/components/ui/tooltip/tooltip.svelte +7 -0
  192. package/dist/components/ui/tooltip/tooltip.svelte.d.ts +4 -0
  193. package/dist/components/viewers/ArchiveViewer.svelte +586 -0
  194. package/dist/components/viewers/ArchiveViewer.svelte.d.ts +7 -0
  195. package/dist/components/viewers/CLAUDE.md +60 -0
  196. package/dist/components/viewers/CodeViewer.svelte +553 -0
  197. package/dist/components/viewers/CodeViewer.svelte.d.ts +7 -0
  198. package/dist/components/viewers/CogViewer.svelte +1345 -0
  199. package/dist/components/viewers/CogViewer.svelte.d.ts +7 -0
  200. package/dist/components/viewers/CopcViewer.svelte +25 -0
  201. package/dist/components/viewers/CopcViewer.svelte.d.ts +7 -0
  202. package/dist/components/viewers/DatabaseViewer.svelte +169 -0
  203. package/dist/components/viewers/DatabaseViewer.svelte.d.ts +7 -0
  204. package/dist/components/viewers/FileInfo.svelte +174 -0
  205. package/dist/components/viewers/FileInfo.svelte.d.ts +10 -0
  206. package/dist/components/viewers/FlatGeobufViewer.svelte +755 -0
  207. package/dist/components/viewers/FlatGeobufViewer.svelte.d.ts +7 -0
  208. package/dist/components/viewers/GeoParquetMapViewer.svelte +278 -0
  209. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +17 -0
  210. package/dist/components/viewers/ImageViewer.svelte +233 -0
  211. package/dist/components/viewers/ImageViewer.svelte.d.ts +7 -0
  212. package/dist/components/viewers/LoadProgress.svelte +93 -0
  213. package/dist/components/viewers/LoadProgress.svelte.d.ts +15 -0
  214. package/dist/components/viewers/MapViewer.svelte +234 -0
  215. package/dist/components/viewers/MapViewer.svelte.d.ts +7 -0
  216. package/dist/components/viewers/MarkdownViewer.svelte +478 -0
  217. package/dist/components/viewers/MarkdownViewer.svelte.d.ts +7 -0
  218. package/dist/components/viewers/MediaViewer.svelte +121 -0
  219. package/dist/components/viewers/MediaViewer.svelte.d.ts +7 -0
  220. package/dist/components/viewers/ModelViewer.svelte +164 -0
  221. package/dist/components/viewers/ModelViewer.svelte.d.ts +7 -0
  222. package/dist/components/viewers/NotebookViewer.svelte +389 -0
  223. package/dist/components/viewers/NotebookViewer.svelte.d.ts +7 -0
  224. package/dist/components/viewers/PdfViewer.svelte +278 -0
  225. package/dist/components/viewers/PdfViewer.svelte.d.ts +7 -0
  226. package/dist/components/viewers/PmtilesViewer.svelte +191 -0
  227. package/dist/components/viewers/PmtilesViewer.svelte.d.ts +7 -0
  228. package/dist/components/viewers/QueryHistoryPanel.svelte +159 -0
  229. package/dist/components/viewers/QueryHistoryPanel.svelte.d.ts +8 -0
  230. package/dist/components/viewers/RawViewer.svelte +117 -0
  231. package/dist/components/viewers/RawViewer.svelte.d.ts +7 -0
  232. package/dist/components/viewers/StacMapViewer.svelte +20 -0
  233. package/dist/components/viewers/StacMapViewer.svelte.d.ts +7 -0
  234. package/dist/components/viewers/StyleEditorOverlay.svelte +27 -0
  235. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +7 -0
  236. package/dist/components/viewers/TableGrid.svelte +355 -0
  237. package/dist/components/viewers/TableGrid.svelte.d.ts +12 -0
  238. package/dist/components/viewers/TableStatusBar.svelte +92 -0
  239. package/dist/components/viewers/TableStatusBar.svelte.d.ts +11 -0
  240. package/dist/components/viewers/TableToolbar.svelte +382 -0
  241. package/dist/components/viewers/TableToolbar.svelte.d.ts +25 -0
  242. package/dist/components/viewers/TableViewer.svelte +923 -0
  243. package/dist/components/viewers/TableViewer.svelte.d.ts +7 -0
  244. package/dist/components/viewers/ViewerRouter.svelte +70 -0
  245. package/dist/components/viewers/ViewerRouter.svelte.d.ts +7 -0
  246. package/dist/components/viewers/ZarrMapViewer.svelte +288 -0
  247. package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +17 -0
  248. package/dist/components/viewers/ZarrViewer.svelte +256 -0
  249. package/dist/components/viewers/ZarrViewer.svelte.d.ts +7 -0
  250. package/dist/components/viewers/map/AttributeTable.svelte +52 -0
  251. package/dist/components/viewers/map/AttributeTable.svelte.d.ts +8 -0
  252. package/dist/components/viewers/map/MapContainer.svelte +158 -0
  253. package/dist/components/viewers/map/MapContainer.svelte.d.ts +12 -0
  254. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +389 -0
  255. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte.d.ts +10 -0
  256. package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +332 -0
  257. package/dist/components/viewers/pmtiles/PmtilesMapView.svelte.d.ts +11 -0
  258. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +373 -0
  259. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte.d.ts +12 -0
  260. package/dist/components/viewers/pmtiles/SvgTileRenderer.svelte +112 -0
  261. package/dist/components/viewers/pmtiles/SvgTileRenderer.svelte.d.ts +10 -0
  262. package/dist/file-icons/CLAUDE.md +21 -0
  263. package/dist/file-icons/FileTypeIcon.svelte +74 -0
  264. package/dist/file-icons/FileTypeIcon.svelte.d.ts +9 -0
  265. package/dist/file-icons/index.d.ts +56 -0
  266. package/dist/file-icons/index.js +1070 -0
  267. package/dist/i18n/CLAUDE.md +19 -0
  268. package/dist/i18n/ar.d.ts +1 -0
  269. package/dist/i18n/ar.js +404 -0
  270. package/dist/i18n/en.d.ts +1 -0
  271. package/dist/i18n/en.js +404 -0
  272. package/dist/i18n/index.svelte.d.ts +9 -0
  273. package/dist/i18n/index.svelte.js +27 -0
  274. package/dist/index.d.ts +20 -0
  275. package/dist/index.js +13 -0
  276. package/dist/query/CLAUDE.md +22 -0
  277. package/dist/query/engine.d.ts +56 -0
  278. package/dist/query/engine.js +6 -0
  279. package/dist/query/index.d.ts +4 -0
  280. package/dist/query/index.js +19 -0
  281. package/dist/query/wasm.d.ts +20 -0
  282. package/dist/query/wasm.js +890 -0
  283. package/dist/storage/CLAUDE.md +23 -0
  284. package/dist/storage/adapter.d.ts +21 -0
  285. package/dist/storage/adapter.js +1 -0
  286. package/dist/storage/browser-azure.d.ts +25 -0
  287. package/dist/storage/browser-azure.js +271 -0
  288. package/dist/storage/browser-cloud.d.ts +32 -0
  289. package/dist/storage/browser-cloud.js +293 -0
  290. package/dist/storage/index.d.ts +11 -0
  291. package/dist/storage/index.js +37 -0
  292. package/dist/storage/url-adapter.d.ts +19 -0
  293. package/dist/storage/url-adapter.js +51 -0
  294. package/dist/stores/CLAUDE.md +29 -0
  295. package/dist/stores/browser.svelte.d.ts +28 -0
  296. package/dist/stores/browser.svelte.js +160 -0
  297. package/dist/stores/connections.svelte.d.ts +56 -0
  298. package/dist/stores/connections.svelte.js +272 -0
  299. package/dist/stores/credentials.svelte.d.ts +56 -0
  300. package/dist/stores/credentials.svelte.js +79 -0
  301. package/dist/stores/files.svelte.d.ts +20 -0
  302. package/dist/stores/files.svelte.js +76 -0
  303. package/dist/stores/query-history.svelte.d.ts +16 -0
  304. package/dist/stores/query-history.svelte.js +57 -0
  305. package/dist/stores/safelock.svelte.d.ts +8 -0
  306. package/dist/stores/safelock.svelte.js +52 -0
  307. package/dist/stores/settings.svelte.d.ts +11 -0
  308. package/dist/stores/settings.svelte.js +101 -0
  309. package/dist/stores/tab-resources.svelte.d.ts +25 -0
  310. package/dist/stores/tab-resources.svelte.js +61 -0
  311. package/dist/stores/tabs.svelte.d.ts +17 -0
  312. package/dist/stores/tabs.svelte.js +110 -0
  313. package/dist/types/notebookjs.d.ts +14 -0
  314. package/dist/types.d.ts +47 -0
  315. package/dist/types.js +1 -0
  316. package/dist/utils/CLAUDE.md +54 -0
  317. package/dist/utils/analytics.d.ts +10 -0
  318. package/dist/utils/analytics.js +38 -0
  319. package/dist/utils/archive.d.ts +70 -0
  320. package/dist/utils/archive.js +333 -0
  321. package/dist/utils/column-types.d.ts +5 -0
  322. package/dist/utils/column-types.js +137 -0
  323. package/dist/utils/deck.d.ts +98 -0
  324. package/dist/utils/deck.js +208 -0
  325. package/dist/utils/evidence-context.d.ts +22 -0
  326. package/dist/utils/evidence-context.js +56 -0
  327. package/dist/utils/export.d.ts +2 -0
  328. package/dist/utils/export.js +51 -0
  329. package/dist/utils/format.d.ts +14 -0
  330. package/dist/utils/format.js +56 -0
  331. package/dist/utils/geoarrow.d.ts +32 -0
  332. package/dist/utils/geoarrow.js +672 -0
  333. package/dist/utils/hex.d.ts +10 -0
  334. package/dist/utils/hex.js +27 -0
  335. package/dist/utils/host-detection.d.ts +23 -0
  336. package/dist/utils/host-detection.js +289 -0
  337. package/dist/utils/map-selection.d.ts +12 -0
  338. package/dist/utils/map-selection.js +45 -0
  339. package/dist/utils/markdown-sql.d.ts +30 -0
  340. package/dist/utils/markdown-sql.js +73 -0
  341. package/dist/utils/markdown.d.ts +18 -0
  342. package/dist/utils/markdown.js +146 -0
  343. package/dist/utils/model3d.d.ts +13 -0
  344. package/dist/utils/model3d.js +62 -0
  345. package/dist/utils/parquet-metadata.d.ts +58 -0
  346. package/dist/utils/parquet-metadata.js +228 -0
  347. package/dist/utils/pdf.d.ts +8 -0
  348. package/dist/utils/pdf.js +28 -0
  349. package/dist/utils/pmtiles-tile.d.ts +38 -0
  350. package/dist/utils/pmtiles-tile.js +64 -0
  351. package/dist/utils/pmtiles.d.ts +46 -0
  352. package/dist/utils/pmtiles.js +135 -0
  353. package/dist/utils/shiki.d.ts +8 -0
  354. package/dist/utils/shiki.js +98 -0
  355. package/dist/utils/storage-url.d.ts +64 -0
  356. package/dist/utils/storage-url.js +374 -0
  357. package/dist/utils/url-state.d.ts +40 -0
  358. package/dist/utils/url-state.js +113 -0
  359. package/dist/utils/url.d.ts +27 -0
  360. package/dist/utils/url.js +115 -0
  361. package/dist/utils/wkb.d.ts +43 -0
  362. package/dist/utils/wkb.js +345 -0
  363. package/dist/utils/zarr.d.ts +39 -0
  364. package/dist/utils/zarr.js +204 -0
  365. package/dist/utils.d.ts +12 -0
  366. package/dist/utils.js +5 -0
  367. 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}&times;{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} &times; {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>