@walkthru-earth/objex 1.2.1 → 1.3.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 (49) 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 +24 -7
  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 +11 -5
  12. package/dist/components/viewers/StacMapViewer.svelte.d.ts +1 -0
  13. package/dist/components/viewers/StacMosaicViewer.svelte +699 -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.d.ts +80 -18
  31. package/dist/utils/cog.js +187 -125
  32. package/dist/utils/colormap-sprite.d.ts +39 -0
  33. package/dist/utils/colormap-sprite.js +77 -0
  34. package/dist/utils/connection-identity.d.ts +51 -0
  35. package/dist/utils/connection-identity.js +97 -0
  36. package/dist/utils/host-detection.js +48 -302
  37. package/dist/utils/parquet-metadata.d.ts +7 -1
  38. package/dist/utils/parquet-metadata.js +35 -1
  39. package/dist/utils/stac-geoparquet.d.ts +90 -0
  40. package/dist/utils/stac-geoparquet.js +223 -0
  41. package/dist/utils/stac-hydrate.d.ts +38 -0
  42. package/dist/utils/stac-hydrate.js +243 -0
  43. package/dist/utils/stac.d.ts +136 -0
  44. package/dist/utils/stac.js +176 -0
  45. package/dist/utils/storage-url.d.ts +26 -0
  46. package/dist/utils/storage-url.js +164 -28
  47. package/dist/utils/zarr.d.ts +34 -0
  48. package/dist/utils/zarr.js +94 -0
  49. package/package.json +14 -13
@@ -1,11 +1,17 @@
1
- import type { Tab } from '../../types';
2
- import { type ZarrNode } from '../../utils/zarr.js';
1
+ import type { Tab } from '../../types.js';
2
+ import { type ZarrHierarchy, type ZarrNode } from '../../utils/zarr.js';
3
3
  type $$ComponentProps = {
4
4
  tab: Tab;
5
5
  variables: ZarrNode[];
6
6
  coords?: ZarrNode[];
7
7
  spatialRefAttrs: Record<string, any> | null;
8
8
  zarrVersion?: number | null;
9
+ /**
10
+ * Full pre-loaded hierarchy. When present, `detectGeoZarr` can short-circuit
11
+ * to the `@developmentseed/deck.gl-zarr` path for GeoZarr-valid stores.
12
+ * Non-GeoZarr stores fall through to `@carbonplan/zarr-layer`.
13
+ */
14
+ hierarchy?: ZarrHierarchy | null;
9
15
  };
10
16
  declare const ZarrMapViewer: import("svelte").Component<$$ComponentProps, {}, "">;
11
17
  type ZarrMapViewer = ReturnType<typeof ZarrMapViewer>;
@@ -474,6 +474,7 @@ function selectStoreAttrs() {
474
474
  coords={coordArrays}
475
475
  spatialRefAttrs={hierarchy?.spatialRefAttrs ?? null}
476
476
  zarrVersion={hierarchy?.zarrVersion}
477
+ hierarchy={hierarchy ?? null}
477
478
  />
478
479
  {/await}
479
480
  {/key}
package/dist/i18n/ar.js CHANGED
@@ -40,6 +40,8 @@ export const ar = {
40
40
  'connection.readOnlyCliTitle': 'تقييد عبر سطر الأوامر',
41
41
  'connection.testSuccess': 'الاتصال ناجح',
42
42
  'connection.testFail': 'فشل الاتصال. تحقق من الإعدادات وحاول مرة أخرى.',
43
+ 'connection.duplicateMerged': 'تم العثور على اتصال موجود ("{name}") لهذه الحاوية. تم تحديث بيانات الاعتماد.',
44
+ 'connection.duplicateBlocked': 'يوجد بالفعل اتصال آخر ("{name}") يستخدم هذه الحاوية. عدّله بدلاً من ذلك.',
43
45
  'connection.testButton': 'اختبار الاتصال',
44
46
  'connection.testing': 'جارٍ الاختبار...',
45
47
  'connection.cancel': 'إلغاء',
@@ -300,6 +302,7 @@ export const ar = {
300
302
  'code.stacCatalog': 'كتالوج STAC',
301
303
  'code.stacCollection': 'مجموعة STAC',
302
304
  'code.stacItem': 'عنصر STAC',
305
+ 'code.stacGeoparquet': 'stac-geoparquet',
303
306
  'code.browseStac': 'تصفح',
304
307
  'code.keplerGl': 'Kepler.gl',
305
308
  'code.openKepler': 'فتح الخريطة',
@@ -382,11 +385,35 @@ export const ar = {
382
385
  'mapInfo.columns': 'الأعمدة',
383
386
  'mapInfo.size': 'الحجم',
384
387
  'mapInfo.bands': 'النطاقات',
388
+ 'map.mosaicEmpty': 'كتالوج STAC لا يحتوي على عناصر بها أصول COG.',
389
+ 'map.mosaicNoAssets': 'لا تحتوي عناصر STAC على عنوان URL صالح لأصل COG.',
390
+ 'map.multiCogMissingBands': 'عنصر STAC هذا ينقصه نطاقات الأحمر والأخضر والأزرق اللازمة للتركيب.',
391
+ 'map.multiCogPreset.label': 'الإعداد المسبق',
392
+ 'map.multiCogPreset.trueColor': 'ألوان طبيعية',
393
+ 'map.multiCogPreset.falseColorIR': 'أشعة تحت حمراء كاذبة اللون',
394
+ 'map.multiCogPreset.swir': 'الأشعة تحت الحمراء قصيرة الموجة',
395
+ 'map.multiCogPreset.vegetation': 'الغطاء النباتي',
396
+ 'map.multiCogPreset.agriculture': 'الزراعة',
397
+ // تبديل عرض STAC (StacTabViewer)
398
+ 'stac.viewMosaic': 'خريطة',
399
+ 'stac.viewMultiCog': 'خريطة (نطاقات)',
400
+ 'stac.viewStacMap': 'stac-map',
401
+ 'stac.viewBrowser': 'متصفح STAC',
402
+ 'stac.viewJson': 'JSON',
403
+ 'stac.viewTable': 'جدول',
404
+ 'stac.stacBrowserJsonOnly': 'متصفح STAC يدعم كتالوجات JSON فقط. استخدم stac-map لملفات parquet.',
405
+ 'stac.iframeDisabledPrivate': 'معطل للبكتات الخاصة، الإطار الخارجي لا يستطيع توقيع طلباته. استخدم الخريطة أو JSON بدلاً من ذلك.',
406
+ 'stac.iframePrivateBucketWarning': 'بكت خاص، الإطار الخارجي لا يستطيع توقيع طلباته، لذا ستفشل طلبات العناصر الفرعية بـ 403. لكن المانيفست الجذري سيعمل.',
407
+ 'stac.mosaicSourcesOne': 'فسيفساء، {count} مصدر',
408
+ 'stac.mosaicSourcesOther': 'فسيفساء، {count} مصادر',
409
+ 'stac.mosaicInfo': 'معلومات الفسيفساء',
410
+ 'stac.mosaicSourcesLabel': 'المصادر',
385
411
  // COG Controls
386
412
  'cog.style': 'النمط',
387
413
  'cog.band': 'النطاق',
388
414
  'cog.singleBand': 'نطاق واحد',
389
415
  'cog.colorRamp': 'تدرج الألوان',
416
+ 'cog.colorRampSearch': 'بحث في التدرجات…',
390
417
  'cog.pixelValue': 'قيمة البكسل',
391
418
  'cog.reading': 'قراءة البكسل...',
392
419
  'cog.rescale': 'إعادة القياس',
package/dist/i18n/en.js CHANGED
@@ -40,6 +40,8 @@ export const en = {
40
40
  'connection.readOnlyCliTitle': 'Restrict via CLI',
41
41
  'connection.testSuccess': 'Connection successful',
42
42
  'connection.testFail': 'Connection failed. Check your settings and try again.',
43
+ 'connection.duplicateMerged': 'Matched an existing connection ("{name}") for this bucket. Credentials were updated.',
44
+ 'connection.duplicateBlocked': 'Another connection ("{name}") already uses this bucket. Edit that one instead.',
43
45
  'connection.testButton': 'Test Connection',
44
46
  'connection.testing': 'Testing...',
45
47
  'connection.cancel': 'Cancel',
@@ -300,6 +302,7 @@ export const en = {
300
302
  'code.stacCatalog': 'STAC Catalog',
301
303
  'code.stacCollection': 'STAC Collection',
302
304
  'code.stacItem': 'STAC Item',
305
+ 'code.stacGeoparquet': 'stac-geoparquet',
303
306
  'code.browseStac': 'Browse',
304
307
  'code.keplerGl': 'Kepler.gl',
305
308
  'code.openKepler': 'Open Map',
@@ -382,11 +385,35 @@ export const en = {
382
385
  'mapInfo.columns': 'Columns',
383
386
  'mapInfo.size': 'Size',
384
387
  'mapInfo.bands': 'Bands',
388
+ 'map.mosaicEmpty': 'STAC catalog has no items with COG assets.',
389
+ 'map.mosaicNoAssets': 'None of the STAC items expose a usable COG asset URL.',
390
+ 'map.multiCogMissingBands': 'This STAC item is missing the red/green/blue bands required for a composite.',
391
+ 'map.multiCogPreset.label': 'Preset',
392
+ 'map.multiCogPreset.trueColor': 'True Color',
393
+ 'map.multiCogPreset.falseColorIR': 'False-Color IR',
394
+ 'map.multiCogPreset.swir': 'SWIR',
395
+ 'map.multiCogPreset.vegetation': 'Vegetation',
396
+ 'map.multiCogPreset.agriculture': 'Agriculture',
397
+ // STAC tab toggle (StacTabViewer)
398
+ 'stac.viewMosaic': 'Map',
399
+ 'stac.viewMultiCog': 'Map (Bands)',
400
+ 'stac.viewStacMap': 'stac-map',
401
+ 'stac.viewBrowser': 'STAC Browser',
402
+ 'stac.viewJson': 'JSON',
403
+ 'stac.viewTable': 'Table',
404
+ 'stac.stacBrowserJsonOnly': 'STAC Browser supports JSON catalogs only. Use stac-map for parquet.',
405
+ 'stac.iframeDisabledPrivate': 'Disabled for private buckets, the external iframe cannot sign its own crawl. Use Map or JSON instead.',
406
+ 'stac.iframePrivateBucketWarning': 'Private bucket, the external iframe cannot sign its own crawl, so child items will 403. The top manifest still renders.',
407
+ 'stac.mosaicSourcesOne': 'Mosaic, {count} source',
408
+ 'stac.mosaicSourcesOther': 'Mosaic, {count} sources',
409
+ 'stac.mosaicInfo': 'Mosaic info',
410
+ 'stac.mosaicSourcesLabel': 'Sources',
385
411
  // COG Controls
386
412
  'cog.style': 'Style',
387
413
  'cog.band': 'Band',
388
414
  'cog.singleBand': 'Single',
389
415
  'cog.colorRamp': 'Color ramp',
416
+ 'cog.colorRampSearch': 'Search ramps…',
390
417
  'cog.pixelValue': 'Pixel Value',
391
418
  'cog.reading': 'Reading pixel...',
392
419
  'cog.rescale': 'Rescale',
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export type { CogInfo, GeoBounds } from './utils/cog.js';
14
14
  export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './utils/cog.js';
15
15
  export type { TypeCategory } from './utils/column-types.js';
16
16
  export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
17
+ export type { ConnectionIdentityInput } from './utils/connection-identity.js';
18
+ export { connectionIdentityKey, isSameConnectionIdentity, normalizeEndpoint, normalizeProvider } from './utils/connection-identity.js';
17
19
  export { handleLoadError } from './utils/error.js';
18
20
  export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
19
21
  export type { SortConfig, SortDirection, SortField } from './utils/file-sort.js';
@@ -28,6 +30,8 @@ export type { ParsedMarkdownDocument, SqlBlock } from './utils/markdown-sql.js';
28
30
  export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
29
31
  export type { GeoColumnMeta, GeoParquetMeta, ParquetFileMetadata } from './utils/parquet-metadata.js';
30
32
  export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
33
+ export type { StacBboxStruct, StacGeoparquetRow, StacGeoparquetSchemaColumn, StacRowToItemOptions } from './utils/stac-geoparquet.js';
34
+ export { flattenStacBbox, isStacGeoparquetSchema, pickStacPrimaryAsset, resolveStacAssetHref, STAC_GEOPARQUET_REQUIRED_COLUMNS, stacRowToItem } from './utils/stac-geoparquet.js';
31
35
  export type { Defaults, ParsedStorageUrl, StorageProvider } from './utils/storage-url.js';
32
36
  export { describeParseResult, looksLikeUrl, parseStorageUrl } from './utils/storage-url.js';
33
37
  export type { GeoType, ParsedGeometry } from './utils/wkb.js';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
12
12
  export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
13
13
  export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './utils/cog.js';
14
14
  export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
15
+ export { connectionIdentityKey, isSameConnectionIdentity, normalizeEndpoint, normalizeProvider } from './utils/connection-identity.js';
15
16
  // Error handling
16
17
  export { handleLoadError } from './utils/error.js';
17
18
  // Data export / serialization
@@ -24,6 +25,7 @@ export { generateHexDump } from './utils/hex.js';
24
25
  export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
25
26
  export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
26
27
  export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
28
+ export { flattenStacBbox, isStacGeoparquetSchema, pickStacPrimaryAsset, resolveStacAssetHref, STAC_GEOPARQUET_REQUIRED_COLUMNS, stacRowToItem } from './utils/stac-geoparquet.js';
27
29
  export { describeParseResult, looksLikeUrl, parseStorageUrl } from './utils/storage-url.js';
28
30
  // Utilities
29
31
  export { findGeoColumn, findGeoColumnFromRows, parseWKB, toBinary } from './utils/wkb.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Read a stac-geoparquet file through the existing DuckDB-WASM engine and
3
+ * materialize a standard STAC FeatureCollection in memory.
4
+ *
5
+ * Reuses:
6
+ * - `getQueryEngine()` + `queryCancellable`/`query` for the single worker
7
+ * - `resolveTableSourceAsync(tab)` for presigned `signed-s3` URL handling
8
+ * - `stacRowToItem` from `utils/stac-geoparquet.js` for the pure transform
9
+ * - `parseWKB` from `utils/wkb.js` for geometry decoding
10
+ *
11
+ * The returned `FeatureCollection` is the same shape `classifyStac()` returns
12
+ * as `{ kind: 'item-collection', fc }`, so downstream viewers
13
+ * (`StacMosaicViewer`, `MultiCogViewer`) consume it unchanged.
14
+ */
15
+ import type { Tab } from '../types.js';
16
+ import type { StacFeatureCollection } from '../utils/stac.js';
17
+ export interface QueryStacGeoparquetOptions {
18
+ signal?: AbortSignal;
19
+ /** Hard cap on rows. Matches `hydrateStacItems` default. */
20
+ limit?: number;
21
+ }
22
+ /**
23
+ * Query a stac-geoparquet tab and return a STAC FeatureCollection whose
24
+ * features are proper STAC Items (assets absolutized, WKB decoded, bbox
25
+ * flattened).
26
+ *
27
+ * @param tab - the tab pointing at the `.parquet` file
28
+ * @param connId - connection id used for DuckDB's httpfs S3 config; pass
29
+ * an empty string for URL-source tabs (DuckDB will use anonymous httpfs)
30
+ */
31
+ export declare function queryStacGeoparquetFeatureCollection(tab: Tab, connId: string, opts?: QueryStacGeoparquetOptions): Promise<StacFeatureCollection>;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Read a stac-geoparquet file through the existing DuckDB-WASM engine and
3
+ * materialize a standard STAC FeatureCollection in memory.
4
+ *
5
+ * Reuses:
6
+ * - `getQueryEngine()` + `queryCancellable`/`query` for the single worker
7
+ * - `resolveTableSourceAsync(tab)` for presigned `signed-s3` URL handling
8
+ * - `stacRowToItem` from `utils/stac-geoparquet.js` for the pure transform
9
+ * - `parseWKB` from `utils/wkb.js` for geometry decoding
10
+ *
11
+ * The returned `FeatureCollection` is the same shape `classifyStac()` returns
12
+ * as `{ kind: 'item-collection', fc }`, so downstream viewers
13
+ * (`StacMosaicViewer`, `MultiCogViewer`) consume it unchanged.
14
+ */
15
+ import { stacRowToItem } from '../utils/stac-geoparquet.js';
16
+ import { parseWKB } from '../utils/wkb.js';
17
+ import { QueryCancelledError } from './engine.js';
18
+ import { getQueryEngine } from './index.js';
19
+ import { resolveTableSourceAsync } from './source.js';
20
+ const DEFAULT_LIMIT = 2000;
21
+ /**
22
+ * Build the SELECT list. All columns are optional in the stac-geoparquet
23
+ * spec, so we only project what we know we'll use and the spec requires.
24
+ * The optional `proj:*` / `raster:*` / `bands` columns are sniffed from the
25
+ * schema so missing columns don't trigger a DuckDB binder error.
26
+ */
27
+ function buildSelectList(availableColumns) {
28
+ const required = [
29
+ 'id',
30
+ 'collection',
31
+ 'type',
32
+ 'stac_version',
33
+ 'stac_extensions',
34
+ 'assets',
35
+ 'bbox',
36
+ 'links'
37
+ ];
38
+ const optional = [
39
+ 'datetime',
40
+ 'proj:code',
41
+ 'proj:bbox',
42
+ 'proj:transform',
43
+ 'proj:shape',
44
+ 'raster:spatial_resolution',
45
+ 'bands'
46
+ ];
47
+ const cols = [];
48
+ for (const name of required) {
49
+ if (availableColumns.has(name))
50
+ cols.push(quoteIdent(name));
51
+ }
52
+ for (const name of optional) {
53
+ if (availableColumns.has(name))
54
+ cols.push(quoteIdent(name));
55
+ }
56
+ // Always project geometry as WKB so parseWKB can decode it regardless of
57
+ // whether DuckDB presents it as the v1.5 GEOMETRY type or a plain BLOB.
58
+ if (availableColumns.has('geometry')) {
59
+ cols.push('ST_AsWKB(geometry) AS geom_wkb');
60
+ }
61
+ return cols.join(', ');
62
+ }
63
+ function quoteIdent(name) {
64
+ return `"${name.replace(/"/g, '""')}"`;
65
+ }
66
+ /**
67
+ * Query a stac-geoparquet tab and return a STAC FeatureCollection whose
68
+ * features are proper STAC Items (assets absolutized, WKB decoded, bbox
69
+ * flattened).
70
+ *
71
+ * @param tab - the tab pointing at the `.parquet` file
72
+ * @param connId - connection id used for DuckDB's httpfs S3 config; pass
73
+ * an empty string for URL-source tabs (DuckDB will use anonymous httpfs)
74
+ */
75
+ export async function queryStacGeoparquetFeatureCollection(tab, connId, opts = {}) {
76
+ const { signal, limit = DEFAULT_LIMIT } = opts;
77
+ if (signal?.aborted)
78
+ throw new QueryCancelledError();
79
+ const engine = await getQueryEngine();
80
+ const resolved = await resolveTableSourceAsync(tab);
81
+ if (signal?.aborted)
82
+ throw new QueryCancelledError();
83
+ // Discover which optional columns are present so the SELECT list doesn't
84
+ // reference missing columns.
85
+ const schema = await engine.getSchema(connId, resolved);
86
+ if (signal?.aborted)
87
+ throw new QueryCancelledError();
88
+ const available = new Set(schema.map((f) => f.name));
89
+ const selectList = buildSelectList(available);
90
+ if (!available.has('geometry') || !available.has('assets')) {
91
+ throw new Error('Not a stac-geoparquet file (missing geometry or assets column)');
92
+ }
93
+ const sql = `SELECT ${selectList} FROM ${resolved.ref} LIMIT ${limit}`;
94
+ // Prefer cancellable path when the engine exposes it.
95
+ let resultPromise;
96
+ let cancel = null;
97
+ if (engine.queryCancellable) {
98
+ const handle = engine.queryCancellable(connId, sql);
99
+ cancel = handle.cancel;
100
+ resultPromise = handle.result;
101
+ }
102
+ else {
103
+ resultPromise = engine.query(connId, sql);
104
+ }
105
+ const onAbort = () => {
106
+ cancel?.().catch(() => { });
107
+ };
108
+ signal?.addEventListener('abort', onAbort, { once: true });
109
+ let rows;
110
+ try {
111
+ const result = await resultPromise;
112
+ rows = result.rows ?? [];
113
+ }
114
+ finally {
115
+ signal?.removeEventListener('abort', onAbort);
116
+ }
117
+ if (signal?.aborted)
118
+ throw new QueryCancelledError();
119
+ // Asset hrefs in stac-geoparquet are typically written relative to each
120
+ // item's original `self` URL, not the parquet URL. The stactools default
121
+ // layout places each item JSON at `{catalog_dir}/{item.id}/{item.id}.json`,
122
+ // so a per-row base of `{parquet_dir}/{item.id}/` resolves `./foo.tif` to
123
+ // `{parquet_dir}/{item.id}/foo.tif`. Absolute hrefs pass through unchanged
124
+ // via `resolveStacAssetHref`.
125
+ const parquetUrl = resolved.fileUrl ?? tab.path;
126
+ const parquetDir = parquetUrl.replace(/[^/]*(?:\?.*)?$/, '');
127
+ const features = rows.map((row) => {
128
+ const id = typeof row.id === 'string' ? row.id : String(row.id ?? '');
129
+ const itemBase = id ? `${parquetDir}${id}/` : parquetUrl;
130
+ return stacRowToItem(row, itemBase, { wkbParser: parseWKB });
131
+ });
132
+ return {
133
+ type: 'FeatureCollection',
134
+ features
135
+ };
136
+ }
@@ -1,55 +1,70 @@
1
1
  import type { Connection, ConnectionConfig } from '../types.js';
2
+ import { type ConnectionIdentityInput } from '../utils/connection-identity.js';
2
3
  import type { DetectedHost } from '../utils/host-detection.js';
4
+ /**
5
+ * Outcome of a write. `existed` is true when dedup reused an already-saved
6
+ * connection, false when a new row was persisted. Callers that present UI
7
+ * (dialogs, toasts) use this to decide whether to say "created" or "merged
8
+ * into existing".
9
+ */
10
+ export interface ConnectionWriteResult {
11
+ id: string;
12
+ existed: boolean;
13
+ }
14
+ /**
15
+ * Thrown by `update()` when the proposed identity collides with a different
16
+ * saved connection. Lets the UI tell the user which connection already owns
17
+ * that identity instead of silently producing a phantom duplicate.
18
+ */
19
+ export declare class DuplicateConnectionError extends Error {
20
+ readonly existingId: string;
21
+ readonly existingName: string;
22
+ constructor(existingId: string, existingName: string);
23
+ }
3
24
  export declare const connectionStore: {
4
25
  readonly items: Connection[];
5
26
  readonly loaded: boolean;
6
27
  /**
7
28
  * Load connections from localStorage.
8
- * Safe to call multiple times subsequent calls are no-ops.
29
+ * Safe to call multiple times, subsequent calls are no-ops.
9
30
  */
10
31
  load(): Promise<void>;
11
- /**
12
- * Force-reload connections.
13
- */
14
32
  reload(): Promise<void>;
15
33
  /**
16
- * Save a new connection to localStorage.
34
+ * Persist a connection. If an existing connection shares the same
35
+ * identity (see `connectionIdentityKey`), it's reused and its
36
+ * credentials are refreshed from the new config instead of spawning
37
+ * a duplicate record. Returns `{ id, existed }` so UI can distinguish
38
+ * "created" from "merged".
17
39
  */
18
- save(config: ConnectionConfig): Promise<string | null>;
40
+ save(config: ConnectionConfig): Promise<ConnectionWriteResult>;
19
41
  /**
20
- * Update an existing connection.
42
+ * Update an existing connection. Throws `DuplicateConnectionError`
43
+ * when the new identity would collide with a different saved row,
44
+ * rather than silently overwriting and leaving a phantom duplicate.
21
45
  */
22
46
  update(id: string, config: ConnectionConfig): Promise<boolean>;
23
- /**
24
- * Remove a connection by ID.
25
- */
26
47
  remove(id: string): Promise<boolean>;
27
- /**
28
- * Test whether a connection is reachable via a lightweight list.
29
- */
30
48
  test(id: string): Promise<boolean>;
31
49
  /**
32
50
  * Test a connection using provided config values (works for both new
33
51
  * and existing connections without saving first).
34
52
  */
35
53
  testWithConfig(config: ConnectionConfig, existingId?: string): Promise<boolean>;
36
- /** True when a dialog open has been requested and not yet consumed. */
37
54
  readonly dialogRequested: boolean;
38
- /** Request opening the new-connection dialog from anywhere. */
39
55
  requestDialog(): void;
40
- /** Mark the dialog request as consumed. */
41
56
  clearDialogRequest(): void;
42
- /**
43
- * Synchronous lookup by ID (from the already-loaded list).
44
- */
45
57
  getById(id: string): Connection | undefined;
46
58
  /**
47
- * Find an existing connection that matches bucket + endpoint.
59
+ * Find an already-saved connection that matches the canonical identity
60
+ * of `input` (provider + bucket + endpoint/region per provider rules).
61
+ * Used by auto-detect, manual-add dedup, and edit-collision checks.
48
62
  */
49
- findByBucketEndpoint(bucket: string, endpoint: string): Connection | undefined;
63
+ findByIdentity(input: ConnectionIdentityInput): Connection | undefined;
50
64
  /**
51
- * Create a connection from a DetectedHost, deduplicating by bucket+endpoint.
52
- * Returns the connection ID (existing or newly created).
65
+ * Auto-connect path for a URL-detected bucket. Reuses an existing
66
+ * connection when identity matches, otherwise creates one anonymously.
67
+ * Always returns the final connection ID.
53
68
  */
54
69
  saveHostConnection(detected: DetectedHost): Promise<string>;
55
70
  };