@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.
- package/README.md +11 -2
- package/dist/components/browser/FileBrowser.svelte +41 -54
- package/dist/components/browser/FileTreeSidebar.svelte +43 -7
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +43 -25
- package/dist/components/viewers/CodeViewer.svelte +23 -0
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +353 -1160
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/TableViewer.svelte +123 -41
- package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
- package/dist/components/viewers/ZarrViewer.svelte +1 -4
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +6 -2
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +12 -2
- package/dist/i18n/ar.js +24 -0
- package/dist/i18n/en.js +24 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/query/engine.d.ts +20 -4
- package/dist/query/index.d.ts +2 -1
- package/dist/query/index.js +1 -0
- package/dist/query/source.d.ts +30 -0
- package/dist/query/source.js +37 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +138 -85
- package/dist/storage/providers.d.ts +47 -0
- package/dist/storage/providers.js +160 -0
- package/dist/stores/connections.svelte.js +5 -31
- package/dist/stores/files.svelte.d.ts +2 -8
- package/dist/stores/files.svelte.js +5 -38
- package/dist/stores/query-history.svelte.js +3 -25
- package/dist/stores/settings.svelte.d.ts +1 -0
- package/dist/stores/settings.svelte.js +10 -30
- package/dist/stores/tabs.svelte.d.ts +9 -2
- package/dist/stores/tabs.svelte.js +11 -2
- package/dist/types.d.ts +11 -0
- package/dist/utils/cloud-url.d.ts +27 -0
- package/dist/utils/cloud-url.js +61 -0
- package/dist/utils/cog.d.ts +244 -0
- package/dist/utils/cog.js +1039 -0
- package/dist/utils/deck.d.ts +0 -18
- package/dist/utils/deck.js +0 -36
- package/dist/utils/export.d.ts +22 -2
- package/dist/utils/export.js +35 -10
- package/dist/utils/file-sort.d.ts +20 -0
- package/dist/utils/file-sort.js +41 -0
- package/dist/utils/geometry-type.d.ts +52 -0
- package/dist/utils/geometry-type.js +76 -0
- package/dist/utils/local-storage.d.ts +16 -0
- package/dist/utils/local-storage.js +37 -0
- package/dist/utils/markdown-sql.d.ts +1 -1
- package/dist/utils/markdown-sql.js +3 -4
- package/dist/utils/pmtiles-tile.d.ts +0 -2
- package/dist/utils/pmtiles-tile.js +0 -8
- package/dist/utils/url-state.d.ts +6 -0
- package/dist/utils/url-state.js +34 -26
- package/dist/utils/url.d.ts +13 -25
- package/dist/utils/url.js +17 -78
- package/dist/utils/zarr-tab.d.ts +22 -0
- package/dist/utils/zarr-tab.js +30 -0
- package/dist/utils/zarr.d.ts +0 -2
- package/dist/utils/zarr.js +73 -44
- package/package.json +50 -46
- package/dist/components/ui/tabs/index.d.ts +0 -5
- package/dist/components/ui/tabs/index.js +0 -7
- package/dist/components/ui/tabs/tabs-content.svelte +0 -17
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-list.svelte +0 -16
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs.svelte +0 -19
- package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
- package/dist/components/viewers/MapViewer.svelte +0 -234
- package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
- package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
- 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 {
|
|
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 {
|
|
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
|
|
95
|
-
const source =
|
|
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
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
upper
|
|
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');
|
|
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
|
-
|
|
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
|
-
?
|
|
131
|
+
? wrapWkbWithCrs(quoted, sourceCrs)
|
|
125
132
|
: `ST_GeomFromGeoJSON(${quoted})`;
|
|
126
133
|
if (sourceCrs) {
|
|
127
|
-
|
|
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
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
const
|
|
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
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
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
|
|
405
|
-
|
|
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,
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
{
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
454
|
+
{/if}
|
|
458
455
|
</div>
|
|
459
456
|
</div>
|
|
460
457
|
</div>
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
13
|
-
|
|
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
|
-
/**
|
|
15
|
-
|
|
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 {
|
|
56
|
+
export { DEFAULT_INFO, FOLDER_INFO };
|
package/dist/file-icons/index.js
CHANGED
|
@@ -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 {
|
|
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',
|
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
|