@walkthru-earth/objex 1.0.0 → 1.2.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 (84) hide show
  1. package/README.md +11 -2
  2. package/dist/components/browser/FileBrowser.svelte +41 -54
  3. package/dist/components/browser/FileTreeSidebar.svelte +43 -7
  4. package/dist/components/layout/ConnectionDialog.svelte +100 -1
  5. package/dist/components/layout/Sidebar.svelte +43 -25
  6. package/dist/components/viewers/CodeViewer.svelte +23 -0
  7. package/dist/components/viewers/CogControls.svelte +208 -0
  8. package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
  9. package/dist/components/viewers/CogViewer.svelte +353 -1160
  10. package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
  11. package/dist/components/viewers/DatabaseViewer.svelte +345 -37
  12. package/dist/components/viewers/MarkdownViewer.svelte +1 -1
  13. package/dist/components/viewers/TableViewer.svelte +123 -41
  14. package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
  15. package/dist/components/viewers/ZarrViewer.svelte +1 -4
  16. package/dist/constants.d.ts +6 -2
  17. package/dist/constants.js +6 -2
  18. package/dist/file-icons/index.d.ts +1 -1
  19. package/dist/file-icons/index.js +12 -2
  20. package/dist/i18n/ar.js +24 -0
  21. package/dist/i18n/en.js +24 -0
  22. package/dist/i18n/index.svelte.d.ts +0 -1
  23. package/dist/i18n/index.svelte.js +0 -3
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +10 -0
  26. package/dist/query/engine.d.ts +20 -4
  27. package/dist/query/index.d.ts +2 -1
  28. package/dist/query/index.js +1 -0
  29. package/dist/query/source.d.ts +30 -0
  30. package/dist/query/source.js +37 -0
  31. package/dist/query/wasm.d.ts +7 -5
  32. package/dist/query/wasm.js +138 -85
  33. package/dist/storage/providers.d.ts +47 -0
  34. package/dist/storage/providers.js +160 -0
  35. package/dist/stores/connections.svelte.js +5 -31
  36. package/dist/stores/files.svelte.d.ts +2 -8
  37. package/dist/stores/files.svelte.js +5 -38
  38. package/dist/stores/query-history.svelte.js +3 -25
  39. package/dist/stores/settings.svelte.d.ts +1 -0
  40. package/dist/stores/settings.svelte.js +10 -30
  41. package/dist/stores/tabs.svelte.d.ts +9 -2
  42. package/dist/stores/tabs.svelte.js +11 -2
  43. package/dist/types.d.ts +11 -0
  44. package/dist/utils/cloud-url.d.ts +27 -0
  45. package/dist/utils/cloud-url.js +61 -0
  46. package/dist/utils/cog.d.ts +244 -0
  47. package/dist/utils/cog.js +1039 -0
  48. package/dist/utils/deck.d.ts +0 -18
  49. package/dist/utils/deck.js +0 -36
  50. package/dist/utils/export.d.ts +22 -2
  51. package/dist/utils/export.js +35 -10
  52. package/dist/utils/file-sort.d.ts +20 -0
  53. package/dist/utils/file-sort.js +41 -0
  54. package/dist/utils/geometry-type.d.ts +52 -0
  55. package/dist/utils/geometry-type.js +76 -0
  56. package/dist/utils/local-storage.d.ts +16 -0
  57. package/dist/utils/local-storage.js +37 -0
  58. package/dist/utils/markdown-sql.d.ts +1 -1
  59. package/dist/utils/markdown-sql.js +3 -4
  60. package/dist/utils/pmtiles-tile.d.ts +0 -2
  61. package/dist/utils/pmtiles-tile.js +0 -8
  62. package/dist/utils/url-state.d.ts +6 -0
  63. package/dist/utils/url-state.js +34 -26
  64. package/dist/utils/url.d.ts +13 -25
  65. package/dist/utils/url.js +17 -78
  66. package/dist/utils/zarr-tab.d.ts +22 -0
  67. package/dist/utils/zarr-tab.js +30 -0
  68. package/dist/utils/zarr.d.ts +0 -2
  69. package/dist/utils/zarr.js +73 -44
  70. package/package.json +50 -46
  71. package/dist/components/ui/tabs/index.d.ts +0 -5
  72. package/dist/components/ui/tabs/index.js +0 -7
  73. package/dist/components/ui/tabs/tabs-content.svelte +0 -17
  74. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
  75. package/dist/components/ui/tabs/tabs-list.svelte +0 -16
  76. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
  77. package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
  78. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
  79. package/dist/components/ui/tabs/tabs.svelte +0 -19
  80. package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
  81. package/dist/components/viewers/MapViewer.svelte +0 -234
  82. package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
  83. package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
  84. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
@@ -2,15 +2,23 @@
2
2
  import { format as formatSql } from 'sql-formatter';
3
3
  import { untrack } from 'svelte';
4
4
  import CodeMirrorEditor from '../editor/CodeMirrorEditor.svelte';
5
- import { buildDuckDbSource, isCloudNativeFormat } from '../../file-icons/index.js';
5
+ import { DEFAULT_TARGET_CRS } from '../../constants.js';
6
+ import { isCloudNativeFormat } from '../../file-icons/index.js';
6
7
  import { t } from '../../i18n/index.svelte.js';
7
8
  import type { MapQueryResult, SchemaField } from '../../query/engine';
8
- import { getQueryEngine, QueryCancelledError, type QueryHandle } from '../../query/index.js';
9
+ import {
10
+ getQueryEngine,
11
+ QueryCancelledError,
12
+ type QueryHandle,
13
+ type ResolvedTableSource,
14
+ resolveTableSource
15
+ } from '../../query/index.js';
9
16
  import { queryHistory } from '../../stores/query-history.svelte.js';
10
17
  import { settings } from '../../stores/settings.svelte.js';
11
18
  import { tabResources } from '../../stores/tab-resources.svelte.js';
12
19
  import type { Tab } from '../../types';
13
20
  import type { GeoArrowGeomType } from '../../utils/geoarrow.js';
21
+ import { buildTransformExpr, wrapWkbWithCrs } from '../../utils/geometry-type.js';
14
22
  import {
15
23
  extractBounds,
16
24
  extractEpsgFromGeoMeta,
@@ -91,26 +99,23 @@ const columnTypes = $derived(Object.fromEntries(schema.map((f) => [f.name, f.typ
91
99
  const displayColumns = $derived(columns.filter((c) => c !== '__wkb'));
92
100
 
93
101
  function buildDefaultSql(offset = 0): string {
94
- const fileUrl = buildDuckDbUrl(tab);
95
- const source = buildDuckDbSource(tab.path, fileUrl);
102
+ const resolved = resolveTableSource(tab);
103
+ const source = resolved.ref;
96
104
 
97
105
  let sql: string;
98
106
  if (geoCol) {
99
107
  const quoted = `"${geoCol}"`;
100
108
  const upper = geoColType.toUpperCase();
101
- // Spatial types that ST_AsWKB accepts directly (GEOMETRY, WKB_BLOB, etc.).
102
- // Includes Arrow "Binary"/"LargeBinary" DuckDB GEOMETRY columns from
103
- // ST_ReadSHP/ST_Read appear as Arrow Binary but are NOT WKB blobs.
104
- const isSpatialType =
105
- upper === 'GEOMETRY' ||
106
- upper === 'GEOGRAPHY' ||
109
+ // Spatial types that ST_AsWKB accepts directly: GEOMETRY, GEOMETRY('EPSG:...'),
110
+ // GEOGRAPHY, WKB_BLOB, POINT, LINESTRING, POLYGON, Binary (Arrow serialization).
111
+ const spatialType =
112
+ upper.startsWith('GEOMETRY') ||
113
+ upper.startsWith('GEOGRAPHY') ||
107
114
  upper === 'WKB_BLOB' ||
108
115
  upper.includes('POINT') ||
109
116
  upper.includes('LINESTRING') ||
110
117
  upper.includes('POLYGON') ||
111
- upper.includes('BINARY'); // Arrow serialization of DuckDB GEOMETRY
112
- // Actual WKB BLOB columns (e.g. GeoParquet) need explicit ST_GeomFromWKB
113
- // because DuckDB has no implicit BLOB→GEOMETRY cast.
118
+ upper.includes('BINARY');
114
119
  const isWkbBlob = upper === 'BLOB' || upper === 'BYTEA';
115
120
 
116
121
  let wkbExpr: string;
@@ -118,13 +123,17 @@ function buildDefaultSql(offset = 0): string {
118
123
  // Already WKB — use directly, no conversion needed
119
124
  wkbExpr = `${quoted} AS __wkb`;
120
125
  } else {
121
- let geomExpr = isSpatialType
126
+ // For BLOB inputs with a known source CRS, attach it via ST_SetCRS so the
127
+ // 2-arg ST_Transform form can be used (DuckDB v1.5+).
128
+ let geomExpr = spatialType
122
129
  ? quoted
123
130
  : isWkbBlob
124
- ? `ST_GeomFromWKB(${quoted})`
131
+ ? wrapWkbWithCrs(quoted, sourceCrs)
125
132
  : `ST_GeomFromGeoJSON(${quoted})`;
126
133
  if (sourceCrs) {
127
- geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', 'EPSG:4326', always_xy := true)`;
134
+ // geometry_always_xy is set globally at DB init, so no per-call always_xy.
135
+ const effectiveType = isWkbBlob ? `GEOMETRY('${sourceCrs}')` : geoColType;
136
+ geomExpr = buildTransformExpr(geomExpr, effectiveType, sourceCrs, DEFAULT_TARGET_CRS);
128
137
  }
129
138
  wkbExpr = `ST_AsWKB(${geomExpr}) AS __wkb`;
130
139
  }
@@ -284,19 +293,24 @@ async function loadTable() {
284
293
  customSql = initialSql;
285
294
 
286
295
  try {
287
- const fileUrl = buildDuckDbUrl(tab);
288
- const httpsUrl = buildHttpsUrl(tab);
289
- const cloudNative = isCloudNativeFormat(tab.path);
290
- const isParquet = /\.parquet$/i.test(tab.path);
291
- const streamable = canStreamDirectly(tab);
296
+ const resolved: ResolvedTableSource = resolveTableSource(tab);
297
+ const isFileSource = resolved.isFileSource;
298
+ const fileUrl = resolved.fileUrl ?? '';
299
+ const httpsUrl = isFileSource ? buildHttpsUrl(tab) : '';
300
+ const cloudNative = isFileSource && isCloudNativeFormat(tab.path);
301
+ const isParquet = isFileSource && /\.parquet$/i.test(tab.path);
302
+ const streamable = isFileSource && canStreamDirectly(tab);
292
303
 
293
304
  // Start DuckDB boot immediately (runs in parallel with hyparquet)
294
305
  loadStage = t('table.initEngine');
295
306
  const enginePromise = getQueryEngine();
296
307
 
297
308
  // ── Fast metadata via hyparquet (runs concurrently with DuckDB boot) ──
309
+ // Only applies to file-backed sources. SQL-backed sources (attached
310
+ // DuckLake tables, etc.) go straight to DuckDB for schema + CRS.
298
311
  let metaFromHyparquet = false;
299
312
  let needsDuckDbCrs = false;
313
+ let isLegacyGeoParquet = false;
300
314
  if (cloudNative && isParquet && streamable) {
301
315
  try {
302
316
  loadStage = t('table.readingMetadata');
@@ -357,11 +371,9 @@ async function loadTable() {
357
371
 
358
372
  if (meta.geo) {
359
373
  geoCol = meta.geo.primaryColumn;
360
- // With enable_geoparquet_conversion=false, DuckDB reads ALL
361
- // GeoParquet geometry columns as BLOB regardless of Parquet
362
- // logical type. Native Parquet GEOMETRY (Format 2.11+) is a
363
- // DuckDB v1.5 feature not yet in WASM (v1.33).
364
- geoColType = 'BLOB';
374
+ // DuckDB v1.5 reads GeoParquet columns as GEOMETRY('EPSG:...').
375
+ // Set GEOMETRY here as placeholder DuckDB schema will refine.
376
+ geoColType = 'GEOMETRY';
365
377
  sourceCrs = extractEpsgFromGeoMeta(meta.geo);
366
378
  const geomTypes = extractGeometryTypes(meta.geo);
367
379
  if (geomTypes.length === 1) knownGeomType = geomTypes[0];
@@ -401,9 +413,8 @@ async function loadTable() {
401
413
  const detectedGeoCol = findGeoColumn(schema);
402
414
  if (detectedGeoCol) {
403
415
  geoCol = detectedGeoCol;
404
- // DuckDB WASM v1.33 doesn't support native Parquet GEOMETRY
405
- // columns are always BLOB with enable_geoparquet_conversion=false.
406
- geoColType = 'BLOB';
416
+ // DuckDB v1.5 reads native Parquet GEOMETRY natively.
417
+ geoColType = 'GEOMETRY';
407
418
  needsDuckDbCrs = true;
408
419
  loadProgress = [
409
420
  ...loadProgress,
@@ -420,6 +431,7 @@ async function loadTable() {
420
431
  { label: t('progress.format'), value: t('progress.stacDetected') }
421
432
  ];
422
433
  }
434
+ isLegacyGeoParquet = meta.legacyGeoParquet;
423
435
  metaFromHyparquet = true;
424
436
  } catch {
425
437
  // hyparquet failed (CORS, auth, format) — fall back to DuckDB
@@ -431,11 +443,54 @@ async function loadTable() {
431
443
  const engine = await enginePromise;
432
444
  if (thisGen !== loadGeneration) return;
433
445
 
446
+ // Legacy GeoParquet (geopandas <0.12) — files with schema_version but no
447
+ // "version" field. DuckDB v1.5 may reject these with auto-conversion enabled.
448
+ // Disable conversion for this connection and fall back to BLOB handling.
449
+ if (metaFromHyparquet && isLegacyGeoParquet && geoCol) {
450
+ try {
451
+ await engine.query(connId, 'SET enable_geoparquet_conversion = false');
452
+ geoColType = 'BLOB';
453
+ } catch {
454
+ // Setting failed — DuckDB may still handle it gracefully
455
+ }
456
+ }
457
+
458
+ // DuckDB v1.5: refresh geoColType from DuckDB's actual schema.
459
+ // hyparquet reports BLOB for the physical Parquet type, but DuckDB v1.5
460
+ // reads GeoParquet as GEOMETRY('EPSG:...') with CRS embedded in the type.
461
+ if (metaFromHyparquet && geoCol && !isLegacyGeoParquet) {
462
+ try {
463
+ const duckSchema = await engine.getSchema(connId, resolved);
464
+ if (thisGen !== loadGeneration) return;
465
+ const duckGeoField = duckSchema.find((f: { name: string }) => f.name === geoCol);
466
+ if (duckGeoField) {
467
+ geoColType = duckGeoField.type;
468
+ // Extract CRS from type string — may override hyparquet CRS detection
469
+ const typeStr = duckGeoField.type.toUpperCase();
470
+ const crsMatch = duckGeoField.type.match(/^GEOMETRY\('([^']+)'\)/i);
471
+ if (crsMatch) {
472
+ const crsVal = crsMatch[1];
473
+ const isWgs84 =
474
+ crsVal === 'EPSG:4326' ||
475
+ crsVal === 'OGC:CRS84' ||
476
+ (crsVal.startsWith('EPSG:') && [4326, 4979].includes(Number(crsVal.split(':')[1])));
477
+ sourceCrs = isWgs84 ? null : crsVal;
478
+ needsDuckDbCrs = false;
479
+ } else if (typeStr.startsWith('GEOMETRY')) {
480
+ // GEOMETRY without CRS param — still need CRS from metadata
481
+ needsDuckDbCrs = !sourceCrs;
482
+ }
483
+ }
484
+ } catch {
485
+ // Schema refresh failed — continue with hyparquet-detected type
486
+ }
487
+ }
488
+
434
489
  // If hyparquet detected a geo column but couldn't determine CRS
435
490
  // (native Parquet GEOMETRY without "geo" KV metadata), use DuckDB
436
491
  if (metaFromHyparquet && needsDuckDbCrs && geoCol) {
437
492
  try {
438
- sourceCrs = await engine.detectCrs(connId, fileUrl, geoCol);
493
+ sourceCrs = await engine.detectCrs(connId, resolved, geoCol);
439
494
  if (thisGen !== loadGeneration) return;
440
495
  if (sourceCrs) {
441
496
  loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
@@ -450,16 +505,22 @@ async function loadTable() {
450
505
  }
451
506
  }
452
507
 
453
- if (cloudNative && !metaFromHyparquet) {
454
- // Fallback: DuckDB metadata queries
508
+ // DuckDB-backed schema + CRS pre-load. Runs when:
509
+ // - the source is SQL-backed (attached table, no hyparquet path), OR
510
+ // - it's a cloud-native file and hyparquet metadata failed (CORS, etc.)
511
+ if (!isFileSource || (cloudNative && !metaFromHyparquet)) {
512
+ // Fallback / SQL source: DuckDB metadata queries
455
513
  loadStage = t('table.loadingSchema');
456
514
  loadProgress = [
457
515
  ...loadProgress,
458
- { label: t('progress.source'), value: t('progress.duckdbFallback') }
516
+ {
517
+ label: t('progress.source'),
518
+ value: isFileSource ? t('progress.duckdbFallback') : resolved.label
519
+ }
459
520
  ];
460
521
 
461
522
  if (engine.getSchemaAndCrs) {
462
- const result = await engine.getSchemaAndCrs(connId, fileUrl, findGeoColumn);
523
+ const result = await engine.getSchemaAndCrs(connId, resolved, findGeoColumn);
463
524
  if (thisGen !== loadGeneration) return;
464
525
  schema = result.schema;
465
526
  columns = schema.map((f) => f.name);
@@ -486,7 +547,7 @@ async function loadTable() {
486
547
  }
487
548
  }
488
549
  } else {
489
- schema = await engine.getSchema(connId, fileUrl);
550
+ schema = await engine.getSchema(connId, resolved);
490
551
  if (thisGen !== loadGeneration) return;
491
552
  columns = schema.map((f) => f.name);
492
553
  const colPreview =
@@ -507,7 +568,7 @@ async function loadTable() {
507
568
  ...loadProgress,
508
569
  { label: t('progress.geometry'), value: `${detectedGeoCol} (${geoColType})` }
509
570
  ];
510
- sourceCrs = await engine.detectCrs(connId, fileUrl, detectedGeoCol);
571
+ sourceCrs = await engine.detectCrs(connId, resolved, detectedGeoCol);
511
572
  if (thisGen !== loadGeneration) return;
512
573
  if (sourceCrs) {
513
574
  loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
@@ -530,13 +591,34 @@ async function loadTable() {
530
591
 
531
592
  loadStage = t('table.runningQuery');
532
593
  const start = performance.now();
533
- const result = await executeQuery(sqlQuery);
594
+ let result = await executeQuery(sqlQuery);
534
595
  if (thisGen !== loadGeneration) return;
596
+
597
+ // If query failed and this is a GeoParquet file, the error may be from
598
+ // auto-conversion of CRS metadata (e.g. "stoi: no conversion").
599
+ // Retry with enable_geoparquet_conversion=false and BLOB handling.
600
+ if (!result && error && isParquet && geoCol && !isLegacyGeoParquet) {
601
+ try {
602
+ await engine.query(connId, 'SET enable_geoparquet_conversion = false');
603
+ geoColType = 'BLOB';
604
+ sqlQuery = buildDefaultSql(0);
605
+ customSql = sqlQuery;
606
+ error = null;
607
+ result = await executeQuery(sqlQuery);
608
+ if (thisGen !== loadGeneration) return;
609
+ } catch {
610
+ // Retry also failed — original error stands
611
+ }
612
+ }
613
+
535
614
  executionTimeMs = Math.round(performance.now() - start);
536
615
 
537
- if (!cloudNative && result) {
538
- // Non-cloud-native (CSV, GeoJSON, etc.): derive schema from the
539
- // query result so we avoid a second full-file download.
616
+ if (isFileSource && !cloudNative && result) {
617
+ // Non-cloud-native file (CSV, GeoJSON, etc.): derive schema from the
618
+ // query result so we avoid a second full-file download. Skipped for
619
+ // SQL-backed sources (attached DuckLake tables), which already have
620
+ // a proper schema from getSchemaAndCrs — re-deriving here would pick
621
+ // up the projected `__wkb` alias and clobber the real geo column.
540
622
  schema = (result.columns ?? []).map((col, i) => ({
541
623
  name: col,
542
624
  type: result.types?.[i] ?? 'VARCHAR',
@@ -591,7 +673,7 @@ async function loadTable() {
591
673
  } else {
592
674
  loadStage = t('table.countingRows');
593
675
  engine
594
- .getRowCount(connId, fileUrl)
676
+ .getRowCount(connId, resolved)
595
677
  .then((count) => {
596
678
  if (thisGen === loadGeneration) {
597
679
  totalRows = count;
@@ -415,6 +415,35 @@ async function addZarrLayer(map: maplibregl.Map) {
415
415
  opts.spatialDimensions = spatial;
416
416
  }
417
417
 
418
+ // Safety: warn if the array is extremely large without multiscale support.
419
+ // A global-extent array at full resolution can trigger thousands of chunk
420
+ // requests simultaneously, hanging the browser.
421
+ const meta = selectedMeta;
422
+ if (meta?.shape) {
423
+ const dims = meta.dims?.length ? meta.dims : inferDims(meta.name, meta.shape);
424
+ const yIdx = dims.findIndex((d) => ['y', 'lat', 'latitude'].includes(d.toLowerCase()));
425
+ const xIdx = dims.findIndex((d) => ['x', 'lon', 'longitude'].includes(d.toLowerCase()));
426
+ if (yIdx >= 0 && xIdx >= 0) {
427
+ const ySize = meta.shape[yIdx];
428
+ const xSize = meta.shape[xIdx];
429
+ const yChunk = meta.chunks?.[yIdx] ?? ySize;
430
+ const xChunk = meta.chunks?.[xIdx] ?? xSize;
431
+ const yTiles = Math.ceil(ySize / yChunk);
432
+ const xTiles = Math.ceil(xSize / xChunk);
433
+ const totalTiles = yTiles * xTiles;
434
+ // If more than 10 000 tiles at base resolution and no multiscale,
435
+ // the layer will flood the browser with requests at global zoom.
436
+ if (totalTiles > 10_000) {
437
+ error = t('map.zarrTooLarge', {
438
+ tiles: totalTiles.toLocaleString(),
439
+ shape: `${ySize.toLocaleString()} × ${xSize.toLocaleString()}`
440
+ });
441
+ loading = false;
442
+ return;
443
+ }
444
+ }
445
+ }
446
+
418
447
  zarrLayer = new ZarrLayer(opts);
419
448
  map.addLayer(zarrLayer);
420
449
  } catch (err) {
@@ -49,7 +49,6 @@ let showingStoreAttrs = $state(false);
49
49
  */
50
50
  const mapArrays = $derived.by(() => {
51
51
  if (!hierarchy) return [];
52
- // Collect root-level array names (coordinate arrays that zarr-layer can find)
53
52
  const rootArrayNames = new Set(
54
53
  hierarchy.root.children.filter((c) => c.kind === 'array').map((c) => c.name)
55
54
  );
@@ -58,7 +57,6 @@ const mapArrays = $derived.by(() => {
58
57
  function walk(n: ZarrNode) {
59
58
  if (n.kind === 'array' && n.shape && n.shape.length >= 2) {
60
59
  const dims = n.dims?.length ? n.dims : inferDims(n.name, n.shape);
61
- // Must have at least one spatial dim pair
62
60
  let hasLat = false;
63
61
  let hasLon = false;
64
62
  for (const d of dims) {
@@ -67,7 +65,6 @@ const mapArrays = $derived.by(() => {
67
65
  if (lower === 'x' || lower === 'lon' || lower === 'longitude') hasLon = true;
68
66
  }
69
67
  if (hasLat && hasLon) {
70
- // Non-spatial dims must have root-level coordinate arrays
71
68
  const nonSpatialResolvable = dims.every(
72
69
  (d) => spatialNames.has(d.toLowerCase()) || rootArrayNames.has(d)
73
70
  );
@@ -454,7 +451,7 @@ function selectStoreAttrs() {
454
451
  >
455
452
  {t('zarr.map')}
456
453
  </Button>
457
- {/if}
454
+ {/if}
458
455
  </div>
459
456
  </div>
460
457
  </div>
@@ -9,8 +9,12 @@ export declare const STORAGE_KEYS: {
9
9
  };
10
10
  /** EPSG codes considered WGS84 (no reprojection needed). */
11
11
  export declare const WGS84_CODES: Set<number>;
12
- /** Default target CRS for ST_Transform. */
13
- export declare const DEFAULT_TARGET_CRS = "EPSG:4326";
12
+ /**
13
+ * Default target CRS for ST_Transform. Uses OGC:CRS84 (longitude, latitude)
14
+ * to match GeoParquet 1.1+ spec and DuckDB v1.5's canonical form.
15
+ * Functionally equivalent to EPSG:4326 under `geometry_always_xy = true`.
16
+ */
17
+ export declare const DEFAULT_TARGET_CRS = "OGC:CRS84";
14
18
  /** DuckDB-WASM initialization timeout in ms. */
15
19
  export declare const DUCKDB_INIT_TIMEOUT_MS = 30000;
16
20
  /** Maximum entries kept in query history. */
package/dist/constants.js CHANGED
@@ -11,8 +11,12 @@ export const STORAGE_KEYS = {
11
11
  // ── Geo / CRS constants ──
12
12
  /** EPSG codes considered WGS84 (no reprojection needed). */
13
13
  export const WGS84_CODES = new Set([4326, 4979]);
14
- /** Default target CRS for ST_Transform. */
15
- export const DEFAULT_TARGET_CRS = 'EPSG:4326';
14
+ /**
15
+ * Default target CRS for ST_Transform. Uses OGC:CRS84 (longitude, latitude)
16
+ * to match GeoParquet 1.1+ spec and DuckDB v1.5's canonical form.
17
+ * Functionally equivalent to EPSG:4326 under `geometry_always_xy = true`.
18
+ */
19
+ export const DEFAULT_TARGET_CRS = 'OGC:CRS84';
16
20
  // ── Query engine constants ──
17
21
  /** DuckDB-WASM initialization timeout in ms. */
18
22
  export const DUCKDB_INIT_TIMEOUT_MS = 30_000;
@@ -53,4 +53,4 @@ export declare function isQueryable(extension: string): boolean;
53
53
  /** Returns the MIME type for a file extension. */
54
54
  export declare function getMimeType(extension: string): string;
55
55
  /** Re-export folder info for components that need it directly. */
56
- export { FOLDER_INFO, DEFAULT_INFO };
56
+ export { DEFAULT_INFO, FOLDER_INFO };
@@ -883,6 +883,16 @@ const EXTENSIONS = {
883
883
  duckdbReadFn: null,
884
884
  mimeType: 'application/octet-stream'
885
885
  },
886
+ '.ducklake': {
887
+ icon: 'Database',
888
+ color: 'text-teal-600 dark:text-teal-400',
889
+ label: 'DuckLake',
890
+ category: 'database',
891
+ viewer: 'database',
892
+ queryable: true,
893
+ duckdbReadFn: null,
894
+ mimeType: 'application/octet-stream'
895
+ },
886
896
  '.sqlite': {
887
897
  icon: 'Database',
888
898
  color: 'text-sky-600 dark:text-sky-400',
@@ -1055,7 +1065,7 @@ export function buildDuckDbSource(pathOrExt, url) {
1055
1065
  * Other cloud-native formats (.fgb, .pmtiles, .zarr) also support range
1056
1066
  * requests but have dedicated viewers and don't go through DuckDB/TableViewer.
1057
1067
  */
1058
- const CLOUD_NATIVE_EXTS = new Set(['.parquet', '.geoparquet', '.gpq', '.gparquet']);
1068
+ const CLOUD_NATIVE_EXTS = new Set(['.parquet', '.geoparquet', '.gpq', '.gparquet', '.ducklake']);
1059
1069
  export function isCloudNativeFormat(pathOrExt) {
1060
1070
  const ext = pathOrExt.includes('.') ? `.${pathOrExt.split('.').pop().toLowerCase()}` : '';
1061
1071
  return CLOUD_NATIVE_EXTS.has(ext);
@@ -1073,4 +1083,4 @@ export function getMimeType(extension) {
1073
1083
  return getFileTypeInfo(extension).mimeType;
1074
1084
  }
1075
1085
  /** Re-export folder info for components that need it directly. */
1076
- export { FOLDER_INFO, DEFAULT_INFO };
1086
+ export { DEFAULT_INFO, FOLDER_INFO };
package/dist/i18n/ar.js CHANGED
@@ -30,6 +30,14 @@ export const ar = {
30
30
  'connection.accessKey': 'مفتاح الوصول',
31
31
  'connection.secretKey': 'المفتاح السري',
32
32
  'connection.credentialNotice': 'يتم تخزين بيانات اعتمادك فقط في مدير كلمات المرور بمتصفحك إذا اخترت حفظها. لا تُرسل أبداً إلى أي خادم خارجي ولا تُخزّن في التخزين المحلي.',
33
+ 'connection.corsTitle': 'إعداد CORS',
34
+ 'connection.corsRequired': 'يتطلب الوصول عبر المتصفح تفعيل CORS على الحاوية. بدونه، سيتم حظر الطلبات.',
35
+ 'connection.corsDefault': 'CORS مفعّل افتراضياً. لا حاجة لأي إعداد.',
36
+ 'connection.corsDocs': 'توثيق CORS الرسمي',
37
+ 'connection.corsCliTitle': 'تفعيل CORS عبر سطر الأوامر',
38
+ 'connection.readOnlyTitle': 'الوصول للقراءة فقط',
39
+ 'connection.readOnlyDocs': 'توثيق الصلاحيات الرسمي',
40
+ 'connection.readOnlyCliTitle': 'تقييد عبر سطر الأوامر',
33
41
  'connection.testSuccess': 'الاتصال ناجح',
34
42
  'connection.testFail': 'فشل الاتصال. تحقق من الإعدادات وحاول مرة أخرى.',
35
43
  'connection.testButton': 'اختبار الاتصال',
@@ -194,6 +202,12 @@ export const ar = {
194
202
  'database.tablesHeader': 'الجداول',
195
203
  'database.loadingTable': 'جارٍ تحميل الجدول...',
196
204
  'database.selectTable': 'اختر جدولاً للتصفح',
205
+ // DuckLake
206
+ 'ducklake.loading': 'جا��ٍ ربط كتالوج DuckLake...',
207
+ 'ducklake.snapshot': 'لقطة',
208
+ 'ducklake.snapshots': 'لقطات',
209
+ 'ducklake.noTables': 'لا توجد جداول في هذا المخطط',
210
+ 'ducklake.extensionHint': 'قد لا يكون امتداد DuckLake متاحاً لهذا الإصدار من DuckDB-WASM.',
197
211
  // PDF Viewer
198
212
  'pdf.badge': 'PDF',
199
213
  'pdf.loading': 'جارٍ تحميل PDF...',
@@ -317,6 +331,7 @@ export const ar = {
317
331
  'map.loadingFgb': 'جارٍ تحميل FlatGeobuf...',
318
332
  'map.loadingCog': 'جارٍ تحميل COG...',
319
333
  'map.loadingZarr': 'جارٍ تحميل بيانات Zarr...',
334
+ 'map.zarrTooLarge': 'المصفوفة كبيرة جداً لعرض الخريطة ({shape}، ~{tiles} بلاطة). يحتاج هذا المجموعة إلى هرم متعدد المقاييس للعرض المبلط.',
320
335
  'map.features': 'معالم',
321
336
  'map.limit': '(الحد)',
322
337
  'map.of': 'من',
@@ -366,6 +381,15 @@ export const ar = {
366
381
  'mapInfo.columns': 'الأعمدة',
367
382
  'mapInfo.size': 'الحجم',
368
383
  'mapInfo.bands': 'النطاقات',
384
+ // COG Controls
385
+ 'cog.style': 'النمط',
386
+ 'cog.band': 'النطاق',
387
+ 'cog.singleBand': 'نطاق واحد',
388
+ 'cog.colorRamp': 'تدرج الألوان',
389
+ 'cog.pixelValue': 'قيمة البكسل',
390
+ 'cog.reading': 'قراءة البكسل...',
391
+ 'cog.rescale': 'إعادة القياس',
392
+ 'cog.rescaleReset': 'إعادة تعيين',
369
393
  // PMTiles Viewer
370
394
  'pmtiles.mapView': 'خريطة',
371
395
  'pmtiles.archiveView': 'الأرشيف',
package/dist/i18n/en.js CHANGED
@@ -30,6 +30,14 @@ export const en = {
30
30
  'connection.accessKey': 'Access Key',
31
31
  'connection.secretKey': 'Secret Key',
32
32
  'connection.credentialNotice': "Your credentials are stored only in your browser's password manager if you choose to save them. They are never sent to any external server or stored in local storage.",
33
+ 'connection.corsTitle': 'CORS Configuration',
34
+ 'connection.corsRequired': 'Browser access requires CORS to be enabled on your bucket. Without it, requests will be blocked.',
35
+ 'connection.corsDefault': 'CORS is enabled by default. No configuration needed.',
36
+ 'connection.corsDocs': 'Official CORS docs',
37
+ 'connection.corsCliTitle': 'Enable CORS via CLI',
38
+ 'connection.readOnlyTitle': 'Read-Only Access',
39
+ 'connection.readOnlyDocs': 'Official permissions docs',
40
+ 'connection.readOnlyCliTitle': 'Restrict via CLI',
33
41
  'connection.testSuccess': 'Connection successful',
34
42
  'connection.testFail': 'Connection failed. Check your settings and try again.',
35
43
  'connection.testButton': 'Test Connection',
@@ -194,6 +202,12 @@ export const en = {
194
202
  'database.tablesHeader': 'Tables',
195
203
  'database.loadingTable': 'Loading table...',
196
204
  'database.selectTable': 'Select a table to browse',
205
+ // DuckLake
206
+ 'ducklake.loading': 'Attaching DuckLake catalog...',
207
+ 'ducklake.snapshot': 'Snapshot',
208
+ 'ducklake.snapshots': 'snapshots',
209
+ 'ducklake.noTables': 'No tables in this schema',
210
+ 'ducklake.extensionHint': 'The DuckLake extension may not be available for this DuckDB-WASM version.',
197
211
  // PDF Viewer
198
212
  'pdf.badge': 'PDF',
199
213
  'pdf.loading': 'Loading PDF...',
@@ -317,6 +331,7 @@ export const en = {
317
331
  'map.loadingFgb': 'Loading FlatGeobuf...',
318
332
  'map.loadingCog': 'Loading COG...',
319
333
  'map.loadingZarr': 'Loading Zarr data...',
334
+ 'map.zarrTooLarge': 'Array too large for map view ({shape}, ~{tiles} tiles). This dataset needs a multiscale pyramid for tiled rendering.',
320
335
  'map.features': 'features',
321
336
  'map.limit': '(limit)',
322
337
  'map.of': 'of',
@@ -366,6 +381,15 @@ export const en = {
366
381
  'mapInfo.columns': 'Columns',
367
382
  'mapInfo.size': 'Size',
368
383
  'mapInfo.bands': 'Bands',
384
+ // COG Controls
385
+ 'cog.style': 'Style',
386
+ 'cog.band': 'Band',
387
+ 'cog.singleBand': 'Single',
388
+ 'cog.colorRamp': 'Color ramp',
389
+ 'cog.pixelValue': 'Pixel Value',
390
+ 'cog.reading': 'Reading pixel...',
391
+ 'cog.rescale': 'Rescale',
392
+ 'cog.rescaleReset': 'Reset',
369
393
  // PMTiles Viewer
370
394
  'pmtiles.mapView': 'Map',
371
395
  'pmtiles.archiveView': 'Archive',
@@ -1,5 +1,4 @@
1
1
  export type Locale = 'en' | 'ar';
2
- export declare function getLocale(): Locale;
3
2
  export declare function setLocale(l: Locale): void;
4
3
  export declare function getDir(): 'ltr' | 'rtl';
5
4
  /**
@@ -3,9 +3,6 @@ import { en } from './en.js';
3
3
  const translations = { en, ar };
4
4
  const RTL_LOCALES = new Set(['ar']);
5
5
  let currentLocale = $state('en');
6
- export function getLocale() {
7
- return currentLocale;
8
- }
9
6
  export function setLocale(l) {
10
7
  currentLocale = l;
11
8
  }
package/dist/index.d.ts CHANGED
@@ -4,17 +4,28 @@ export { buildDuckDbSource, getDuckDbReadFn, getFileTypeInfo, getMimeType, getVi
4
4
  export type { MapQueryHandle, MapQueryResult, QueryEngine, QueryHandle, QueryResult, SchemaField } from './query/engine.js';
5
5
  export { QueryCancelledError } from './query/engine.js';
6
6
  export type { ListPage, StorageAdapter } from './storage/adapter.js';
7
+ export type { ProviderDef, ProviderId, ProviderRegion } from './storage/providers.js';
8
+ export { buildEndpointFromTemplate, buildProviderBaseUrl, getProvider, isGcsProvider, PROVIDER_IDS, PROVIDERS } from './storage/providers.js';
7
9
  export { UrlAdapter } from './storage/url-adapter.js';
8
10
  export type { Connection, ConnectionConfig, FileEntry, Tab, Theme, WriteResult } from './types.js';
9
11
  export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
12
+ export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
13
+ export type { CogInfo, GeoBounds } from './utils/cog.js';
14
+ export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './utils/cog.js';
10
15
  export type { TypeCategory } from './utils/column-types.js';
11
16
  export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
12
17
  export { handleLoadError } from './utils/error.js';
18
+ export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
19
+ export type { SortConfig, SortDirection, SortField } from './utils/file-sort.js';
20
+ export { sortFileEntries, toggleSortField } from './utils/file-sort.js';
13
21
  export { formatDate, formatFileSize, formatValue, getFileExtension, jsonReplacerBigInt } from './utils/format.js';
14
22
  export type { GeoArrowGeomType, GeoArrowResult } from './utils/geoarrow.js';
15
23
  export { buildGeoArrowTables, normalizeGeomType } from './utils/geoarrow.js';
16
24
  export type { HexRow } from './utils/hex.js';
17
25
  export { generateHexDump } from './utils/hex.js';
26
+ export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
27
+ export type { ParsedMarkdownDocument, SqlBlock } from './utils/markdown-sql.js';
28
+ export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
18
29
  export type { GeoColumnMeta, GeoParquetMeta, ParquetFileMetadata } from './utils/parquet-metadata.js';
19
30
  export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
20
31
  export type { Defaults, ParsedStorageUrl, StorageProvider } from './utils/storage-url.js';
package/dist/index.js CHANGED
@@ -4,15 +4,25 @@ export { COPY_FEEDBACK_MS, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, LAYER_HUE
4
4
  // File icons registry
5
5
  export { buildDuckDbSource, getDuckDbReadFn, getFileTypeInfo, getMimeType, getViewerKind, isCloudNativeFormat, isQueryable } from './file-icons/index.js';
6
6
  export { QueryCancelledError } from './query/engine.js';
7
+ export { buildEndpointFromTemplate, buildProviderBaseUrl, getProvider, isGcsProvider, PROVIDER_IDS, PROVIDERS } from './storage/providers.js';
7
8
  export { UrlAdapter } from './storage/url-adapter.js';
8
9
  // Clipboard
9
10
  export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
11
+ // Cloud URL resolution
12
+ export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
13
+ export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './utils/cog.js';
10
14
  export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
11
15
  // Error handling
12
16
  export { handleLoadError } from './utils/error.js';
17
+ // Data export / serialization
18
+ export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
19
+ export { sortFileEntries, toggleSortField } from './utils/file-sort.js';
13
20
  export { formatDate, formatFileSize, formatValue, getFileExtension, jsonReplacerBigInt } from './utils/format.js';
14
21
  export { buildGeoArrowTables, normalizeGeomType } from './utils/geoarrow.js';
15
22
  export { generateHexDump } from './utils/hex.js';
23
+ // localStorage helpers
24
+ export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
25
+ export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
16
26
  export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
17
27
  export { describeParseResult, looksLikeUrl, parseStorageUrl } from './utils/storage-url.js';
18
28
  // Utilities