@walkthru-earth/objex 1.3.0 → 1.4.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 (182) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +20 -12
  3. package/dist/components/browser/FileTreeSidebar.svelte +32 -17
  4. package/dist/components/layout/AboutSheet.svelte +5 -2
  5. package/dist/components/layout/ConnectionDialog.svelte +1 -1
  6. package/dist/components/layout/SettingsSheet.svelte +237 -0
  7. package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
  8. package/dist/components/layout/Sidebar.svelte +73 -6
  9. package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
  10. package/dist/components/layout/StatusBar.svelte +1 -1
  11. package/dist/components/layout/TabBar.svelte +2 -2
  12. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  13. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  14. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  15. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  16. package/dist/components/ui/resizable/index.d.ts +1 -1
  17. package/dist/components/ui/resizable/index.js +2 -2
  18. package/dist/components/ui/slider/index.d.ts +3 -0
  19. package/dist/components/ui/slider/index.js +5 -0
  20. package/dist/components/ui/slider/range-slider.svelte +94 -0
  21. package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
  22. package/dist/components/ui/slider/slider.svelte +83 -0
  23. package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
  24. package/dist/components/viewers/ArchiveViewer.svelte +2 -2
  25. package/dist/components/viewers/CodeViewer.svelte +31 -22
  26. package/dist/components/viewers/CogControls.svelte +338 -184
  27. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  28. package/dist/components/viewers/CogViewer.svelte +320 -119
  29. package/dist/components/viewers/CopcViewer.svelte +1 -1
  30. package/dist/components/viewers/FlatGeobufViewer.svelte +1 -1
  31. package/dist/components/viewers/GeoParquetMapViewer.svelte +6 -6
  32. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  33. package/dist/components/viewers/ImageViewer.svelte +2 -2
  34. package/dist/components/viewers/MarkdownViewer.svelte +12 -9
  35. package/dist/components/viewers/MediaViewer.svelte +2 -2
  36. package/dist/components/viewers/ModelViewer.svelte +1 -1
  37. package/dist/components/viewers/MultiCogViewer.svelte +467 -102
  38. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  39. package/dist/components/viewers/NotebookViewer.svelte +6 -3
  40. package/dist/components/viewers/PdfViewer.svelte +2 -2
  41. package/dist/components/viewers/PmtilesViewer.svelte +3 -6
  42. package/dist/components/viewers/RawViewer.svelte +6 -3
  43. package/dist/components/viewers/StacMapViewer.svelte +10 -2
  44. package/dist/components/viewers/StacMosaicViewer.svelte +1800 -362
  45. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  46. package/dist/components/viewers/StacTabViewer.svelte +24 -13
  47. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  48. package/dist/components/viewers/TableGrid.svelte +4 -4
  49. package/dist/components/viewers/TableStatusBar.svelte +1 -1
  50. package/dist/components/viewers/TableToolbar.svelte +1 -1
  51. package/dist/components/viewers/TableViewer.svelte +25 -17
  52. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  53. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  54. package/dist/components/viewers/ZarrMapViewer.svelte +11 -9
  55. package/dist/components/viewers/ZarrViewer.svelte +4 -4
  56. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  57. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  58. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  59. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  60. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  61. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  62. package/dist/components/viewers/map/AttributeTable.svelte +1 -1
  63. package/dist/components/viewers/map/MapContainer.svelte +37 -11
  64. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +1 -1
  65. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +1 -1
  66. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  67. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  68. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  69. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  70. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  71. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  72. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  73. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  74. package/dist/file-icons/index.d.ts +1 -1
  75. package/dist/file-icons/index.js +1 -1
  76. package/dist/i18n/ar.js +110 -2
  77. package/dist/i18n/en.js +110 -2
  78. package/dist/index.d.ts +2 -28
  79. package/dist/index.js +7 -23
  80. package/dist/query/engine.d.ts +10 -0
  81. package/dist/query/source.js +1 -1
  82. package/dist/query/stac-source-factory.d.ts +65 -0
  83. package/dist/query/stac-source-factory.js +77 -0
  84. package/dist/query/stac-source-parquet.d.ts +135 -0
  85. package/dist/query/stac-source-parquet.js +465 -0
  86. package/dist/query/wasm.d.ts +8 -0
  87. package/dist/query/wasm.js +304 -2
  88. package/dist/storage/presign.js +1 -1
  89. package/dist/storage/providers.js +5 -5
  90. package/dist/stores/config.svelte.d.ts +15 -0
  91. package/dist/stores/config.svelte.js +46 -0
  92. package/dist/stores/connections.svelte.d.ts +2 -2
  93. package/dist/stores/connections.svelte.js +1 -2
  94. package/dist/stores/files.svelte.d.ts +1 -1
  95. package/dist/stores/files.svelte.js +1 -1
  96. package/dist/stores/query-history.svelte.js +1 -1
  97. package/dist/stores/settings.svelte.d.ts +16 -1
  98. package/dist/stores/settings.svelte.js +104 -48
  99. package/dist/stores/tabs.svelte.d.ts +3 -0
  100. package/dist/stores/tabs.svelte.js +17 -0
  101. package/dist/utils/cog-histogram.d.ts +121 -0
  102. package/dist/utils/cog-histogram.js +424 -0
  103. package/dist/utils/cog.d.ts +200 -60
  104. package/dist/utils/cog.js +377 -114
  105. package/dist/utils/colormap-sprite.d.ts +0 -9
  106. package/dist/utils/colormap-sprite.js +0 -21
  107. package/dist/utils/deck.d.ts +16 -12
  108. package/dist/utils/deck.js +10 -4
  109. package/dist/utils/pmtiles-tile.js +2 -2
  110. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  111. package/dist/utils/{url.js → signed-url.js} +32 -10
  112. package/dist/utils/url-state.d.ts +36 -0
  113. package/dist/utils/url-state.js +72 -2
  114. package/dist/utils/zarr-tab.d.ts +1 -2
  115. package/dist/utils/zarr-tab.js +1 -2
  116. package/dist/utils/zarr.d.ts +0 -17
  117. package/dist/utils/zarr.js +1 -45
  118. package/package.json +55 -84
  119. package/dist/components/browser/Breadcrumb.svelte +0 -50
  120. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  121. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  122. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  123. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  124. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  125. package/dist/components/browser/DropZone.svelte +0 -83
  126. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  127. package/dist/components/browser/FileBrowser.svelte +0 -252
  128. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  129. package/dist/components/browser/FileRow.svelte +0 -117
  130. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  131. package/dist/components/browser/RenameDialog.svelte +0 -101
  132. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  133. package/dist/components/browser/SearchBar.svelte +0 -40
  134. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  135. package/dist/components/browser/UploadButton.svelte +0 -65
  136. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  137. package/dist/query/stac-geoparquet.d.ts +0 -31
  138. package/dist/query/stac-geoparquet.js +0 -136
  139. package/dist/utils/clipboard.d.ts +0 -13
  140. package/dist/utils/clipboard.js +0 -38
  141. package/dist/utils/cloud-url.d.ts +0 -27
  142. package/dist/utils/cloud-url.js +0 -61
  143. package/dist/utils/column-types.d.ts +0 -5
  144. package/dist/utils/column-types.js +0 -137
  145. package/dist/utils/connection-identity.d.ts +0 -51
  146. package/dist/utils/connection-identity.js +0 -97
  147. package/dist/utils/error.d.ts +0 -8
  148. package/dist/utils/error.js +0 -12
  149. package/dist/utils/evidence-context.d.ts +0 -22
  150. package/dist/utils/evidence-context.js +0 -56
  151. package/dist/utils/export.d.ts +0 -22
  152. package/dist/utils/export.js +0 -76
  153. package/dist/utils/file-sort.d.ts +0 -20
  154. package/dist/utils/file-sort.js +0 -41
  155. package/dist/utils/format.d.ts +0 -24
  156. package/dist/utils/format.js +0 -78
  157. package/dist/utils/geoarrow.d.ts +0 -32
  158. package/dist/utils/geoarrow.js +0 -672
  159. package/dist/utils/geometry-type.d.ts +0 -52
  160. package/dist/utils/geometry-type.js +0 -76
  161. package/dist/utils/hex.d.ts +0 -10
  162. package/dist/utils/hex.js +0 -27
  163. package/dist/utils/host-detection.d.ts +0 -23
  164. package/dist/utils/host-detection.js +0 -95
  165. package/dist/utils/local-storage.d.ts +0 -16
  166. package/dist/utils/local-storage.js +0 -37
  167. package/dist/utils/markdown-sql.d.ts +0 -30
  168. package/dist/utils/markdown-sql.js +0 -72
  169. package/dist/utils/notebook.d.ts +0 -59
  170. package/dist/utils/notebook.js +0 -211
  171. package/dist/utils/parquet-metadata.d.ts +0 -64
  172. package/dist/utils/parquet-metadata.js +0 -262
  173. package/dist/utils/stac-geoparquet.d.ts +0 -90
  174. package/dist/utils/stac-geoparquet.js +0 -223
  175. package/dist/utils/stac-hydrate.d.ts +0 -38
  176. package/dist/utils/stac-hydrate.js +0 -243
  177. package/dist/utils/stac.d.ts +0 -136
  178. package/dist/utils/stac.js +0 -176
  179. package/dist/utils/storage-url.d.ts +0 -90
  180. package/dist/utils/storage-url.js +0 -568
  181. package/dist/utils/wkb.d.ts +0 -43
  182. package/dist/utils/wkb.js +0 -359
@@ -1,223 +0,0 @@
1
- /**
2
- * stac-geoparquet helpers.
3
- *
4
- * Pure TypeScript, zero Svelte / DuckDB / deck.gl dependencies. Re-exported
5
- * through `@walkthru-earth/objex-utils` so other projects can consume the
6
- * detection + row-to-Item transforms without pulling the Svelte lib.
7
- *
8
- * The module is decoupled from WKB decoding: callers pass a `wkbParser`
9
- * callback (the objex Svelte lib threads its `parseWKB`; other consumers
10
- * can plug in `geoarrow-wasm`, `wkx`, or any other library).
11
- */
12
- /** Columns every stac-geoparquet file MUST carry per the stac-geoparquet spec. */
13
- export const STAC_GEOPARQUET_REQUIRED_COLUMNS = [
14
- 'stac_version',
15
- 'type',
16
- 'geometry',
17
- 'assets'
18
- ];
19
- /**
20
- * Detect stac-geoparquet by presence of the required STAC columns.
21
- *
22
- * Deliberately type-agnostic: some pipelines know the type (DuckDB DESCRIBE,
23
- * Arrow Field), others only have the name list (hyparquet schema walk). The
24
- * set of names is sufficient for routing.
25
- */
26
- export function isStacGeoparquetSchema(schema) {
27
- if (!Array.isArray(schema) || schema.length === 0)
28
- return false;
29
- const names = new Set(schema.map((c) => c.name));
30
- return STAC_GEOPARQUET_REQUIRED_COLUMNS.every((c) => names.has(c));
31
- }
32
- /**
33
- * Flatten a DuckDB `struct(xmin,ymin,xmax,ymax)` bbox to the `[minX, minY, maxX, maxY]`
34
- * array shape that STAC Items and deck.gl-geotiff MosaicLayer expect.
35
- *
36
- * Pass-through for arrays so callers that already have `[minX,minY,maxX,maxY]`
37
- * shape (e.g. from a Feature's `bbox` field) don't need a separate path.
38
- */
39
- export function flattenStacBbox(bbox) {
40
- if (!bbox)
41
- return null;
42
- if (Array.isArray(bbox)) {
43
- if (bbox.length < 4)
44
- return null;
45
- const [minX, minY, maxX, maxY] = bbox;
46
- if (![minX, minY, maxX, maxY].every((v) => Number.isFinite(v)))
47
- return null;
48
- return [minX, minY, maxX, maxY];
49
- }
50
- if (typeof bbox === 'object') {
51
- const { xmin, ymin, xmax, ymax } = bbox;
52
- if (![xmin, ymin, xmax, ymax].every((v) => Number.isFinite(v)))
53
- return null;
54
- return [xmin, ymin, xmax, ymax];
55
- }
56
- return null;
57
- }
58
- /**
59
- * Resolve a possibly-relative STAC asset href against the parquet file URL.
60
- *
61
- * `./foo.tif` or `foo.tif` → absolute against `baseUrl`. Absolute URLs
62
- * (`http(s)://`, `s3://`, `gs://`, etc.) are returned unchanged.
63
- */
64
- export function resolveStacAssetHref(href, baseUrl) {
65
- if (!href)
66
- return href;
67
- if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(href))
68
- return href;
69
- try {
70
- return new URL(href, baseUrl).toString();
71
- }
72
- catch {
73
- return href;
74
- }
75
- }
76
- /**
77
- * Pick the "primary" asset from a STAC Item's `assets` map.
78
- *
79
- * Priority: caller-specified `preferredKeys` → `data` key → first asset with
80
- * `roles` containing `'data'` → first asset. Returns `null` if the map is
81
- * empty or not an object.
82
- */
83
- export function pickStacPrimaryAsset(assets, preferredKeys) {
84
- if (!assets || typeof assets !== 'object')
85
- return null;
86
- const entries = Object.entries(assets).filter(([, a]) => a && typeof a === 'object' && typeof a.href === 'string');
87
- if (entries.length === 0)
88
- return null;
89
- if (preferredKeys) {
90
- for (const key of preferredKeys) {
91
- const match = entries.find(([k]) => k === key);
92
- if (match)
93
- return { key: match[0], asset: match[1] };
94
- }
95
- }
96
- const data = entries.find(([k]) => k === 'data');
97
- if (data)
98
- return { key: data[0], asset: data[1] };
99
- const byRole = entries.find(([, a]) => Array.isArray(a.roles) && a.roles.includes('data'));
100
- if (byRole)
101
- return { key: byRole[0], asset: byRole[1] };
102
- return { key: entries[0][0], asset: entries[0][1] };
103
- }
104
- /**
105
- * Normalize the `assets` value read from a stac-geoparquet row.
106
- *
107
- * DuckDB returns `assets` as a `struct` with a fixed set of named fields (one
108
- * per asset key present at write-time), which decodes to a plain object.
109
- * Some rows may have `null` asset values for keys that don't apply to them;
110
- * those are filtered out.
111
- */
112
- function normalizeAssetsField(value, baseUrl) {
113
- if (!value || typeof value !== 'object')
114
- return undefined;
115
- const out = {};
116
- for (const [key, raw] of Object.entries(value)) {
117
- if (!raw || typeof raw !== 'object')
118
- continue;
119
- const asset = raw;
120
- if (typeof asset.href !== 'string' || !asset.href)
121
- continue;
122
- out[key] = {
123
- ...asset,
124
- href: resolveStacAssetHref(asset.href, baseUrl)
125
- };
126
- }
127
- return Object.keys(out).length > 0 ? out : undefined;
128
- }
129
- /** Normalize the `links` field, resolving relative hrefs against `baseUrl`. */
130
- function normalizeLinksField(value, baseUrl) {
131
- if (!Array.isArray(value))
132
- return undefined;
133
- const links = [];
134
- for (const raw of value) {
135
- if (!raw || typeof raw !== 'object')
136
- continue;
137
- const link = raw;
138
- if (typeof link.href !== 'string' || typeof link.rel !== 'string')
139
- continue;
140
- links.push({ ...link, href: resolveStacAssetHref(link.href, baseUrl) });
141
- }
142
- return links.length > 0 ? links : undefined;
143
- }
144
- /**
145
- * Convert one stac-geoparquet row into a standard STAC Item JSON object.
146
- *
147
- * Handles:
148
- * - `assets` named-struct flattening + relative href resolution
149
- * - `bbox` struct → `[minX, minY, maxX, maxY]` array
150
- * - Optional WKB geometry → GeoJSON via `opts.wkbParser`
151
- * - `datetime` → ISO string (passes through already-string values)
152
- * - Promotes `properties.*` columns (`proj:*`, `datetime`) onto `item.properties`
153
- */
154
- export function stacRowToItem(row, baseUrl, opts = {}) {
155
- const { wkbParser, wkbColumn = 'geom_wkb', geometryColumn = 'geometry' } = opts;
156
- let geometry = row[geometryColumn];
157
- if (!geometry) {
158
- const wkb = row[wkbColumn];
159
- if (wkb && wkbParser) {
160
- const bytes = wkb instanceof Uint8Array ? wkb : toUint8Array(wkb);
161
- if (bytes) {
162
- try {
163
- geometry = wkbParser(bytes) ?? undefined;
164
- }
165
- catch {
166
- geometry = undefined;
167
- }
168
- }
169
- }
170
- }
171
- const bbox = flattenStacBbox(row.bbox) ?? undefined;
172
- const assets = normalizeAssetsField(row.assets, baseUrl);
173
- const links = normalizeLinksField(row.links, baseUrl);
174
- const properties = {};
175
- // Hoist common STAC-property columns that live at row level in stac-geoparquet.
176
- for (const [key, value] of Object.entries(row)) {
177
- if (value === null || value === undefined)
178
- continue;
179
- if (key.startsWith('proj:') || key.startsWith('raster:') || key.startsWith('eo:')) {
180
- properties[key] = value;
181
- }
182
- if (key === 'datetime') {
183
- properties.datetime = value instanceof Date ? value.toISOString() : String(value);
184
- }
185
- if (key === 'bands') {
186
- properties.bands = value;
187
- }
188
- }
189
- const item = {
190
- type: 'Feature',
191
- stac_version: typeof row.stac_version === 'string' ? row.stac_version : '1.0.0',
192
- id: typeof row.id === 'string' ? row.id : String(row.id ?? ''),
193
- properties
194
- };
195
- if (typeof row.collection === 'string')
196
- item.collection = row.collection;
197
- if (Array.isArray(row.stac_extensions)) {
198
- item.stac_extensions = row.stac_extensions;
199
- }
200
- if (bbox)
201
- item.bbox = bbox;
202
- if (geometry)
203
- item.geometry = geometry;
204
- if (assets)
205
- item.assets = assets;
206
- if (links)
207
- item.links = links;
208
- return item;
209
- }
210
- /** Best-effort coercion of a value that may already be bytes into a Uint8Array. */
211
- function toUint8Array(value) {
212
- if (value instanceof Uint8Array)
213
- return value;
214
- if (value instanceof ArrayBuffer)
215
- return new Uint8Array(value);
216
- if (ArrayBuffer.isView(value)) {
217
- const view = value;
218
- return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
219
- }
220
- if (Array.isArray(value))
221
- return new Uint8Array(value);
222
- return null;
223
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * STAC link-following hydrator. Walks `links[rel=item]` (Collection),
3
- * `links[rel=child]` → `links[rel=item]` (Catalog), and `links[rel=next]`
4
- * (paginated FeatureCollection / STAC API) into a flat list of StacItems.
5
- */
6
- import type { StorageAdapter } from '../storage/adapter.js';
7
- import { type StacItem, type StacRoutableKind } from './stac.js';
8
- export interface HydrateOptions {
9
- signal: AbortSignal;
10
- /** Max parallel fetches. Default 12. */
11
- concurrency?: number;
12
- /** Hard cap on items; catalogs larger than this are truncated. Default 2000. */
13
- limit?: number;
14
- /** Follow `links[rel=next]` pagination in FeatureCollections. Default true. */
15
- followPagination?: boolean;
16
- /** Emit fetched items in batches for progressive rendering. */
17
- onBatch?: (items: StacItem[]) => void;
18
- /** Emit progress totals for UI. */
19
- onProgress?: (fetched: number, totalHinted: number | undefined) => void;
20
- /**
21
- * Map an absolute HTTPS URL to a bucket-relative key when it belongs to the
22
- * caller's connection. When provided and it returns a non-null string,
23
- * `fetchJson` routes through the storage adapter (which handles SigV4) instead
24
- * of a raw cross-origin `fetch`, so private-bucket catalogs can be walked.
25
- */
26
- urlToKey?: (absoluteUrl: string) => string | null;
27
- }
28
- export interface HydrateResult {
29
- items: StacItem[];
30
- truncated: boolean;
31
- rootBaseHref: string;
32
- }
33
- export declare function hydrateStacItems(root: StacRoutableKind, baseHref: string, adapter: StorageAdapter, opts: HydrateOptions): Promise<HydrateResult>;
34
- /**
35
- * Resolve a possibly-relative href against a base. STAC catalogs commonly use
36
- * `./child/foo.json` or `../foo.json`. `new URL(relative, base)` handles both.
37
- */
38
- export declare function absolutizeHref(href: string, baseHref: string): string;
@@ -1,243 +0,0 @@
1
- /**
2
- * STAC link-following hydrator. Walks `links[rel=item]` (Collection),
3
- * `links[rel=child]` → `links[rel=item]` (Catalog), and `links[rel=next]`
4
- * (paginated FeatureCollection / STAC API) into a flat list of StacItems.
5
- */
6
- import { isStacCatalog, isStacCollection, isStacFeatureCollection, isStacItem } from './stac.js';
7
- export async function hydrateStacItems(root, baseHref, adapter, opts) {
8
- const limit = opts.limit ?? 2000;
9
- const concurrency = opts.concurrency ?? 12;
10
- const followPagination = opts.followPagination ?? true;
11
- const signal = opts.signal;
12
- const urlToKey = opts.urlToKey;
13
- const items = [];
14
- let truncated = false;
15
- const emit = (batch) => {
16
- if (batch.length === 0)
17
- return;
18
- items.push(...batch);
19
- opts.onBatch?.(batch);
20
- opts.onProgress?.(items.length, undefined);
21
- };
22
- if (root.kind === 'item') {
23
- emit([absolutizeItemAssets(root.item, baseHref)]);
24
- return { items, truncated: false, rootBaseHref: baseHref };
25
- }
26
- if (root.kind === 'item-collection') {
27
- await consumeFeatureCollection(root.fc, baseHref, adapter, {
28
- signal,
29
- concurrency,
30
- limit,
31
- followPagination,
32
- urlToKey,
33
- onAccept: (batch) => emit(batch),
34
- stopCheck: () => items.length >= limit,
35
- onTruncate: () => {
36
- truncated = true;
37
- }
38
- });
39
- return { items: items.slice(0, limit), truncated, rootBaseHref: baseHref };
40
- }
41
- if (root.kind === 'collection' || root.kind === 'catalog') {
42
- const itemLinks = collectItemLinks(root.payload, baseHref);
43
- const childLinks = root.kind === 'catalog' || itemLinks.length === 0
44
- ? collectChildLinks(root.payload, baseHref)
45
- : [];
46
- if (itemLinks.length > 0) {
47
- await fetchItems(itemLinks, adapter, baseHref, {
48
- signal,
49
- concurrency,
50
- urlToKey,
51
- onBatch: (batch) => emit(batch),
52
- stopCheck: () => items.length >= limit,
53
- onTruncate: () => {
54
- truncated = true;
55
- }
56
- });
57
- }
58
- if (!truncated && items.length < limit && childLinks.length > 0) {
59
- const queue = [...childLinks];
60
- const stopCheck = () => items.length >= limit || signal.aborted;
61
- const workerCount = Math.min(concurrency, queue.length);
62
- const workers = [];
63
- for (let i = 0; i < workerCount; i++) {
64
- workers.push((async () => {
65
- while (queue.length > 0) {
66
- if (stopCheck()) {
67
- if (items.length >= limit)
68
- truncated = true;
69
- queue.length = 0;
70
- return;
71
- }
72
- const childHref = queue.shift();
73
- if (!childHref)
74
- return;
75
- try {
76
- const childJson = await fetchJson(adapter, childHref, baseHref, signal, urlToKey);
77
- if (stopCheck()) {
78
- if (items.length >= limit)
79
- truncated = true;
80
- queue.length = 0;
81
- return;
82
- }
83
- const childKind = classifyFetchedJson(childJson);
84
- if (childKind.kind === 'none')
85
- continue;
86
- const sub = await hydrateStacItems(childKind, childHref, adapter, {
87
- ...opts,
88
- limit: limit - items.length,
89
- onBatch: (batch) => emit(batch),
90
- onProgress: undefined
91
- });
92
- if (sub.truncated)
93
- truncated = true;
94
- }
95
- catch {
96
- // Skip unreachable child, keep aggregating.
97
- }
98
- }
99
- })());
100
- }
101
- await Promise.all(workers);
102
- }
103
- return { items: items.slice(0, limit), truncated, rootBaseHref: baseHref };
104
- }
105
- return { items, truncated: false, rootBaseHref: baseHref };
106
- }
107
- // ─── Internals ──────────────────────────────────────────────────────
108
- function collectItemLinks(payload, baseHref) {
109
- return (payload.links ?? [])
110
- .filter((l) => !!l && typeof l.href === 'string' && l.rel === 'item')
111
- .map((l) => absolutizeHref(l.href, baseHref));
112
- }
113
- function collectChildLinks(payload, baseHref) {
114
- return (payload.links ?? [])
115
- .filter((l) => !!l && typeof l.href === 'string' && l.rel === 'child')
116
- .map((l) => absolutizeHref(l.href, baseHref));
117
- }
118
- async function consumeFeatureCollection(fc, baseHref, adapter, ctx) {
119
- const accepted = fc.features
120
- .filter(isStacItem)
121
- .map((item) => absolutizeItemAssets(item, baseHref));
122
- ctx.onAccept(accepted);
123
- if (ctx.stopCheck()) {
124
- ctx.onTruncate();
125
- return;
126
- }
127
- if (!ctx.followPagination)
128
- return;
129
- const next = (fc.links ?? []).find((l) => l && l.rel === 'next' && typeof l.href === 'string');
130
- if (!next)
131
- return;
132
- const nextHref = absolutizeHref(next.href, baseHref);
133
- if (ctx.signal.aborted)
134
- return;
135
- try {
136
- const json = await fetchJson(adapter, nextHref, baseHref, ctx.signal, ctx.urlToKey);
137
- if (!isStacFeatureCollection(json))
138
- return;
139
- await consumeFeatureCollection(json, nextHref, adapter, ctx);
140
- }
141
- catch {
142
- // Pagination dead-end, keep what we have.
143
- }
144
- }
145
- async function fetchItems(hrefs, adapter, baseHref, ctx) {
146
- const queue = [...hrefs];
147
- const workers = [];
148
- const workerCount = Math.min(ctx.concurrency, queue.length);
149
- for (let i = 0; i < workerCount; i++) {
150
- workers.push((async () => {
151
- while (queue.length > 0) {
152
- if (ctx.signal.aborted)
153
- return;
154
- if (ctx.stopCheck()) {
155
- ctx.onTruncate();
156
- queue.length = 0;
157
- return;
158
- }
159
- const href = queue.shift();
160
- if (!href)
161
- return;
162
- try {
163
- const json = await fetchJson(adapter, href, baseHref, ctx.signal, ctx.urlToKey);
164
- if (isStacItem(json))
165
- ctx.onBatch([absolutizeItemAssets(json, href)]);
166
- }
167
- catch {
168
- // Skip this item, a dead link is not fatal.
169
- }
170
- }
171
- })());
172
- }
173
- await Promise.all(workers);
174
- }
175
- async function fetchJson(adapter, href, _baseHref, signal, urlToKey) {
176
- // Absolute URLs that belong to the caller's own bucket route through the
177
- // adapter so SigV4 presigning applies — raw `fetch` would 403 on private
178
- // buckets. Foreign origins stay on plain `fetch`. Relative hrefs reach here
179
- // only when absolutizeHref could not resolve them against baseHref.
180
- if (/^https?:/i.test(href)) {
181
- const ownKey = urlToKey ? urlToKey(href) : null;
182
- if (ownKey !== null) {
183
- const buf = await adapter.read(ownKey, undefined, undefined, signal);
184
- return JSON.parse(new TextDecoder().decode(buf));
185
- }
186
- const res = await fetch(href, { signal });
187
- if (!res.ok)
188
- throw new Error(`HTTP ${res.status} for ${href}`);
189
- return await res.json();
190
- }
191
- const buf = await adapter.read(href, undefined, undefined, signal);
192
- return JSON.parse(new TextDecoder().decode(buf));
193
- }
194
- function classifyFetchedJson(json) {
195
- if (isStacItem(json))
196
- return { kind: 'item', item: json };
197
- if (isStacFeatureCollection(json))
198
- return { kind: 'item-collection', fc: json };
199
- if (isStacCollection(json))
200
- return { kind: 'collection', payload: json };
201
- if (isStacCatalog(json))
202
- return { kind: 'catalog', payload: json };
203
- return { kind: 'none' };
204
- }
205
- /**
206
- * Resolve a possibly-relative href against a base. STAC catalogs commonly use
207
- * `./child/foo.json` or `../foo.json`. `new URL(relative, base)` handles both.
208
- */
209
- export function absolutizeHref(href, baseHref) {
210
- if (/^https?:/i.test(href) || /^s3:/i.test(href) || /^azure:/i.test(href))
211
- return href;
212
- try {
213
- return new URL(href, baseHref).toString();
214
- }
215
- catch {
216
- return href;
217
- }
218
- }
219
- /**
220
- * Return a new StacItem with every asset href resolved against the URL the
221
- * item itself was fetched from. Asset hrefs in STAC Items are relative to the
222
- * item JSON (`./B04.tif`, `../scene/asset.tif`), so downstream consumers that
223
- * hand them to `GeoTIFF.fromUrl` or `fetch` need absolute URLs.
224
- */
225
- function absolutizeItemAssets(item, itemHref) {
226
- const assets = item.assets;
227
- if (!assets)
228
- return item;
229
- let changed = false;
230
- const resolved = {};
231
- for (const [key, asset] of Object.entries(assets)) {
232
- if (asset?.href && typeof asset.href === 'string') {
233
- const abs = absolutizeHref(asset.href, itemHref);
234
- if (abs !== asset.href)
235
- changed = true;
236
- resolved[key] = { ...asset, href: abs };
237
- }
238
- else {
239
- resolved[key] = asset;
240
- }
241
- }
242
- return changed ? { ...item, assets: resolved } : item;
243
- }
@@ -1,136 +0,0 @@
1
- /**
2
- * STAC (SpatioTemporal Asset Catalog) detection and parsing.
3
- *
4
- * Pure TypeScript helpers shared by ViewerRouter, StacMosaicViewer, and
5
- * MultiCogViewer. No Svelte dependency, publishable via objex-utils.
6
- */
7
- /** STAC Link (shared by Catalog/Collection/Item). */
8
- export interface StacLink {
9
- rel: string;
10
- href: string;
11
- type?: string;
12
- title?: string;
13
- }
14
- /** STAC Item (GeoJSON Feature shape with stac_version). */
15
- export interface StacItem {
16
- type: 'Feature';
17
- stac_version: string;
18
- id: string;
19
- bbox?: [number, number, number, number];
20
- geometry?: unknown;
21
- properties?: Record<string, unknown>;
22
- assets?: Record<string, StacAsset>;
23
- collection?: string;
24
- links?: StacLink[];
25
- }
26
- /** STAC FeatureCollection (collection of Items). Also used for STAC API responses. */
27
- export interface StacFeatureCollection {
28
- type: 'FeatureCollection';
29
- stac_version?: string;
30
- features: StacItem[];
31
- links?: StacLink[];
32
- }
33
- /** STAC Collection (a grouping of Items with its own metadata and links). */
34
- export interface StacCollection {
35
- type: 'Collection';
36
- stac_version: string;
37
- id: string;
38
- description?: string;
39
- extent?: {
40
- spatial?: {
41
- bbox?: number[][];
42
- };
43
- temporal?: unknown;
44
- };
45
- links: StacLink[];
46
- }
47
- /** STAC Catalog (a directory-like grouping of Catalogs/Collections/Items via links). */
48
- export interface StacCatalog {
49
- type: 'Catalog';
50
- stac_version: string;
51
- id: string;
52
- description?: string;
53
- links: StacLink[];
54
- }
55
- /** Single asset entry within a STAC item. */
56
- export interface StacAsset {
57
- href: string;
58
- type?: string;
59
- title?: string;
60
- roles?: string[];
61
- /** Set when the asset carries `eo:bands`. */
62
- 'eo:bands'?: {
63
- name?: string;
64
- common_name?: string;
65
- }[];
66
- }
67
- /** Sentinel-2 band slot identifier, shared with utils/cog.ts composites. */
68
- export type BandSlot = 'red' | 'green' | 'blue' | 'nir' | 'swir1' | 'swir2' | 'rededge';
69
- /** Parsed band map: slot name → HTTPS asset URL. */
70
- export type BandMap = Partial<Record<BandSlot, string>>;
71
- /** Asset keys providers use for the single "display COG" asset, in priority order. */
72
- export declare const STAC_COG_ASSET_KEYS: readonly ["visual", "image", "data", "rendered_preview"];
73
- /** Shape-check a parsed JSON object as a STAC Item. */
74
- export declare function isStacItem(json: unknown): json is StacItem;
75
- /** Shape-check a parsed JSON object as a STAC FeatureCollection. */
76
- export declare function isStacFeatureCollection(json: unknown): json is StacFeatureCollection;
77
- /** STAC Collection detection: `type === 'Collection'` + stac_version + links array. */
78
- export declare function isStacCollection(json: unknown): json is StacCollection;
79
- /** STAC Catalog detection: `type === 'Catalog'` + stac_version + links array. */
80
- export declare function isStacCatalog(json: unknown): json is StacCatalog;
81
- /** Routing verdict for any STAC-shaped JSON payload. */
82
- export type StacRoutableKind = {
83
- kind: 'item';
84
- item: StacItem;
85
- } | {
86
- kind: 'item-collection';
87
- fc: StacFeatureCollection;
88
- } | {
89
- kind: 'collection';
90
- payload: StacCollection;
91
- } | {
92
- kind: 'catalog';
93
- payload: StacCatalog;
94
- } | {
95
- kind: 'none';
96
- };
97
- /** Classify an arbitrary parsed JSON into one of the STAC routing buckets. */
98
- export declare function classifyStac(json: unknown): StacRoutableKind;
99
- /**
100
- * Pick the COG-ish asset href from a STAC Item. Returns the href of the named
101
- * asset when `preferred` is given and present, else scans STAC_COG_ASSET_KEYS,
102
- * else falls back to any asset whose `type` contains "tiff". Returns null when
103
- * nothing matches.
104
- */
105
- export declare function pickCogAssetHref(item: StacItem, preferred?: string): string | null;
106
- /** True when a single STAC Item exposes a COG-ish asset and a bbox. */
107
- export declare function detectMosaicCapable(item: StacItem): boolean;
108
- /** True when a single STAC Item exposes Sentinel-2-style RGB bands (MultiCog). */
109
- export declare function detectMultiCogCapable(item: StacItem): boolean;
110
- /** WGS84 bbox helper. Returns `null` if no bbox can be derived. */
111
- export declare function stacItemBbox(item: StacItem): [number, number, number, number] | null;
112
- /** Normalized mosaic source entry consumed by MosaicLayer. */
113
- export interface MosaicSourceMeta {
114
- id: string;
115
- bbox: [number, number, number, number];
116
- href: string;
117
- }
118
- /**
119
- * Normalize a STAC Item or a plain `{id?, bbox, href}` record into a
120
- * MosaicSourceMeta. Returns null when essentials (bbox / href) are missing.
121
- */
122
- export declare function buildMosaicSourceMeta(input: StacItem | {
123
- id?: string;
124
- bbox: [number, number, number, number] | number[];
125
- href: string;
126
- }, assetKey?: string): MosaicSourceMeta | null;
127
- /**
128
- * Map Sentinel-2 STAC item assets to a BandMap. Recognizes:
129
- * - `eo:bands[0].common_name` (preferred, stable across providers)
130
- * - asset key heuristics for Microsoft PC / Element 84 / AWS S2 L2A buckets
131
- * Returns an empty map when no bands are identifiable so callers can fall
132
- * back to a different viewer.
133
- */
134
- export declare function extractSentinelBandAssets(item: StacItem): BandMap;
135
- /** True when the band map has enough channels for a True Color composite. */
136
- export declare function hasRgbBands(map: BandMap): boolean;