@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.
- package/README.md +6 -3
- package/dist/components/layout/ConnectionDialog.svelte +35 -3
- package/dist/components/layout/Sidebar.svelte +1 -2
- package/dist/components/viewers/CodeViewer.svelte +51 -14
- package/dist/components/viewers/CodeViewer.svelte.d.ts +11 -1
- package/dist/components/viewers/CogControls.svelte +151 -22
- package/dist/components/viewers/CogControls.svelte.d.ts +5 -1
- package/dist/components/viewers/CogViewer.svelte +75 -8
- package/dist/components/viewers/MultiCogViewer.svelte +416 -0
- package/dist/components/viewers/MultiCogViewer.svelte.d.ts +9 -0
- package/dist/components/viewers/StacMapViewer.svelte +19 -5
- package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
- package/dist/components/viewers/StacMosaicViewer.svelte +785 -0
- package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +9 -0
- package/dist/components/viewers/StacTabViewer.svelte +254 -0
- package/dist/components/viewers/StacTabViewer.svelte.d.ts +13 -0
- package/dist/components/viewers/ViewerRouter.svelte +155 -2
- package/dist/components/viewers/ViewerRouter.svelte.d.ts +1 -1
- package/dist/components/viewers/ZarrMapViewer.svelte +143 -4
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +8 -2
- package/dist/components/viewers/ZarrViewer.svelte +1 -0
- package/dist/i18n/ar.js +27 -0
- package/dist/i18n/en.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/query/stac-geoparquet.d.ts +31 -0
- package/dist/query/stac-geoparquet.js +136 -0
- package/dist/stores/connections.svelte.d.ts +38 -23
- package/dist/stores/connections.svelte.js +105 -114
- package/dist/utils/cog-pure.d.ts +25 -0
- package/dist/utils/cog-pure.js +35 -0
- package/dist/utils/cog.d.ts +88 -43
- package/dist/utils/cog.js +192 -152
- package/dist/utils/colormap-sprite.d.ts +39 -0
- package/dist/utils/colormap-sprite.js +77 -0
- package/dist/utils/connection-identity.d.ts +51 -0
- package/dist/utils/connection-identity.js +97 -0
- package/dist/utils/host-detection.js +48 -302
- package/dist/utils/parquet-metadata.d.ts +7 -1
- package/dist/utils/parquet-metadata.js +35 -1
- package/dist/utils/stac-geoparquet.d.ts +90 -0
- package/dist/utils/stac-geoparquet.js +223 -0
- package/dist/utils/stac-hydrate.d.ts +38 -0
- package/dist/utils/stac-hydrate.js +243 -0
- package/dist/utils/stac.d.ts +136 -0
- package/dist/utils/stac.js +176 -0
- package/dist/utils/storage-url.d.ts +26 -0
- package/dist/utils/storage-url.js +164 -28
- package/dist/utils/zarr.d.ts +34 -0
- package/dist/utils/zarr.js +94 -0
- 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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/dist/utils/zarr.d.ts
CHANGED
|
@@ -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. */
|