@walkthru-earth/objex 1.2.1 → 1.3.1

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 (51) hide show
  1. package/README.md +6 -3
  2. package/dist/components/layout/ConnectionDialog.svelte +35 -3
  3. package/dist/components/layout/Sidebar.svelte +1 -2
  4. package/dist/components/viewers/CodeViewer.svelte +51 -14
  5. package/dist/components/viewers/CodeViewer.svelte.d.ts +11 -1
  6. package/dist/components/viewers/CogControls.svelte +151 -22
  7. package/dist/components/viewers/CogControls.svelte.d.ts +5 -1
  8. package/dist/components/viewers/CogViewer.svelte +75 -8
  9. package/dist/components/viewers/MultiCogViewer.svelte +416 -0
  10. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +9 -0
  11. package/dist/components/viewers/StacMapViewer.svelte +19 -5
  12. package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
  13. package/dist/components/viewers/StacMosaicViewer.svelte +785 -0
  14. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +9 -0
  15. package/dist/components/viewers/StacTabViewer.svelte +254 -0
  16. package/dist/components/viewers/StacTabViewer.svelte.d.ts +13 -0
  17. package/dist/components/viewers/ViewerRouter.svelte +155 -2
  18. package/dist/components/viewers/ViewerRouter.svelte.d.ts +1 -1
  19. package/dist/components/viewers/ZarrMapViewer.svelte +143 -4
  20. package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +8 -2
  21. package/dist/components/viewers/ZarrViewer.svelte +1 -0
  22. package/dist/i18n/ar.js +27 -0
  23. package/dist/i18n/en.js +27 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +2 -0
  26. package/dist/query/stac-geoparquet.d.ts +31 -0
  27. package/dist/query/stac-geoparquet.js +136 -0
  28. package/dist/stores/connections.svelte.d.ts +38 -23
  29. package/dist/stores/connections.svelte.js +105 -114
  30. package/dist/utils/cog-pure.d.ts +25 -0
  31. package/dist/utils/cog-pure.js +35 -0
  32. package/dist/utils/cog.d.ts +88 -43
  33. package/dist/utils/cog.js +192 -152
  34. package/dist/utils/colormap-sprite.d.ts +39 -0
  35. package/dist/utils/colormap-sprite.js +77 -0
  36. package/dist/utils/connection-identity.d.ts +51 -0
  37. package/dist/utils/connection-identity.js +97 -0
  38. package/dist/utils/host-detection.js +48 -302
  39. package/dist/utils/parquet-metadata.d.ts +7 -1
  40. package/dist/utils/parquet-metadata.js +35 -1
  41. package/dist/utils/stac-geoparquet.d.ts +90 -0
  42. package/dist/utils/stac-geoparquet.js +223 -0
  43. package/dist/utils/stac-hydrate.d.ts +38 -0
  44. package/dist/utils/stac-hydrate.js +243 -0
  45. package/dist/utils/stac.d.ts +136 -0
  46. package/dist/utils/stac.js +176 -0
  47. package/dist/utils/storage-url.d.ts +26 -0
  48. package/dist/utils/storage-url.js +164 -28
  49. package/dist/utils/zarr.d.ts +34 -0
  50. package/dist/utils/zarr.js +94 -0
  51. package/package.json +14 -13
@@ -0,0 +1,176 @@
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
+ /** Asset keys providers use for the single "display COG" asset, in priority order. */
8
+ export const STAC_COG_ASSET_KEYS = ['visual', 'image', 'data', 'rendered_preview'];
9
+ /** Shape-check a parsed JSON object as a STAC Item. */
10
+ export function isStacItem(json) {
11
+ if (!json || typeof json !== 'object')
12
+ return false;
13
+ const obj = json;
14
+ return obj.type === 'Feature' && typeof obj.stac_version === 'string';
15
+ }
16
+ /** Shape-check a parsed JSON object as a STAC FeatureCollection. */
17
+ export function isStacFeatureCollection(json) {
18
+ if (!json || typeof json !== 'object')
19
+ return false;
20
+ const obj = json;
21
+ if (obj.type !== 'FeatureCollection')
22
+ return false;
23
+ if (!Array.isArray(obj.features) || obj.features.length === 0)
24
+ return false;
25
+ if (typeof obj.stac_version === 'string')
26
+ return true;
27
+ return isStacItem(obj.features[0]);
28
+ }
29
+ /** STAC Collection detection: `type === 'Collection'` + stac_version + links array. */
30
+ export function isStacCollection(json) {
31
+ if (!json || typeof json !== 'object')
32
+ return false;
33
+ const obj = json;
34
+ return (obj.type === 'Collection' && typeof obj.stac_version === 'string' && Array.isArray(obj.links));
35
+ }
36
+ /** STAC Catalog detection: `type === 'Catalog'` + stac_version + links array. */
37
+ export function isStacCatalog(json) {
38
+ if (!json || typeof json !== 'object')
39
+ return false;
40
+ const obj = json;
41
+ return obj.type === 'Catalog' && typeof obj.stac_version === 'string' && Array.isArray(obj.links);
42
+ }
43
+ /** Classify an arbitrary parsed JSON into one of the STAC routing buckets. */
44
+ export function classifyStac(json) {
45
+ if (isStacItem(json))
46
+ return { kind: 'item', item: json };
47
+ if (isStacFeatureCollection(json))
48
+ return { kind: 'item-collection', fc: json };
49
+ if (isStacCollection(json))
50
+ return { kind: 'collection', payload: json };
51
+ if (isStacCatalog(json))
52
+ return { kind: 'catalog', payload: json };
53
+ return { kind: 'none' };
54
+ }
55
+ /**
56
+ * Pick the COG-ish asset href from a STAC Item. Returns the href of the named
57
+ * asset when `preferred` is given and present, else scans STAC_COG_ASSET_KEYS,
58
+ * else falls back to any asset whose `type` contains "tiff". Returns null when
59
+ * nothing matches.
60
+ */
61
+ export function pickCogAssetHref(item, preferred) {
62
+ const assets = item.assets ?? {};
63
+ if (preferred && assets[preferred]?.href)
64
+ return assets[preferred].href;
65
+ for (const key of STAC_COG_ASSET_KEYS) {
66
+ if (assets[key]?.href)
67
+ return assets[key].href;
68
+ }
69
+ for (const asset of Object.values(assets)) {
70
+ const t = typeof asset?.type === 'string' ? asset.type.toLowerCase() : '';
71
+ if (asset?.href && t.includes('tiff'))
72
+ return asset.href;
73
+ }
74
+ return null;
75
+ }
76
+ /** True when a single STAC Item exposes a COG-ish asset and a bbox. */
77
+ export function detectMosaicCapable(item) {
78
+ return stacItemBbox(item) !== null && pickCogAssetHref(item) !== null;
79
+ }
80
+ /** True when a single STAC Item exposes Sentinel-2-style RGB bands (MultiCog). */
81
+ export function detectMultiCogCapable(item) {
82
+ return hasRgbBands(extractSentinelBandAssets(item));
83
+ }
84
+ /** WGS84 bbox helper. Returns `null` if no bbox can be derived. */
85
+ export function stacItemBbox(item) {
86
+ if (Array.isArray(item.bbox) && item.bbox.length >= 4) {
87
+ return [Number(item.bbox[0]), Number(item.bbox[1]), Number(item.bbox[2]), Number(item.bbox[3])];
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * Normalize a STAC Item or a plain `{id?, bbox, href}` record into a
93
+ * MosaicSourceMeta. Returns null when essentials (bbox / href) are missing.
94
+ */
95
+ export function buildMosaicSourceMeta(input, assetKey) {
96
+ if (!input || typeof input !== 'object')
97
+ return null;
98
+ if (isStacItem(input)) {
99
+ const bbox = stacItemBbox(input);
100
+ if (!bbox)
101
+ return null;
102
+ const href = pickCogAssetHref(input, assetKey);
103
+ if (!href)
104
+ return null;
105
+ return {
106
+ id: String(input.id ?? href),
107
+ bbox,
108
+ href
109
+ };
110
+ }
111
+ const raw = input;
112
+ if (Array.isArray(raw.bbox) && raw.bbox.length >= 4 && typeof raw.href === 'string') {
113
+ return {
114
+ id: String(raw.id ?? raw.href),
115
+ bbox: [Number(raw.bbox[0]), Number(raw.bbox[1]), Number(raw.bbox[2]), Number(raw.bbox[3])],
116
+ href: raw.href
117
+ };
118
+ }
119
+ return null;
120
+ }
121
+ // Provider-specific asset key conventions. Each entry maps a BandSlot to the
122
+ // asset keys providers are known to use. First match wins, so list more
123
+ // specific keys before generic ones.
124
+ const BAND_KEY_FALLBACKS = {
125
+ red: ['red', 'B04', 'B4', 'visual-red'],
126
+ green: ['green', 'B03', 'B3', 'visual-green'],
127
+ blue: ['blue', 'B02', 'B2', 'visual-blue'],
128
+ nir: ['nir', 'nir08', 'B08', 'B8', 'B8A'],
129
+ swir1: ['swir16', 'swir1', 'B11'],
130
+ swir2: ['swir22', 'swir2', 'B12'],
131
+ rededge: ['rededge1', 'rededge', 'B05', 'B5']
132
+ };
133
+ /**
134
+ * Map Sentinel-2 STAC item assets to a BandMap. Recognizes:
135
+ * - `eo:bands[0].common_name` (preferred, stable across providers)
136
+ * - asset key heuristics for Microsoft PC / Element 84 / AWS S2 L2A buckets
137
+ * Returns an empty map when no bands are identifiable so callers can fall
138
+ * back to a different viewer.
139
+ */
140
+ export function extractSentinelBandAssets(item) {
141
+ const out = {};
142
+ const assets = item.assets ?? {};
143
+ for (const [key, asset] of Object.entries(assets)) {
144
+ if (!asset?.href)
145
+ continue;
146
+ const bands = asset['eo:bands'];
147
+ if (Array.isArray(bands) && bands.length >= 1) {
148
+ const common = bands[0]?.common_name?.toLowerCase();
149
+ if (common && isBandSlot(common)) {
150
+ if (!out[common])
151
+ out[common] = asset.href;
152
+ continue;
153
+ }
154
+ }
155
+ for (const slot of Object.keys(BAND_KEY_FALLBACKS)) {
156
+ if (BAND_KEY_FALLBACKS[slot].includes(key) && !out[slot]) {
157
+ out[slot] = asset.href;
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ return out;
163
+ }
164
+ function isBandSlot(value) {
165
+ return (value === 'red' ||
166
+ value === 'green' ||
167
+ value === 'blue' ||
168
+ value === 'nir' ||
169
+ value === 'swir1' ||
170
+ value === 'swir2' ||
171
+ value === 'rededge');
172
+ }
173
+ /** True when the band map has enough channels for a True Color composite. */
174
+ export function hasRgbBands(map) {
175
+ return Boolean(map.red && map.green && map.blue);
176
+ }
@@ -44,6 +44,13 @@ export interface ParsedStorageUrl {
44
44
  /** Original prefix/path after bucket, if any */
45
45
  prefix: string;
46
46
  }
47
+ /** STAC API path test, one source of truth. Tests pathname only. */
48
+ export declare const STAC_API_PATH_RE: RegExp;
49
+ /**
50
+ * Returns true when the host matches any of the provider host patterns
51
+ * that `parseStorageUrl` recognizes on the HTTPS branch.
52
+ */
53
+ export declare function isKnownBucketHost(host: string): boolean;
47
54
  export interface Defaults {
48
55
  region?: string;
49
56
  endpoint?: string;
@@ -62,3 +69,22 @@ export declare function looksLikeUrl(input: string): boolean;
62
69
  * Given a parsed URL result, build a human-readable summary of what was detected.
63
70
  */
64
71
  export declare function describeParseResult(parsed: ParsedStorageUrl): string;
72
+ export type UrlClassification = {
73
+ kind: 'scheme';
74
+ parsed: ParsedStorageUrl;
75
+ } | {
76
+ kind: 'object-storage';
77
+ parsed: ParsedStorageUrl;
78
+ } | {
79
+ kind: 'stac-api';
80
+ url: URL;
81
+ } | {
82
+ kind: 'remote-file';
83
+ url: URL;
84
+ };
85
+ /**
86
+ * Classify a user-supplied URL/URI into one of four buckets. Unparseable or
87
+ * plain inputs fall through to `remote-file` with a best-effort URL parse,
88
+ * returning a synthetic `https://` URL when `new URL()` would throw.
89
+ */
90
+ export declare function classifyUrl(input: string): UrlClassification;
@@ -54,6 +54,96 @@ function buildSchemeMap() {
54
54
  }
55
55
  /** All recognized URI scheme prefixes (lowercase), derived from provider registry */
56
56
  const SCHEME_MAP = buildSchemeMap();
57
+ /**
58
+ * Shared host matchers. Used by both `parseStorageUrl` and `isKnownBucketHost`
59
+ * so provider recognition has a single source of truth.
60
+ */
61
+ const AWS_VHOST_RE = /^(.+)\.s3[.-]([a-z0-9-]+)\.amazonaws\.com$/;
62
+ const AWS_PATH_RE = /^s3[.-]([a-z0-9-]+)\.amazonaws\.com$/;
63
+ const AWS_GLOBAL_HOST = 's3.amazonaws.com';
64
+ const R2_RE = /^([a-z0-9]+)\.r2\.cloudflarestorage\.com$/;
65
+ const GCS_GLOBAL_HOST = 'storage.googleapis.com';
66
+ const GCS_VHOST_RE = /^(.+)\.storage\.googleapis\.com$/;
67
+ const DO_VHOST_RE = /^(.+)\.([a-z0-9-]+)\.digitaloceanspaces\.com$/;
68
+ const DO_PATH_RE = /^([a-z0-9-]+)\.digitaloceanspaces\.com$/;
69
+ const WASABI_RE = /^s3\.([a-z0-9-]+)\.wasabisys\.com$/;
70
+ const B2_S3_RE = /^(.+)\.s3\.([a-z0-9-]+)\.backblazeb2\.com$/;
71
+ const B2_NATIVE_RE = /^f[a-z0-9]+\.backblazeb2\.com$/;
72
+ const OSS_RE = /^(.+)\.(oss-[a-z0-9-]+)\.aliyuncs\.com$/;
73
+ const COS_RE = /^(.+)\.cos\.([a-z0-9-]+)\.myqcloud\.com$/;
74
+ const YANDEX_HOST = 'storage.yandexcloud.net';
75
+ const CONTABO_RE = /^([a-z0-9]+)\.contabostorage\.com$/;
76
+ const HETZNER_RE = /^([a-z0-9]+)\.your-objectstorage\.com$/;
77
+ const LINODE_VHOST_RE = /^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/;
78
+ const LINODE_PATH_RE = /^([a-z0-9-]+)\.linodeobjects\.com$/;
79
+ const OVH_RE = /^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/;
80
+ const AZURE_BLOB_RE = /^([a-z0-9]+)\.blob\.core\.windows\.net$/;
81
+ const STORJ_GATEWAY_RE = /^gateway\.(?:([a-z0-9]+)\.)?storjshare\.io$/;
82
+ const STORJ_LINK_RE = /^link\.(?:([a-z0-9]+)\.)?storjshare\.io$/;
83
+ function isMinioLikeHost(host) {
84
+ return (host.includes('minio') ||
85
+ host === 'localhost' ||
86
+ host === '127.0.0.1' ||
87
+ host.startsWith('192.168.') ||
88
+ host.startsWith('10.'));
89
+ }
90
+ /** STAC API path test, one source of truth. Tests pathname only. */
91
+ export const STAC_API_PATH_RE = /\/(collections|items|catalogs|search)(\/|\?|$)/i;
92
+ /**
93
+ * Returns true when the host matches any of the provider host patterns
94
+ * that `parseStorageUrl` recognizes on the HTTPS branch.
95
+ */
96
+ export function isKnownBucketHost(host) {
97
+ if (!host)
98
+ return false;
99
+ if (AWS_VHOST_RE.test(host))
100
+ return true;
101
+ if (AWS_PATH_RE.test(host))
102
+ return true;
103
+ if (host === AWS_GLOBAL_HOST)
104
+ return true;
105
+ if (R2_RE.test(host))
106
+ return true;
107
+ if (host === GCS_GLOBAL_HOST)
108
+ return true;
109
+ if (GCS_VHOST_RE.test(host))
110
+ return true;
111
+ if (DO_VHOST_RE.test(host))
112
+ return true;
113
+ if (DO_PATH_RE.test(host))
114
+ return true;
115
+ if (WASABI_RE.test(host))
116
+ return true;
117
+ if (B2_S3_RE.test(host))
118
+ return true;
119
+ if (B2_NATIVE_RE.test(host))
120
+ return true;
121
+ if (OSS_RE.test(host))
122
+ return true;
123
+ if (COS_RE.test(host))
124
+ return true;
125
+ if (host === YANDEX_HOST)
126
+ return true;
127
+ if (CONTABO_RE.test(host))
128
+ return true;
129
+ if (HETZNER_RE.test(host))
130
+ return true;
131
+ if (LINODE_VHOST_RE.test(host))
132
+ return true;
133
+ if (LINODE_PATH_RE.test(host))
134
+ return true;
135
+ if (OVH_RE.test(host))
136
+ return true;
137
+ if (AZURE_BLOB_RE.test(host))
138
+ return true;
139
+ if (STORJ_GATEWAY_RE.test(host))
140
+ return true;
141
+ if (STORJ_LINK_RE.test(host))
142
+ return true;
143
+ if (isMinioLikeHost(host))
144
+ return true;
145
+ return false;
146
+ }
57
147
  function defaultResult(defaults) {
58
148
  return {
59
149
  bucket: '',
@@ -101,7 +191,7 @@ export function parseStorageUrl(input, defaults = {}) {
101
191
  const pathParts = url.pathname.replace(/^\//, '').split('/').filter(Boolean);
102
192
  // --- AWS S3 ---
103
193
  // Virtual-hosted: <bucket>.s3.<region>.amazonaws.com
104
- const awsVhost = host.match(/^(.+)\.s3[.-]([a-z0-9-]+)\.amazonaws\.com$/);
194
+ const awsVhost = host.match(AWS_VHOST_RE);
105
195
  if (awsVhost) {
106
196
  return {
107
197
  bucket: awsVhost[1],
@@ -112,7 +202,7 @@ export function parseStorageUrl(input, defaults = {}) {
112
202
  };
113
203
  }
114
204
  // Path-style: s3.<region>.amazonaws.com/<bucket>
115
- const awsPath = host.match(/^s3[.-]([a-z0-9-]+)\.amazonaws\.com$/);
205
+ const awsPath = host.match(AWS_PATH_RE);
116
206
  if (awsPath && pathParts.length > 0) {
117
207
  return {
118
208
  bucket: pathParts[0],
@@ -123,7 +213,7 @@ export function parseStorageUrl(input, defaults = {}) {
123
213
  };
124
214
  }
125
215
  // Global: s3.amazonaws.com/<bucket>
126
- if (host === 's3.amazonaws.com' && pathParts.length > 0) {
216
+ if (host === AWS_GLOBAL_HOST && pathParts.length > 0) {
127
217
  return {
128
218
  bucket: pathParts[0],
129
219
  region: defaults.region || 'us-east-1',
@@ -134,7 +224,7 @@ export function parseStorageUrl(input, defaults = {}) {
134
224
  }
135
225
  // --- Cloudflare R2 ---
136
226
  // <account>.r2.cloudflarestorage.com/<bucket>
137
- const r2Match = host.match(/^([a-z0-9]+)\.r2\.cloudflarestorage\.com$/);
227
+ const r2Match = host.match(R2_RE);
138
228
  if (r2Match && pathParts.length > 0) {
139
229
  return {
140
230
  bucket: pathParts[0],
@@ -146,7 +236,7 @@ export function parseStorageUrl(input, defaults = {}) {
146
236
  }
147
237
  // --- Google Cloud Storage ---
148
238
  // storage.googleapis.com/<bucket>
149
- if (host === 'storage.googleapis.com' && pathParts.length > 0) {
239
+ if (host === GCS_GLOBAL_HOST && pathParts.length > 0) {
150
240
  return {
151
241
  bucket: pathParts[0],
152
242
  region: defaults.region || 'us',
@@ -156,7 +246,7 @@ export function parseStorageUrl(input, defaults = {}) {
156
246
  };
157
247
  }
158
248
  // <bucket>.storage.googleapis.com
159
- const gcsVhost = host.match(/^(.+)\.storage\.googleapis\.com$/);
249
+ const gcsVhost = host.match(GCS_VHOST_RE);
160
250
  if (gcsVhost) {
161
251
  return {
162
252
  bucket: gcsVhost[1],
@@ -168,7 +258,7 @@ export function parseStorageUrl(input, defaults = {}) {
168
258
  }
169
259
  // --- DigitalOcean Spaces ---
170
260
  // <bucket>.<region>.digitaloceanspaces.com
171
- const doVhost = host.match(/^(.+)\.([a-z0-9-]+)\.digitaloceanspaces\.com$/);
261
+ const doVhost = host.match(DO_VHOST_RE);
172
262
  if (doVhost) {
173
263
  return {
174
264
  bucket: doVhost[1],
@@ -179,7 +269,7 @@ export function parseStorageUrl(input, defaults = {}) {
179
269
  };
180
270
  }
181
271
  // <region>.digitaloceanspaces.com/<bucket>
182
- const doPath = host.match(/^([a-z0-9-]+)\.digitaloceanspaces\.com$/);
272
+ const doPath = host.match(DO_PATH_RE);
183
273
  if (doPath && pathParts.length > 0) {
184
274
  return {
185
275
  bucket: pathParts[0],
@@ -191,7 +281,7 @@ export function parseStorageUrl(input, defaults = {}) {
191
281
  }
192
282
  // --- Wasabi ---
193
283
  // s3.<region>.wasabisys.com/<bucket>
194
- const wasabiMatch = host.match(/^s3\.([a-z0-9-]+)\.wasabisys\.com$/);
284
+ const wasabiMatch = host.match(WASABI_RE);
195
285
  if (wasabiMatch && pathParts.length > 0) {
196
286
  return {
197
287
  bucket: pathParts[0],
@@ -203,7 +293,7 @@ export function parseStorageUrl(input, defaults = {}) {
203
293
  }
204
294
  // --- Backblaze B2 ---
205
295
  // <bucket>.s3.<region>.backblazeb2.com (S3-compatible)
206
- const b2S3 = host.match(/^(.+)\.s3\.([a-z0-9-]+)\.backblazeb2\.com$/);
296
+ const b2S3 = host.match(B2_S3_RE);
207
297
  if (b2S3) {
208
298
  return {
209
299
  bucket: b2S3[1],
@@ -214,7 +304,7 @@ export function parseStorageUrl(input, defaults = {}) {
214
304
  };
215
305
  }
216
306
  // f<id>.backblazeb2.com/file/<bucket>
217
- const b2Native = host.match(/^f[a-z0-9]+\.backblazeb2\.com$/);
307
+ const b2Native = host.match(B2_NATIVE_RE);
218
308
  if (b2Native && pathParts[0] === 'file' && pathParts.length > 1) {
219
309
  return {
220
310
  bucket: pathParts[1],
@@ -226,7 +316,7 @@ export function parseStorageUrl(input, defaults = {}) {
226
316
  }
227
317
  // --- Alibaba Cloud OSS ---
228
318
  // <bucket>.oss-<region>.aliyuncs.com
229
- const ossMatch = host.match(/^(.+)\.(oss-[a-z0-9-]+)\.aliyuncs\.com$/);
319
+ const ossMatch = host.match(OSS_RE);
230
320
  if (ossMatch) {
231
321
  return {
232
322
  bucket: ossMatch[1],
@@ -238,7 +328,7 @@ export function parseStorageUrl(input, defaults = {}) {
238
328
  }
239
329
  // --- Tencent Cloud COS ---
240
330
  // <bucket>.cos.<region>.myqcloud.com
241
- const cosMatch = host.match(/^(.+)\.cos\.([a-z0-9-]+)\.myqcloud\.com$/);
331
+ const cosMatch = host.match(COS_RE);
242
332
  if (cosMatch) {
243
333
  return {
244
334
  bucket: cosMatch[1],
@@ -250,7 +340,7 @@ export function parseStorageUrl(input, defaults = {}) {
250
340
  }
251
341
  // --- Yandex Cloud ---
252
342
  // storage.yandexcloud.net/<bucket>
253
- if (host === 'storage.yandexcloud.net' && pathParts.length > 0) {
343
+ if (host === YANDEX_HOST && pathParts.length > 0) {
254
344
  return {
255
345
  bucket: pathParts[0],
256
346
  region: defaults.region || 'ru-central1',
@@ -261,7 +351,7 @@ export function parseStorageUrl(input, defaults = {}) {
261
351
  }
262
352
  // --- Contabo ---
263
353
  // <region>.contabostorage.com/<bucket>
264
- const contaboMatch = host.match(/^([a-z0-9]+)\.contabostorage\.com$/);
354
+ const contaboMatch = host.match(CONTABO_RE);
265
355
  if (contaboMatch && pathParts.length > 0) {
266
356
  return {
267
357
  bucket: pathParts[0],
@@ -273,7 +363,7 @@ export function parseStorageUrl(input, defaults = {}) {
273
363
  }
274
364
  // --- Hetzner ---
275
365
  // <region>.your-objectstorage.com/<bucket>
276
- const hetznerMatch = host.match(/^([a-z0-9]+)\.your-objectstorage\.com$/);
366
+ const hetznerMatch = host.match(HETZNER_RE);
277
367
  if (hetznerMatch && pathParts.length > 0) {
278
368
  return {
279
369
  bucket: pathParts[0],
@@ -285,7 +375,7 @@ export function parseStorageUrl(input, defaults = {}) {
285
375
  }
286
376
  // --- Linode / Akamai ---
287
377
  // <bucket>.<region>.linodeobjects.com or <region>.linodeobjects.com/<bucket>
288
- const linodeVhost = host.match(/^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/);
378
+ const linodeVhost = host.match(LINODE_VHOST_RE);
289
379
  if (linodeVhost) {
290
380
  return {
291
381
  bucket: linodeVhost[1],
@@ -295,7 +385,7 @@ export function parseStorageUrl(input, defaults = {}) {
295
385
  prefix: pathParts.join('/')
296
386
  };
297
387
  }
298
- const linodePath = host.match(/^([a-z0-9-]+)\.linodeobjects\.com$/);
388
+ const linodePath = host.match(LINODE_PATH_RE);
299
389
  if (linodePath && pathParts.length > 0) {
300
390
  return {
301
391
  bucket: pathParts[0],
@@ -307,7 +397,7 @@ export function parseStorageUrl(input, defaults = {}) {
307
397
  }
308
398
  // --- OVHcloud ---
309
399
  // s3.<region>.io.cloud.ovh.net/<bucket>
310
- const ovhMatch = host.match(/^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/);
400
+ const ovhMatch = host.match(OVH_RE);
311
401
  if (ovhMatch && pathParts.length > 0) {
312
402
  return {
313
403
  bucket: pathParts[0],
@@ -319,12 +409,7 @@ export function parseStorageUrl(input, defaults = {}) {
319
409
  }
320
410
  // --- MinIO ---
321
411
  // Common patterns: minio.<domain>, localhost with port
322
- const isMinioLike = host.includes('minio') ||
323
- host === 'localhost' ||
324
- host === '127.0.0.1' ||
325
- host.startsWith('192.168.') ||
326
- host.startsWith('10.');
327
- if (isMinioLike && pathParts.length > 0) {
412
+ if (isMinioLikeHost(host) && pathParts.length > 0) {
328
413
  return {
329
414
  bucket: pathParts[0],
330
415
  region: defaults.region || 'us-east-1',
@@ -335,7 +420,7 @@ export function parseStorageUrl(input, defaults = {}) {
335
420
  }
336
421
  // --- Azure Blob Storage ---
337
422
  // <account>.blob.core.windows.net/<container>
338
- const azureBlob = host.match(/^([a-z0-9]+)\.blob\.core\.windows\.net$/);
423
+ const azureBlob = host.match(AZURE_BLOB_RE);
339
424
  if (azureBlob && pathParts.length > 0) {
340
425
  return {
341
426
  bucket: pathParts[0],
@@ -347,7 +432,7 @@ export function parseStorageUrl(input, defaults = {}) {
347
432
  }
348
433
  // --- Storj ---
349
434
  // S3 gateway: gateway.storjshare.io/<bucket> (or gateway.<region>.storjshare.io)
350
- const storjGateway = host.match(/^gateway\.(?:([a-z0-9]+)\.)?storjshare\.io$/);
435
+ const storjGateway = host.match(STORJ_GATEWAY_RE);
351
436
  if (storjGateway && pathParts.length > 0) {
352
437
  return {
353
438
  bucket: pathParts[0],
@@ -358,7 +443,7 @@ export function parseStorageUrl(input, defaults = {}) {
358
443
  };
359
444
  }
360
445
  // Linksharing: link.storjshare.io/raw/<access>/<bucket>/... or /s/<access>/<bucket>/...
361
- const storjLink = host.match(/^link\.(?:([a-z0-9]+)\.)?storjshare\.io$/);
446
+ const storjLink = host.match(STORJ_LINK_RE);
362
447
  if (storjLink && pathParts.length >= 3 && (pathParts[0] === 'raw' || pathParts[0] === 's')) {
363
448
  return {
364
449
  bucket: pathParts[2],
@@ -368,6 +453,17 @@ export function parseStorageUrl(input, defaults = {}) {
368
453
  prefix: pathParts.slice(3).join('/')
369
454
  };
370
455
  }
456
+ // --- STAC API endpoints (Element 84, MPC, etc.) ---
457
+ // Paths like /v1/collections/.../items/... are not S3 buckets, the
458
+ // first path segment is an API version, not a bucket name. Return
459
+ // no bucket so detectHostBucket() falls through and the URL opens
460
+ // as a direct remote fetch.
461
+ if (STAC_API_PATH_RE.test(url.pathname)) {
462
+ return {
463
+ ...defaultResult(defaults),
464
+ endpoint: `${url.protocol}//${url.host}`
465
+ };
466
+ }
371
467
  // --- Generic custom endpoint with bucket in path ---
372
468
  if (pathParts.length > 0) {
373
469
  const endpoint = `${url.protocol}//${url.host}`;
@@ -430,3 +526,43 @@ export function describeParseResult(parsed) {
430
526
  parts.push(`prefix="${parsed.prefix}"`);
431
527
  return parts.length > 0 ? `Detected: ${parts.join(', ')}` : '';
432
528
  }
529
+ /**
530
+ * Classify a user-supplied URL/URI into one of four buckets. Unparseable or
531
+ * plain inputs fall through to `remote-file` with a best-effort URL parse,
532
+ * returning a synthetic `https://` URL when `new URL()` would throw.
533
+ */
534
+ export function classifyUrl(input) {
535
+ const trimmed = input.trim();
536
+ const lower = trimmed.toLowerCase();
537
+ for (const scheme of Object.keys(SCHEME_MAP)) {
538
+ if (lower.startsWith(scheme)) {
539
+ return { kind: 'scheme', parsed: parseStorageUrl(trimmed) };
540
+ }
541
+ }
542
+ if (lower.startsWith('http://') || lower.startsWith('https://')) {
543
+ let url;
544
+ try {
545
+ url = new URL(trimmed);
546
+ }
547
+ catch {
548
+ return {
549
+ kind: 'remote-file',
550
+ url: new URL(`https://${trimmed.replace(/^https?:\/\//i, '')}`)
551
+ };
552
+ }
553
+ const parsed = parseStorageUrl(trimmed);
554
+ if (parsed.bucket && isKnownBucketHost(url.hostname)) {
555
+ return { kind: 'object-storage', parsed };
556
+ }
557
+ if (STAC_API_PATH_RE.test(url.pathname)) {
558
+ return { kind: 'stac-api', url };
559
+ }
560
+ return { kind: 'remote-file', url };
561
+ }
562
+ try {
563
+ return { kind: 'remote-file', url: new URL(trimmed) };
564
+ }
565
+ catch {
566
+ return { kind: 'remote-file', url: new URL(`https://${trimmed}`) };
567
+ }
568
+ }
@@ -44,6 +44,40 @@ export interface ZarrHierarchy {
44
44
  storeAttrs: Record<string, any>;
45
45
  spatialRefAttrs: Record<string, any> | null;
46
46
  }
47
+ export interface GeoZarrInfo {
48
+ /** Relative path within the store to the group carrying GeoZarr attrs. */
49
+ variantPath: string;
50
+ /** Raw attributes object that parses as GeoZarr (for caller re-use). */
51
+ attrs: Record<string, any>;
52
+ }
53
+ /**
54
+ * Walk the hierarchy looking for a node whose attributes satisfy the core
55
+ * GeoZarr convention: `multiscales` + a spatial convention (`spatial` /
56
+ * `spatial:dimensions`) + CRS info (`geo-proj`, `proj:code`, or `crs_wkt`).
57
+ *
58
+ * Check is shape-only (no zarrita I/O), so it's safe to call synchronously
59
+ * after `fetchHierarchy`. A non-null return indicates the store should be
60
+ * rendered via `@developmentseed/deck.gl-zarr`; a null return sends the
61
+ * caller to the `@carbonplan/zarr-layer` fallback.
62
+ */
63
+ export declare function detectGeoZarr(hierarchy: ZarrHierarchy): GeoZarrInfo | null;
64
+ /**
65
+ * Convert a decoded GeoZarr tile (band-planar or packed RGB) into RGBA
66
+ * `ImageData` for deck.gl-zarr's `renderTile` callback. Input is expected to
67
+ * be a Uint8 or Uint16 typed array with either 3 interleaved bytes per pixel
68
+ * or 3 planar bands of width*height values.
69
+ *
70
+ * For 16-bit data the caller supplies the rescale range so the CPU
71
+ * normalization matches what the GPU `LinearRescale` module would produce.
72
+ */
73
+ export declare function zarrTileToImageData(raw: ArrayLike<number> & {
74
+ length: number;
75
+ }, width: number, height: number, opts?: {
76
+ layout?: 'packed' | 'planar';
77
+ bands?: 1 | 3;
78
+ rescaleMin?: number;
79
+ rescaleMax?: number;
80
+ }): ImageData;
47
81
  /** Dimension-like variable names treated as coordinates. */
48
82
  export declare const DIM_LIKE_NAMES: Set<string>;
49
83
  /** Guess dimension names from shape length when metadata is absent. */