@walkthru-earth/objex 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/components/browser/FileBrowser.svelte +25 -14
- package/dist/components/browser/FileTreeSidebar.svelte +43 -7
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +70 -25
- package/dist/components/viewers/ArchiveViewer.svelte +4 -4
- package/dist/components/viewers/CodeViewer.svelte +44 -5
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +373 -1162
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/CopcViewer.svelte +20 -2
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/PmtilesViewer.svelte +2 -2
- package/dist/components/viewers/StacMapViewer.svelte +25 -9
- package/dist/components/viewers/TableViewer.svelte +162 -51
- package/dist/components/viewers/ZarrMapViewer.svelte +33 -4
- package/dist/components/viewers/ZarrViewer.svelte +3 -6
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
- 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 +25 -0
- package/dist/i18n/en.js +25 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -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 +42 -0
- package/dist/query/source.js +54 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +267 -107
- package/dist/storage/adapter.d.ts +9 -0
- package/dist/storage/adapter.js +13 -1
- package/dist/storage/browser-azure.d.ts +1 -1
- package/dist/storage/browser-azure.js +4 -0
- package/dist/storage/browser-cloud.d.ts +1 -1
- package/dist/storage/browser-cloud.js +7 -0
- package/dist/storage/presign.d.ts +13 -0
- package/dist/storage/presign.js +55 -0
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +171 -0
- package/dist/stores/browser.svelte.d.ts +2 -0
- package/dist/stores/browser.svelte.js +17 -1
- package/dist/stores/files.svelte.d.ts +1 -2
- package/dist/stores/files.svelte.js +1 -2
- 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/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/geometry-type.d.ts +52 -0
- package/dist/utils/geometry-type.js +76 -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 +26 -9
- package/dist/utils/url.js +52 -25
- package/dist/utils/wkb.js +22 -8
- 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 +47 -43
- 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,24 @@
|
|
|
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
|
+
resolveTableSourceAsync
|
|
16
|
+
} from '../../query/index.js';
|
|
9
17
|
import { queryHistory } from '../../stores/query-history.svelte.js';
|
|
10
18
|
import { settings } from '../../stores/settings.svelte.js';
|
|
11
19
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
12
20
|
import type { Tab } from '../../types';
|
|
13
21
|
import type { GeoArrowGeomType } from '../../utils/geoarrow.js';
|
|
22
|
+
import { buildTransformExpr, wrapWkbWithCrs } from '../../utils/geometry-type.js';
|
|
14
23
|
import {
|
|
15
24
|
extractBounds,
|
|
16
25
|
extractEpsgFromGeoMeta,
|
|
@@ -53,6 +62,8 @@ let viewMode = $state<'table' | 'map' | 'stac' | 'info'>(
|
|
|
53
62
|
);
|
|
54
63
|
let sqlQuery = $state('');
|
|
55
64
|
let customSql = $state('');
|
|
65
|
+
// Presigned URL for the source-cooperative parquet-table iframe (external fetcher).
|
|
66
|
+
let parquetIframeUrl = $state('');
|
|
56
67
|
let queryRunning = $state(false);
|
|
57
68
|
let executionTimeMs = $state(0);
|
|
58
69
|
|
|
@@ -90,27 +101,28 @@ const columnTypes = $derived(Object.fromEntries(schema.map((f) => [f.name, f.typ
|
|
|
90
101
|
// Columns for display — exclude internal __wkb helper
|
|
91
102
|
const displayColumns = $derived(columns.filter((c) => c !== '__wkb'));
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
let resolvedSource: ResolvedTableSource | null = null;
|
|
105
|
+
|
|
106
|
+
function buildDefaultSql(
|
|
107
|
+
offset = 0,
|
|
108
|
+
resolved: ResolvedTableSource = resolvedSource ?? resolveTableSource(tab)
|
|
109
|
+
): string {
|
|
110
|
+
const source = resolved.ref;
|
|
96
111
|
|
|
97
112
|
let sql: string;
|
|
98
113
|
if (geoCol) {
|
|
99
114
|
const quoted = `"${geoCol}"`;
|
|
100
115
|
const upper = geoColType.toUpperCase();
|
|
101
|
-
// Spatial types that ST_AsWKB accepts directly
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
upper
|
|
106
|
-
upper === 'GEOGRAPHY' ||
|
|
116
|
+
// Spatial types that ST_AsWKB accepts directly: GEOMETRY, GEOMETRY('EPSG:...'),
|
|
117
|
+
// GEOGRAPHY, WKB_BLOB, POINT, LINESTRING, POLYGON, Binary (Arrow serialization).
|
|
118
|
+
const spatialType =
|
|
119
|
+
upper.startsWith('GEOMETRY') ||
|
|
120
|
+
upper.startsWith('GEOGRAPHY') ||
|
|
107
121
|
upper === 'WKB_BLOB' ||
|
|
108
122
|
upper.includes('POINT') ||
|
|
109
123
|
upper.includes('LINESTRING') ||
|
|
110
124
|
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.
|
|
125
|
+
upper.includes('BINARY');
|
|
114
126
|
const isWkbBlob = upper === 'BLOB' || upper === 'BYTEA';
|
|
115
127
|
|
|
116
128
|
let wkbExpr: string;
|
|
@@ -118,13 +130,17 @@ function buildDefaultSql(offset = 0): string {
|
|
|
118
130
|
// Already WKB — use directly, no conversion needed
|
|
119
131
|
wkbExpr = `${quoted} AS __wkb`;
|
|
120
132
|
} else {
|
|
121
|
-
|
|
133
|
+
// For BLOB inputs with a known source CRS, attach it via ST_SetCRS so the
|
|
134
|
+
// 2-arg ST_Transform form can be used (DuckDB v1.5+).
|
|
135
|
+
let geomExpr = spatialType
|
|
122
136
|
? quoted
|
|
123
137
|
: isWkbBlob
|
|
124
|
-
?
|
|
138
|
+
? wrapWkbWithCrs(quoted, sourceCrs)
|
|
125
139
|
: `ST_GeomFromGeoJSON(${quoted})`;
|
|
126
140
|
if (sourceCrs) {
|
|
127
|
-
|
|
141
|
+
// geometry_always_xy is set globally at DB init, so no per-call always_xy.
|
|
142
|
+
const effectiveType = isWkbBlob ? `GEOMETRY('${sourceCrs}')` : geoColType;
|
|
143
|
+
geomExpr = buildTransformExpr(geomExpr, effectiveType, sourceCrs, DEFAULT_TARGET_CRS);
|
|
128
144
|
}
|
|
129
145
|
wkbExpr = `ST_AsWKB(${geomExpr}) AS __wkb`;
|
|
130
146
|
}
|
|
@@ -195,6 +211,8 @@ $effect(() => {
|
|
|
195
211
|
geoCol = null;
|
|
196
212
|
knownGeomType = undefined;
|
|
197
213
|
metadataBounds = null;
|
|
214
|
+
resolvedSource = null;
|
|
215
|
+
parquetIframeUrl = '';
|
|
198
216
|
error = null;
|
|
199
217
|
});
|
|
200
218
|
return unregister;
|
|
@@ -259,6 +277,10 @@ async function forceCancel() {
|
|
|
259
277
|
|
|
260
278
|
async function loadTable() {
|
|
261
279
|
const thisGen = ++loadGeneration;
|
|
280
|
+
// Snapshot reactive values once. Reading `$derived` across awaits after the
|
|
281
|
+
// component's effect is torn down returns Svelte's destroyed-signal sentinel,
|
|
282
|
+
// which throws "can't convert symbol to string" in downstream template literals.
|
|
283
|
+
const cid = connId;
|
|
262
284
|
|
|
263
285
|
// Cancel in-flight query from a previous load to prevent duplicate concurrent queries
|
|
264
286
|
if (activeHandle) {
|
|
@@ -278,25 +300,41 @@ async function loadTable() {
|
|
|
278
300
|
loadStage = t('table.preparingQuery');
|
|
279
301
|
loadProgress = [];
|
|
280
302
|
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
sqlQuery =
|
|
284
|
-
customSql =
|
|
303
|
+
resolvedSource = null;
|
|
304
|
+
const eagerSql = buildDefaultSql(0);
|
|
305
|
+
sqlQuery = eagerSql;
|
|
306
|
+
customSql = eagerSql;
|
|
285
307
|
|
|
286
308
|
try {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
309
|
+
const resolved: ResolvedTableSource = await resolveTableSourceAsync(tab);
|
|
310
|
+
if (thisGen !== loadGeneration) return;
|
|
311
|
+
resolvedSource = resolved;
|
|
312
|
+
const resolvedSql = buildDefaultSql(0, resolved);
|
|
313
|
+
sqlQuery = resolvedSql;
|
|
314
|
+
// Only overwrite the editor if the user hasn't edited it during the presign await.
|
|
315
|
+
if (customSql === eagerSql) customSql = resolvedSql;
|
|
316
|
+
const isFileSource = resolved.isFileSource;
|
|
317
|
+
const fileUrl = resolved.fileUrl ?? '';
|
|
318
|
+
const httpsUrl = isFileSource ? buildHttpsUrl(tab) : '';
|
|
319
|
+
const cloudNative = isFileSource && isCloudNativeFormat(tab.path);
|
|
320
|
+
const isParquet = isFileSource && /\.parquet$/i.test(tab.path);
|
|
321
|
+
const streamable = isFileSource && canStreamDirectly(tab);
|
|
322
|
+
|
|
323
|
+
// Parquet-table iframe fetches from its own origin. `resolved.fileUrl`
|
|
324
|
+
// is already the presigned HTTPS URL for signed-s3 (or the public URL
|
|
325
|
+
// for anonymous / SAS), so reuse it instead of signing a second time.
|
|
326
|
+
if (isParquet) parquetIframeUrl = fileUrl;
|
|
292
327
|
|
|
293
328
|
// Start DuckDB boot immediately (runs in parallel with hyparquet)
|
|
294
329
|
loadStage = t('table.initEngine');
|
|
295
330
|
const enginePromise = getQueryEngine();
|
|
296
331
|
|
|
297
332
|
// ── Fast metadata via hyparquet (runs concurrently with DuckDB boot) ──
|
|
333
|
+
// Only applies to file-backed sources. SQL-backed sources (attached
|
|
334
|
+
// DuckLake tables, etc.) go straight to DuckDB for schema + CRS.
|
|
298
335
|
let metaFromHyparquet = false;
|
|
299
336
|
let needsDuckDbCrs = false;
|
|
337
|
+
let isLegacyGeoParquet = false;
|
|
300
338
|
if (cloudNative && isParquet && streamable) {
|
|
301
339
|
try {
|
|
302
340
|
loadStage = t('table.readingMetadata');
|
|
@@ -357,11 +395,9 @@ async function loadTable() {
|
|
|
357
395
|
|
|
358
396
|
if (meta.geo) {
|
|
359
397
|
geoCol = meta.geo.primaryColumn;
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
// DuckDB v1.5 feature not yet in WASM (v1.33).
|
|
364
|
-
geoColType = 'BLOB';
|
|
398
|
+
// DuckDB v1.5 reads GeoParquet columns as GEOMETRY('EPSG:...').
|
|
399
|
+
// Set GEOMETRY here as placeholder — DuckDB schema will refine.
|
|
400
|
+
geoColType = 'GEOMETRY';
|
|
365
401
|
sourceCrs = extractEpsgFromGeoMeta(meta.geo);
|
|
366
402
|
const geomTypes = extractGeometryTypes(meta.geo);
|
|
367
403
|
if (geomTypes.length === 1) knownGeomType = geomTypes[0];
|
|
@@ -401,9 +437,8 @@ async function loadTable() {
|
|
|
401
437
|
const detectedGeoCol = findGeoColumn(schema);
|
|
402
438
|
if (detectedGeoCol) {
|
|
403
439
|
geoCol = detectedGeoCol;
|
|
404
|
-
// DuckDB
|
|
405
|
-
|
|
406
|
-
geoColType = 'BLOB';
|
|
440
|
+
// DuckDB v1.5 reads native Parquet GEOMETRY natively.
|
|
441
|
+
geoColType = 'GEOMETRY';
|
|
407
442
|
needsDuckDbCrs = true;
|
|
408
443
|
loadProgress = [
|
|
409
444
|
...loadProgress,
|
|
@@ -420,6 +455,7 @@ async function loadTable() {
|
|
|
420
455
|
{ label: t('progress.format'), value: t('progress.stacDetected') }
|
|
421
456
|
];
|
|
422
457
|
}
|
|
458
|
+
isLegacyGeoParquet = meta.legacyGeoParquet;
|
|
423
459
|
metaFromHyparquet = true;
|
|
424
460
|
} catch {
|
|
425
461
|
// hyparquet failed (CORS, auth, format) — fall back to DuckDB
|
|
@@ -431,11 +467,54 @@ async function loadTable() {
|
|
|
431
467
|
const engine = await enginePromise;
|
|
432
468
|
if (thisGen !== loadGeneration) return;
|
|
433
469
|
|
|
470
|
+
// Legacy GeoParquet (geopandas <0.12) — files with schema_version but no
|
|
471
|
+
// "version" field. DuckDB v1.5 may reject these with auto-conversion enabled.
|
|
472
|
+
// Disable conversion for this connection and fall back to BLOB handling.
|
|
473
|
+
if (metaFromHyparquet && isLegacyGeoParquet && geoCol) {
|
|
474
|
+
try {
|
|
475
|
+
await engine.query(cid, 'SET enable_geoparquet_conversion = false');
|
|
476
|
+
geoColType = 'BLOB';
|
|
477
|
+
} catch {
|
|
478
|
+
// Setting failed — DuckDB may still handle it gracefully
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// DuckDB v1.5: refresh geoColType from DuckDB's actual schema.
|
|
483
|
+
// hyparquet reports BLOB for the physical Parquet type, but DuckDB v1.5
|
|
484
|
+
// reads GeoParquet as GEOMETRY('EPSG:...') with CRS embedded in the type.
|
|
485
|
+
if (metaFromHyparquet && geoCol && !isLegacyGeoParquet) {
|
|
486
|
+
try {
|
|
487
|
+
const duckSchema = await engine.getSchema(cid, resolved);
|
|
488
|
+
if (thisGen !== loadGeneration) return;
|
|
489
|
+
const duckGeoField = duckSchema.find((f: { name: string }) => f.name === geoCol);
|
|
490
|
+
if (duckGeoField) {
|
|
491
|
+
geoColType = duckGeoField.type;
|
|
492
|
+
// Extract CRS from type string — may override hyparquet CRS detection
|
|
493
|
+
const typeStr = duckGeoField.type.toUpperCase();
|
|
494
|
+
const crsMatch = duckGeoField.type.match(/^GEOMETRY\('([^']+)'\)/i);
|
|
495
|
+
if (crsMatch) {
|
|
496
|
+
const crsVal = crsMatch[1];
|
|
497
|
+
const isWgs84 =
|
|
498
|
+
crsVal === 'EPSG:4326' ||
|
|
499
|
+
crsVal === 'OGC:CRS84' ||
|
|
500
|
+
(crsVal.startsWith('EPSG:') && [4326, 4979].includes(Number(crsVal.split(':')[1])));
|
|
501
|
+
sourceCrs = isWgs84 ? null : crsVal;
|
|
502
|
+
needsDuckDbCrs = false;
|
|
503
|
+
} else if (typeStr.startsWith('GEOMETRY')) {
|
|
504
|
+
// GEOMETRY without CRS param — still need CRS from metadata
|
|
505
|
+
needsDuckDbCrs = !sourceCrs;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// Schema refresh failed — continue with hyparquet-detected type
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
434
513
|
// If hyparquet detected a geo column but couldn't determine CRS
|
|
435
514
|
// (native Parquet GEOMETRY without "geo" KV metadata), use DuckDB
|
|
436
515
|
if (metaFromHyparquet && needsDuckDbCrs && geoCol) {
|
|
437
516
|
try {
|
|
438
|
-
sourceCrs = await engine.detectCrs(
|
|
517
|
+
sourceCrs = await engine.detectCrs(cid, resolved, geoCol);
|
|
439
518
|
if (thisGen !== loadGeneration) return;
|
|
440
519
|
if (sourceCrs) {
|
|
441
520
|
loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
|
|
@@ -450,16 +529,22 @@ async function loadTable() {
|
|
|
450
529
|
}
|
|
451
530
|
}
|
|
452
531
|
|
|
453
|
-
|
|
454
|
-
|
|
532
|
+
// DuckDB-backed schema + CRS pre-load. Runs when:
|
|
533
|
+
// - the source is SQL-backed (attached table, no hyparquet path), OR
|
|
534
|
+
// - it's a cloud-native file and hyparquet metadata failed (CORS, etc.)
|
|
535
|
+
if (!isFileSource || (cloudNative && !metaFromHyparquet)) {
|
|
536
|
+
// Fallback / SQL source: DuckDB metadata queries
|
|
455
537
|
loadStage = t('table.loadingSchema');
|
|
456
538
|
loadProgress = [
|
|
457
539
|
...loadProgress,
|
|
458
|
-
{
|
|
540
|
+
{
|
|
541
|
+
label: t('progress.source'),
|
|
542
|
+
value: isFileSource ? t('progress.duckdbFallback') : resolved.label
|
|
543
|
+
}
|
|
459
544
|
];
|
|
460
545
|
|
|
461
546
|
if (engine.getSchemaAndCrs) {
|
|
462
|
-
const result = await engine.getSchemaAndCrs(
|
|
547
|
+
const result = await engine.getSchemaAndCrs(cid, resolved, findGeoColumn);
|
|
463
548
|
if (thisGen !== loadGeneration) return;
|
|
464
549
|
schema = result.schema;
|
|
465
550
|
columns = schema.map((f) => f.name);
|
|
@@ -486,7 +571,7 @@ async function loadTable() {
|
|
|
486
571
|
}
|
|
487
572
|
}
|
|
488
573
|
} else {
|
|
489
|
-
schema = await engine.getSchema(
|
|
574
|
+
schema = await engine.getSchema(cid, resolved);
|
|
490
575
|
if (thisGen !== loadGeneration) return;
|
|
491
576
|
columns = schema.map((f) => f.name);
|
|
492
577
|
const colPreview =
|
|
@@ -507,7 +592,7 @@ async function loadTable() {
|
|
|
507
592
|
...loadProgress,
|
|
508
593
|
{ label: t('progress.geometry'), value: `${detectedGeoCol} (${geoColType})` }
|
|
509
594
|
];
|
|
510
|
-
sourceCrs = await engine.detectCrs(
|
|
595
|
+
sourceCrs = await engine.detectCrs(cid, resolved, detectedGeoCol);
|
|
511
596
|
if (thisGen !== loadGeneration) return;
|
|
512
597
|
if (sourceCrs) {
|
|
513
598
|
loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
|
|
@@ -530,13 +615,34 @@ async function loadTable() {
|
|
|
530
615
|
|
|
531
616
|
loadStage = t('table.runningQuery');
|
|
532
617
|
const start = performance.now();
|
|
533
|
-
|
|
618
|
+
let result = await executeQuery(sqlQuery);
|
|
534
619
|
if (thisGen !== loadGeneration) return;
|
|
620
|
+
|
|
621
|
+
// If query failed and this is a GeoParquet file, the error may be from
|
|
622
|
+
// auto-conversion of CRS metadata (e.g. "stoi: no conversion").
|
|
623
|
+
// Retry with enable_geoparquet_conversion=false and BLOB handling.
|
|
624
|
+
if (!result && error && isParquet && geoCol && !isLegacyGeoParquet) {
|
|
625
|
+
try {
|
|
626
|
+
await engine.query(cid, 'SET enable_geoparquet_conversion = false');
|
|
627
|
+
geoColType = 'BLOB';
|
|
628
|
+
sqlQuery = buildDefaultSql(0);
|
|
629
|
+
customSql = sqlQuery;
|
|
630
|
+
error = null;
|
|
631
|
+
result = await executeQuery(sqlQuery);
|
|
632
|
+
if (thisGen !== loadGeneration) return;
|
|
633
|
+
} catch {
|
|
634
|
+
// Retry also failed — original error stands
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
535
638
|
executionTimeMs = Math.round(performance.now() - start);
|
|
536
639
|
|
|
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.
|
|
640
|
+
if (isFileSource && !cloudNative && result) {
|
|
641
|
+
// Non-cloud-native file (CSV, GeoJSON, etc.): derive schema from the
|
|
642
|
+
// query result so we avoid a second full-file download. Skipped for
|
|
643
|
+
// SQL-backed sources (attached DuckLake tables), which already have
|
|
644
|
+
// a proper schema from getSchemaAndCrs — re-deriving here would pick
|
|
645
|
+
// up the projected `__wkb` alias and clobber the real geo column.
|
|
540
646
|
schema = (result.columns ?? []).map((col, i) => ({
|
|
541
647
|
name: col,
|
|
542
648
|
type: result.types?.[i] ?? 'VARCHAR',
|
|
@@ -591,7 +697,7 @@ async function loadTable() {
|
|
|
591
697
|
} else {
|
|
592
698
|
loadStage = t('table.countingRows');
|
|
593
699
|
engine
|
|
594
|
-
.getRowCount(
|
|
700
|
+
.getRowCount(cid, resolved)
|
|
595
701
|
.then((count) => {
|
|
596
702
|
if (thisGen === loadGeneration) {
|
|
597
703
|
totalRows = count;
|
|
@@ -613,11 +719,14 @@ async function loadTable() {
|
|
|
613
719
|
}
|
|
614
720
|
|
|
615
721
|
async function executeQuery(sql: string) {
|
|
722
|
+
// Snapshot `connId` — reading the $derived after the effect is destroyed
|
|
723
|
+
// returns a Symbol sentinel and crashes downstream template literals.
|
|
724
|
+
const cid = connId;
|
|
616
725
|
try {
|
|
617
726
|
const engine = await getQueryEngine();
|
|
618
727
|
|
|
619
728
|
if (engine.queryCancellable) {
|
|
620
|
-
const handle = engine.queryCancellable(
|
|
729
|
+
const handle = engine.queryCancellable(cid, sql);
|
|
621
730
|
activeHandle = handle;
|
|
622
731
|
try {
|
|
623
732
|
const result = await handle.result;
|
|
@@ -634,7 +743,7 @@ async function executeQuery(sql: string) {
|
|
|
634
743
|
}
|
|
635
744
|
}
|
|
636
745
|
|
|
637
|
-
const result = await engine.query(
|
|
746
|
+
const result = await engine.query(cid, sql);
|
|
638
747
|
columns = result.columns;
|
|
639
748
|
rows = result.rows;
|
|
640
749
|
return result;
|
|
@@ -664,6 +773,8 @@ async function loadPage(page: number) {
|
|
|
664
773
|
}
|
|
665
774
|
|
|
666
775
|
async function runCustomSql() {
|
|
776
|
+
// Snapshot before any await — see note in executeQuery.
|
|
777
|
+
const cid = connId;
|
|
667
778
|
queryRunning = true;
|
|
668
779
|
error = null;
|
|
669
780
|
isCustomQuery = true;
|
|
@@ -686,7 +797,7 @@ async function runCustomSql() {
|
|
|
686
797
|
timestamp: Date.now(),
|
|
687
798
|
durationMs: executionTimeMs,
|
|
688
799
|
rowCount: rows.length,
|
|
689
|
-
connectionId:
|
|
800
|
+
connectionId: cid || undefined
|
|
690
801
|
});
|
|
691
802
|
} catch (err) {
|
|
692
803
|
executionTimeMs = Math.round(performance.now() - start);
|
|
@@ -698,7 +809,7 @@ async function runCustomSql() {
|
|
|
698
809
|
durationMs: executionTimeMs,
|
|
699
810
|
rowCount: 0,
|
|
700
811
|
error: error ?? undefined,
|
|
701
|
-
connectionId:
|
|
812
|
+
connectionId: cid || undefined
|
|
702
813
|
});
|
|
703
814
|
} finally {
|
|
704
815
|
queryRunning = false;
|
|
@@ -902,7 +1013,7 @@ function setStacView() {
|
|
|
902
1013
|
<FileInfo
|
|
903
1014
|
entries={loadProgress}
|
|
904
1015
|
{schema}
|
|
905
|
-
parquetUrl={/\.parquet$/i.test(tab.path) ?
|
|
1016
|
+
parquetUrl={/\.parquet$/i.test(tab.path) ? parquetIframeUrl : ''}
|
|
906
1017
|
/>
|
|
907
1018
|
</div>
|
|
908
1019
|
{:else if viewMode === 'stac'}
|
|
@@ -5,7 +5,7 @@ import { onDestroy, untrack } from 'svelte';
|
|
|
5
5
|
import { t } from '../../i18n/index.svelte.js';
|
|
6
6
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
7
7
|
import type { Tab } from '../../types';
|
|
8
|
-
import {
|
|
8
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
9
9
|
import {
|
|
10
10
|
ensureCodecsRegistered,
|
|
11
11
|
extractZarrStoreUrl,
|
|
@@ -372,7 +372,7 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
372
372
|
await ensureCodecsRegistered();
|
|
373
373
|
const { ZarrLayer } = await import('@carbonplan/zarr-layer');
|
|
374
374
|
|
|
375
|
-
const storeUrl = buildStoreUrl();
|
|
375
|
+
const storeUrl = await buildStoreUrl();
|
|
376
376
|
const selector = buildSelector();
|
|
377
377
|
|
|
378
378
|
const opts: any = {
|
|
@@ -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) {
|
|
@@ -423,8 +452,8 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
423
452
|
}
|
|
424
453
|
}
|
|
425
454
|
|
|
426
|
-
function buildStoreUrl(): string {
|
|
427
|
-
const rawUrl =
|
|
455
|
+
async function buildStoreUrl(): Promise<string> {
|
|
456
|
+
const rawUrl = (await buildHttpsUrlAsync(tab)).replace(/\/+$/, '');
|
|
428
457
|
return extractZarrStoreUrl(rawUrl) ?? rawUrl;
|
|
429
458
|
}
|
|
430
459
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '../ui/resizable/index.js';
|
|
10
10
|
import { t } from '../../i18n/index.svelte.js';
|
|
11
11
|
import type { Tab } from '../../types';
|
|
12
|
-
import {
|
|
12
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
13
13
|
import { getUrlView, updateUrlView } from '../../utils/url-state.js';
|
|
14
14
|
import {
|
|
15
15
|
computeChunkCount,
|
|
@@ -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
|
);
|
|
@@ -123,7 +120,7 @@ async function loadHierarchy() {
|
|
|
123
120
|
error = null;
|
|
124
121
|
|
|
125
122
|
try {
|
|
126
|
-
const rawUrl =
|
|
123
|
+
const rawUrl = (await buildHttpsUrlAsync(tab)).replace(/\/+$/, '');
|
|
127
124
|
const url = extractZarrStoreUrl(rawUrl) ?? rawUrl;
|
|
128
125
|
const storeName = tab.name.replace(/\.(zarr|zr3)$/, '');
|
|
129
126
|
|
|
@@ -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>
|
|
@@ -12,7 +12,6 @@ import type { Tab } from '../../../types';
|
|
|
12
12
|
import { setupSelectionLayer, updateSelection } from '../../../utils/map-selection.js';
|
|
13
13
|
import { buildPmtilesLayers, getPmtilesProtocol, type PmtilesMetadata } from '../../../utils/pmtiles';
|
|
14
14
|
import { layerHue } from '../../../utils/pmtiles-tile.js';
|
|
15
|
-
import { buildHttpsUrl } from '../../../utils/url.js';
|
|
16
15
|
import AttributeTable from '../map/AttributeTable.svelte';
|
|
17
16
|
import MapContainer from '../map/MapContainer.svelte';
|
|
18
17
|
|
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': 'من',
|
|
@@ -327,6 +342,7 @@ export const ar = {
|
|
|
327
342
|
'map.flatgeobufInfo': 'معلومات FlatGeobuf',
|
|
328
343
|
'map.cogInfo': 'معلومات COG',
|
|
329
344
|
'map.cogCorsError': 'تعذّر تحميل COG: الخادم لا يسمح بطلبات عبر النطاقات (CORS). يجب استضافة الملف مع تفعيل ترويسات CORS.',
|
|
345
|
+
'map.cogInvalidTiff': 'هذا الملف ليس ملف TIFF صالح. نوع المحتوى image/tiff لكن الأجزاء الأولى من الملف لا تطابق توقيع TIFF، قد يكون الملف تالفاً أو مشفّراً أو مُعنوناً بشكل خاطئ.',
|
|
330
346
|
'map.cogUnsupportedFormat': 'يستخدم هذا الملف صيغة {{type}} غير مدعومة لعرض الخريطة. يمكن عرض ملفات COG بصيغة RGB فقط.',
|
|
331
347
|
'map.noGeoColumn': 'لم يتم اكتشاف عمود هندسي في المخطط',
|
|
332
348
|
'map.noData': 'لا تتوفر بيانات لعرض الخريطة',
|
|
@@ -366,6 +382,15 @@ export const ar = {
|
|
|
366
382
|
'mapInfo.columns': 'الأعمدة',
|
|
367
383
|
'mapInfo.size': 'الحجم',
|
|
368
384
|
'mapInfo.bands': 'النطاقات',
|
|
385
|
+
// COG Controls
|
|
386
|
+
'cog.style': 'النمط',
|
|
387
|
+
'cog.band': 'النطاق',
|
|
388
|
+
'cog.singleBand': 'نطاق واحد',
|
|
389
|
+
'cog.colorRamp': 'تدرج الألوان',
|
|
390
|
+
'cog.pixelValue': 'قيمة البكسل',
|
|
391
|
+
'cog.reading': 'قراءة البكسل...',
|
|
392
|
+
'cog.rescale': 'إعادة القياس',
|
|
393
|
+
'cog.rescaleReset': 'إعادة تعيين',
|
|
369
394
|
// PMTiles Viewer
|
|
370
395
|
'pmtiles.mapView': 'خريطة',
|
|
371
396
|
'pmtiles.archiveView': 'الأرشيف',
|