@walkthru-earth/objex 1.2.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/dist/components/browser/FileTreeSidebar.svelte +1 -1
- package/dist/components/layout/Sidebar.svelte +27 -0
- package/dist/components/viewers/ArchiveViewer.svelte +4 -4
- package/dist/components/viewers/CodeViewer.svelte +21 -5
- package/dist/components/viewers/CogViewer.svelte +21 -3
- package/dist/components/viewers/CopcViewer.svelte +20 -2
- package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
- package/dist/components/viewers/PmtilesViewer.svelte +2 -2
- package/dist/components/viewers/StacMapViewer.svelte +25 -9
- package/dist/components/viewers/TableViewer.svelte +50 -21
- package/dist/components/viewers/ZarrMapViewer.svelte +4 -4
- package/dist/components/viewers/ZarrViewer.svelte +2 -2
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
- package/dist/i18n/ar.js +1 -0
- package/dist/i18n/en.js +1 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/source.d.ts +12 -0
- package/dist/query/source.js +25 -8
- package/dist/query/wasm.js +130 -23
- 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 +6 -0
- package/dist/storage/providers.js +13 -2
- package/dist/stores/browser.svelte.d.ts +2 -0
- package/dist/stores/browser.svelte.js +17 -1
- package/dist/utils/url.d.ts +13 -0
- package/dist/utils/url.js +36 -0
- package/dist/utils/wkb.js +22 -8
- package/package.json +1 -1
|
@@ -356,7 +356,7 @@ async function expandToPath(path: string) {
|
|
|
356
356
|
? await findNodeAtRoot(accumulatedPath)
|
|
357
357
|
: await findNodeInParent(parentNode, accumulatedPath);
|
|
358
358
|
|
|
359
|
-
if (!node
|
|
359
|
+
if (!node?.entry.is_dir) break;
|
|
360
360
|
|
|
361
361
|
if (node.children.length === 0) {
|
|
362
362
|
await loadChildren(node);
|
|
@@ -57,6 +57,33 @@ $effect(() => {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// Auto-detected ?url= buckets are saved anonymously (zero-click demo flow).
|
|
61
|
+
// If the first LIST returns 401/403, the bucket is actually private — flip
|
|
62
|
+
// the connection to non-anonymous and open the credential dialog so the
|
|
63
|
+
// user can paste keys instead of seeing a silent failure.
|
|
64
|
+
$effect(() => {
|
|
65
|
+
const conn = browser.authRequired;
|
|
66
|
+
if (!conn) return;
|
|
67
|
+
handleAuthRequired(conn);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
async function handleAuthRequired(conn: Connection) {
|
|
71
|
+
browser.clearAuthRequired();
|
|
72
|
+
await connections.update(conn.id, {
|
|
73
|
+
name: conn.name,
|
|
74
|
+
provider: conn.provider,
|
|
75
|
+
endpoint: conn.endpoint,
|
|
76
|
+
bucket: conn.bucket,
|
|
77
|
+
region: conn.region,
|
|
78
|
+
anonymous: false,
|
|
79
|
+
authMethod: conn.authMethod,
|
|
80
|
+
rootPrefix: conn.rootPrefix
|
|
81
|
+
});
|
|
82
|
+
const updated = connections.getById(conn.id);
|
|
83
|
+
if (!updated) return;
|
|
84
|
+
await ensureCredentials(updated);
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
async function handleAutoDetection() {
|
|
61
88
|
const url = new URL(window.location.href);
|
|
62
89
|
const rawUrl = url.searchParams.get('url');
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
streamZipEntriesFromUrl
|
|
30
30
|
} from '../../utils/archive';
|
|
31
31
|
import { formatFileSize } from '../../utils/format';
|
|
32
|
-
import {
|
|
32
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
33
33
|
|
|
34
34
|
let { tab }: { tab: Tab } = $props();
|
|
35
35
|
|
|
@@ -177,7 +177,7 @@ async function loadZip() {
|
|
|
177
177
|
const signal = abortController!.signal;
|
|
178
178
|
|
|
179
179
|
if (tab.source === 'remote') {
|
|
180
|
-
const url =
|
|
180
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
181
181
|
try {
|
|
182
182
|
scanning = true;
|
|
183
183
|
for await (const batch of streamZipEntriesFromUrl(url, signal)) {
|
|
@@ -208,7 +208,7 @@ async function loadTar() {
|
|
|
208
208
|
const signal = abortController!.signal;
|
|
209
209
|
|
|
210
210
|
if (tab.source === 'remote') {
|
|
211
|
-
const url =
|
|
211
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
212
212
|
try {
|
|
213
213
|
scanning = true;
|
|
214
214
|
remoteUrl = url;
|
|
@@ -240,7 +240,7 @@ async function loadTarGz() {
|
|
|
240
240
|
|
|
241
241
|
// For remote URLs: stream-fetch → decompress → parse progressively
|
|
242
242
|
if (tab.source === 'remote' || tab.source === 'url') {
|
|
243
|
-
const url =
|
|
243
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
244
244
|
try {
|
|
245
245
|
scanning = true;
|
|
246
246
|
const decompressedChunks: Uint8Array[] = [];
|
|
@@ -11,7 +11,7 @@ import type { Tab } from '../../types';
|
|
|
11
11
|
import { copyToClipboard } from '../../utils/clipboard.js';
|
|
12
12
|
import { handleLoadError } from '../../utils/error.js';
|
|
13
13
|
import { extensionToShikiLang, highlightCode } from '../../utils/shiki';
|
|
14
|
-
import { buildHttpsUrl } from '../../utils/url.js';
|
|
14
|
+
import { buildHttpsUrl, buildHttpsUrlAsync, canStreamDirectly } from '../../utils/url.js';
|
|
15
15
|
import { getUrlView, updateUrlView } from '../../utils/url-state.js';
|
|
16
16
|
import { openZarrTab } from '../../utils/zarr-tab.js';
|
|
17
17
|
|
|
@@ -96,7 +96,23 @@ const stacBadgeKey = $derived<Record<string, string>>({
|
|
|
96
96
|
'stac-collection': 'code.stacCollection',
|
|
97
97
|
'stac-item': 'code.stacItem'
|
|
98
98
|
});
|
|
99
|
-
|
|
99
|
+
// Third-party iframes can't route through the storage adapter, so the URL
|
|
100
|
+
// must carry auth. Public/SAS connections resolve synchronously; `signed-s3`
|
|
101
|
+
// must wait for the presign so the iframe never loads a bare `s3://` href.
|
|
102
|
+
let styleUrl = $state('');
|
|
103
|
+
$effect(() => {
|
|
104
|
+
const id = tab.id;
|
|
105
|
+
styleUrl = canStreamDirectly(tab) ? buildHttpsUrl(tab) : '';
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
(async () => {
|
|
108
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
109
|
+
if (cancelled || id !== tab.id) return;
|
|
110
|
+
styleUrl = url;
|
|
111
|
+
})();
|
|
112
|
+
return () => {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
};
|
|
115
|
+
});
|
|
100
116
|
const stacBrowserSrc = $derived(
|
|
101
117
|
`https://radiantearth.github.io/stac-browser/#/external/${styleUrl}`
|
|
102
118
|
);
|
|
@@ -481,7 +497,7 @@ async function copyCode() {
|
|
|
481
497
|
</div>
|
|
482
498
|
</div>
|
|
483
499
|
|
|
484
|
-
{#if viewMode === 'stac-browser'}
|
|
500
|
+
{#if viewMode === 'stac-browser' && styleUrl}
|
|
485
501
|
<div class="flex-1 overflow-hidden">
|
|
486
502
|
<iframe
|
|
487
503
|
src={stacBrowserSrc}
|
|
@@ -490,7 +506,7 @@ async function copyCode() {
|
|
|
490
506
|
allow="fullscreen"
|
|
491
507
|
></iframe>
|
|
492
508
|
</div>
|
|
493
|
-
{:else if viewMode === 'kepler'}
|
|
509
|
+
{:else if viewMode === 'kepler' && styleUrl}
|
|
494
510
|
<div class="flex-1 overflow-hidden">
|
|
495
511
|
<iframe
|
|
496
512
|
src={keplerSrc}
|
|
@@ -499,7 +515,7 @@ async function copyCode() {
|
|
|
499
515
|
allow="fullscreen"
|
|
500
516
|
></iframe>
|
|
501
517
|
</div>
|
|
502
|
-
{:else if viewMode === 'maputnik'}
|
|
518
|
+
{:else if viewMode === 'maputnik' && styleUrl}
|
|
503
519
|
<div class="flex-1 overflow-hidden">
|
|
504
520
|
<iframe
|
|
505
521
|
src={maputnikSrc}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
resolveProj4Def,
|
|
28
28
|
selectCogPipeline
|
|
29
29
|
} from '../../utils/cog.js';
|
|
30
|
-
import {
|
|
30
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
31
31
|
import CogControls from './CogControls.svelte';
|
|
32
32
|
import MapContainer from './map/MapContainer.svelte';
|
|
33
33
|
|
|
@@ -57,6 +57,7 @@ let proj4DefRef: string | null = null;
|
|
|
57
57
|
let sampleFormatRef = 1;
|
|
58
58
|
let isTiledRef = true;
|
|
59
59
|
let clickHandlerRef: ((e: maplibregl.MapMouseEvent) => void) | null = null;
|
|
60
|
+
let resolvedHttpsUrl: string | null = null;
|
|
60
61
|
// True when the library-default uint pipeline will run. LinearRescale only
|
|
61
62
|
// operates on already-normalized RGB 0..1, so the slider is meaningful only
|
|
62
63
|
// here, and only for non-palette data (palette renders through Colormap).
|
|
@@ -104,6 +105,7 @@ $effect(() => {
|
|
|
104
105
|
overlayRef = null;
|
|
105
106
|
geotiffRef = null;
|
|
106
107
|
proj4DefRef = null;
|
|
108
|
+
resolvedHttpsUrl = null;
|
|
107
109
|
loading = true;
|
|
108
110
|
error = null;
|
|
109
111
|
cogInfo = null;
|
|
@@ -165,7 +167,9 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
165
167
|
const signal = abortController.signal;
|
|
166
168
|
|
|
167
169
|
try {
|
|
168
|
-
const url =
|
|
170
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
171
|
+
if (signal.aborted) return;
|
|
172
|
+
resolvedHttpsUrl = url;
|
|
169
173
|
|
|
170
174
|
// Pre-flight: read first IFD to check if tiled (single range request).
|
|
171
175
|
let isTiled = true;
|
|
@@ -187,6 +191,19 @@ async function loadCog(map: maplibregl.Map) {
|
|
|
187
191
|
}
|
|
188
192
|
} catch (preflightErr) {
|
|
189
193
|
if (signal.aborted) return;
|
|
194
|
+
// `@developmentseed/geotiff` throws "Only tiff supported version:<n>"
|
|
195
|
+
// when the first 4 bytes don't match II*\0 / MM\0* / II+\0 / MM\0+.
|
|
196
|
+
// This happens on files that advertise image/tiff but are corrupt,
|
|
197
|
+
// encrypted, or a different format entirely (GDAL reports "not
|
|
198
|
+
// recognized as being in a supported file format" on the same file).
|
|
199
|
+
// Surface a clear message and bail — COGLayer would re-invoke the
|
|
200
|
+
// same loader and throw the identical error uncaught during update.
|
|
201
|
+
const msg = preflightErr instanceof Error ? preflightErr.message : String(preflightErr);
|
|
202
|
+
if (/Only tiff supported version|not a tiff|Invalid.*magic/i.test(msg)) {
|
|
203
|
+
error = t('map.cogInvalidTiff');
|
|
204
|
+
loading = false;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
// Store refs for pixel inspection and rebuild
|
|
@@ -255,7 +272,7 @@ function buildAndAddLayer(
|
|
|
255
272
|
// Apply upstream-bug workarounds in place (overview filter, 4326 bbox clamp).
|
|
256
273
|
if (preflightGeotiff) normalizeCogGeotiff(preflightGeotiff);
|
|
257
274
|
|
|
258
|
-
const cogInput = preflightGeotiff ??
|
|
275
|
+
const cogInput = preflightGeotiff ?? resolvedHttpsUrl ?? '';
|
|
259
276
|
|
|
260
277
|
const layer = new COGLayer({
|
|
261
278
|
// Stable id per tab so rebuilds on band/style change don't force deck.gl
|
|
@@ -384,6 +401,7 @@ function cleanup() {
|
|
|
384
401
|
geotiffRef = null;
|
|
385
402
|
proj4DefRef = null;
|
|
386
403
|
pixelValue = null;
|
|
404
|
+
resolvedHttpsUrl = null;
|
|
387
405
|
}
|
|
388
406
|
|
|
389
407
|
$effect(() => {
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Tab } from '../../types';
|
|
3
|
-
import {
|
|
3
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
4
4
|
|
|
5
5
|
let { tab }: { tab: Tab } = $props();
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
let fileUrl = $state('');
|
|
8
|
+
|
|
9
|
+
$effect(() => {
|
|
10
|
+
const id = tab.id;
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
(async () => {
|
|
13
|
+
if (tab.source === 'url') {
|
|
14
|
+
fileUrl = tab.path;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
18
|
+
if (cancelled || id !== tab.id) return;
|
|
19
|
+
fileUrl = url;
|
|
20
|
+
})();
|
|
21
|
+
return () => {
|
|
22
|
+
cancelled = true;
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
8
26
|
const viewerUrl = $derived(
|
|
9
27
|
fileUrl ? `https://viewer.copc.io/?copc=${encodeURIComponent(fileUrl)}` : ''
|
|
10
28
|
);
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
hoverCursor,
|
|
20
20
|
loadDeckModules
|
|
21
21
|
} from '../../utils/deck.js';
|
|
22
|
-
import {
|
|
22
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
23
23
|
import AttributeTable from './map/AttributeTable.svelte';
|
|
24
24
|
import MapContainer from './map/MapContainer.svelte';
|
|
25
25
|
|
|
@@ -55,6 +55,7 @@ let mapReadyPromise: Promise<void> | null = null;
|
|
|
55
55
|
// Stored from preview for load-all (skip index)
|
|
56
56
|
let storedHeader: HeaderMeta | null = null;
|
|
57
57
|
let storedFeatureOffset = 0;
|
|
58
|
+
let signedUrl: string | null = null;
|
|
58
59
|
|
|
59
60
|
// proj4 converter for reprojecting from source CRS → WGS84
|
|
60
61
|
let proj4Forward: ((coord: [number, number]) => [number, number]) | null = null;
|
|
@@ -217,6 +218,7 @@ function cleanup() {
|
|
|
217
218
|
dataVersion = 0;
|
|
218
219
|
storedHeader = null;
|
|
219
220
|
storedFeatureOffset = 0;
|
|
221
|
+
signedUrl = null;
|
|
220
222
|
proj4Forward = null;
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -261,9 +263,14 @@ async function loadFlatGeobuf() {
|
|
|
261
263
|
await mapReadyPromise;
|
|
262
264
|
if (!overlay) return;
|
|
263
265
|
|
|
266
|
+
// Sign once per load so header + feature stream share the same signature.
|
|
267
|
+
// Cached across loadAllFeatures() so the "Load all" button doesn't re-sign.
|
|
268
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
269
|
+
signedUrl = url;
|
|
270
|
+
|
|
264
271
|
// Read header via range requests (fast: 1-2 small requests)
|
|
265
272
|
// Gets metadata + feature offset to skip the spatial index
|
|
266
|
-
await readHeaderWithRangeRequests();
|
|
273
|
+
await readHeaderWithRangeRequests(url);
|
|
267
274
|
|
|
268
275
|
// Set up on-the-fly reprojection if the file uses a non-WGS84 CRS
|
|
269
276
|
proj4Forward = null;
|
|
@@ -298,7 +305,7 @@ async function loadFlatGeobuf() {
|
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
// Stream features (skips index if header was read, else sequential)
|
|
301
|
-
await streamFeatures(settings.featureLimit);
|
|
308
|
+
await streamFeatures(url, settings.featureLimit);
|
|
302
309
|
} catch (err) {
|
|
303
310
|
console.error('[FGB]', 'loadFlatGeobuf error:', err);
|
|
304
311
|
if (err instanceof DOMException && err.name === 'AbortError') return;
|
|
@@ -314,9 +321,7 @@ async function loadFlatGeobuf() {
|
|
|
314
321
|
* Read header via range requests (fast: 1-2 small requests).
|
|
315
322
|
* Stores header + feature offset for the composite stream approach.
|
|
316
323
|
*/
|
|
317
|
-
async function readHeaderWithRangeRequests(): Promise<boolean> {
|
|
318
|
-
const url = buildHttpsUrl(tab);
|
|
319
|
-
|
|
324
|
+
async function readHeaderWithRangeRequests(url: string): Promise<boolean> {
|
|
320
325
|
let reader: HttpReader;
|
|
321
326
|
try {
|
|
322
327
|
reader = await HttpReader.open(url, false);
|
|
@@ -357,7 +362,9 @@ async function loadAllFeatures() {
|
|
|
357
362
|
try {
|
|
358
363
|
features = [];
|
|
359
364
|
featureCount = 0;
|
|
360
|
-
await
|
|
365
|
+
const url = signedUrl ?? (await buildHttpsUrlAsync(tab));
|
|
366
|
+
signedUrl = url;
|
|
367
|
+
await streamFeatures(url);
|
|
361
368
|
} catch (err) {
|
|
362
369
|
console.error('[FGB]', 'loadAllFeatures error:', err);
|
|
363
370
|
if (err instanceof DOMException && err.name === 'AbortError') return;
|
|
@@ -372,10 +379,9 @@ async function loadAllFeatures() {
|
|
|
372
379
|
* Stream features sequentially.
|
|
373
380
|
* If storedHeader is available, skips the index with a Range request + composite stream.
|
|
374
381
|
*/
|
|
375
|
-
async function streamFeatures(limit?: number) {
|
|
382
|
+
async function streamFeatures(url: string, limit?: number) {
|
|
376
383
|
const ac = new AbortController();
|
|
377
384
|
abortController = ac;
|
|
378
|
-
const url = buildHttpsUrl(tab);
|
|
379
385
|
const t0 = performance.now();
|
|
380
386
|
|
|
381
387
|
let iter: AsyncGenerator;
|
|
@@ -11,7 +11,7 @@ import { t } from '../../i18n/index.svelte.js';
|
|
|
11
11
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
12
12
|
import type { Tab } from '../../types';
|
|
13
13
|
import { loadPmtiles, type PmtilesMetadata } from '../../utils/pmtiles';
|
|
14
|
-
import {
|
|
14
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
15
15
|
import { getUrlView, updateUrlView } from '../../utils/url-state.js';
|
|
16
16
|
|
|
17
17
|
let { tab }: { tab: Tab } = $props();
|
|
@@ -73,7 +73,7 @@ async function load() {
|
|
|
73
73
|
error = null;
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
|
-
pmtilesUrl =
|
|
76
|
+
pmtilesUrl = await buildHttpsUrlAsync(tab);
|
|
77
77
|
const result = await loadPmtiles(pmtilesUrl);
|
|
78
78
|
pmtilesInstance = result.pmtiles;
|
|
79
79
|
metadata = result.metadata;
|
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Tab } from '../../types';
|
|
3
|
-
import {
|
|
3
|
+
import { buildHttpsUrlAsync } from '../../utils/url.js';
|
|
4
4
|
|
|
5
5
|
let { tab }: { tab: Tab } = $props();
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
let fileUrl = $state('');
|
|
8
|
+
|
|
9
|
+
$effect(() => {
|
|
10
|
+
const id = tab.id;
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
(async () => {
|
|
13
|
+
const url = await buildHttpsUrlAsync(tab);
|
|
14
|
+
if (cancelled || id !== tab.id) return;
|
|
15
|
+
fileUrl = url;
|
|
16
|
+
})();
|
|
17
|
+
return () => {
|
|
18
|
+
cancelled = true;
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
8
22
|
const iframeSrc = $derived(
|
|
9
|
-
`https://developmentseed.org/stac-map?href=${encodeURIComponent(fileUrl)}`
|
|
23
|
+
fileUrl ? `https://developmentseed.org/stac-map?href=${encodeURIComponent(fileUrl)}` : ''
|
|
10
24
|
);
|
|
11
25
|
</script>
|
|
12
26
|
|
|
13
27
|
<div class="relative flex h-full overflow-hidden">
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
{#if iframeSrc}
|
|
29
|
+
<iframe
|
|
30
|
+
src={iframeSrc}
|
|
31
|
+
class="h-full w-full border-0"
|
|
32
|
+
title="STAC Map"
|
|
33
|
+
allow="fullscreen"
|
|
34
|
+
></iframe>
|
|
35
|
+
{/if}
|
|
20
36
|
</div>
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
QueryCancelledError,
|
|
12
12
|
type QueryHandle,
|
|
13
13
|
type ResolvedTableSource,
|
|
14
|
-
resolveTableSource
|
|
14
|
+
resolveTableSource,
|
|
15
|
+
resolveTableSourceAsync
|
|
15
16
|
} from '../../query/index.js';
|
|
16
17
|
import { queryHistory } from '../../stores/query-history.svelte.js';
|
|
17
18
|
import { settings } from '../../stores/settings.svelte.js';
|
|
@@ -61,6 +62,8 @@ let viewMode = $state<'table' | 'map' | 'stac' | 'info'>(
|
|
|
61
62
|
);
|
|
62
63
|
let sqlQuery = $state('');
|
|
63
64
|
let customSql = $state('');
|
|
65
|
+
// Presigned URL for the source-cooperative parquet-table iframe (external fetcher).
|
|
66
|
+
let parquetIframeUrl = $state('');
|
|
64
67
|
let queryRunning = $state(false);
|
|
65
68
|
let executionTimeMs = $state(0);
|
|
66
69
|
|
|
@@ -98,8 +101,12 @@ const columnTypes = $derived(Object.fromEntries(schema.map((f) => [f.name, f.typ
|
|
|
98
101
|
// Columns for display — exclude internal __wkb helper
|
|
99
102
|
const displayColumns = $derived(columns.filter((c) => c !== '__wkb'));
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
let resolvedSource: ResolvedTableSource | null = null;
|
|
105
|
+
|
|
106
|
+
function buildDefaultSql(
|
|
107
|
+
offset = 0,
|
|
108
|
+
resolved: ResolvedTableSource = resolvedSource ?? resolveTableSource(tab)
|
|
109
|
+
): string {
|
|
103
110
|
const source = resolved.ref;
|
|
104
111
|
|
|
105
112
|
let sql: string;
|
|
@@ -204,6 +211,8 @@ $effect(() => {
|
|
|
204
211
|
geoCol = null;
|
|
205
212
|
knownGeomType = undefined;
|
|
206
213
|
metadataBounds = null;
|
|
214
|
+
resolvedSource = null;
|
|
215
|
+
parquetIframeUrl = '';
|
|
207
216
|
error = null;
|
|
208
217
|
});
|
|
209
218
|
return unregister;
|
|
@@ -268,6 +277,10 @@ async function forceCancel() {
|
|
|
268
277
|
|
|
269
278
|
async function loadTable() {
|
|
270
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;
|
|
271
284
|
|
|
272
285
|
// Cancel in-flight query from a previous load to prevent duplicate concurrent queries
|
|
273
286
|
if (activeHandle) {
|
|
@@ -287,13 +300,19 @@ async function loadTable() {
|
|
|
287
300
|
loadStage = t('table.preparingQuery');
|
|
288
301
|
loadProgress = [];
|
|
289
302
|
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
sqlQuery =
|
|
293
|
-
customSql =
|
|
303
|
+
resolvedSource = null;
|
|
304
|
+
const eagerSql = buildDefaultSql(0);
|
|
305
|
+
sqlQuery = eagerSql;
|
|
306
|
+
customSql = eagerSql;
|
|
294
307
|
|
|
295
308
|
try {
|
|
296
|
-
const resolved: ResolvedTableSource =
|
|
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;
|
|
297
316
|
const isFileSource = resolved.isFileSource;
|
|
298
317
|
const fileUrl = resolved.fileUrl ?? '';
|
|
299
318
|
const httpsUrl = isFileSource ? buildHttpsUrl(tab) : '';
|
|
@@ -301,6 +320,11 @@ async function loadTable() {
|
|
|
301
320
|
const isParquet = isFileSource && /\.parquet$/i.test(tab.path);
|
|
302
321
|
const streamable = isFileSource && canStreamDirectly(tab);
|
|
303
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;
|
|
327
|
+
|
|
304
328
|
// Start DuckDB boot immediately (runs in parallel with hyparquet)
|
|
305
329
|
loadStage = t('table.initEngine');
|
|
306
330
|
const enginePromise = getQueryEngine();
|
|
@@ -448,7 +472,7 @@ async function loadTable() {
|
|
|
448
472
|
// Disable conversion for this connection and fall back to BLOB handling.
|
|
449
473
|
if (metaFromHyparquet && isLegacyGeoParquet && geoCol) {
|
|
450
474
|
try {
|
|
451
|
-
await engine.query(
|
|
475
|
+
await engine.query(cid, 'SET enable_geoparquet_conversion = false');
|
|
452
476
|
geoColType = 'BLOB';
|
|
453
477
|
} catch {
|
|
454
478
|
// Setting failed — DuckDB may still handle it gracefully
|
|
@@ -460,7 +484,7 @@ async function loadTable() {
|
|
|
460
484
|
// reads GeoParquet as GEOMETRY('EPSG:...') with CRS embedded in the type.
|
|
461
485
|
if (metaFromHyparquet && geoCol && !isLegacyGeoParquet) {
|
|
462
486
|
try {
|
|
463
|
-
const duckSchema = await engine.getSchema(
|
|
487
|
+
const duckSchema = await engine.getSchema(cid, resolved);
|
|
464
488
|
if (thisGen !== loadGeneration) return;
|
|
465
489
|
const duckGeoField = duckSchema.find((f: { name: string }) => f.name === geoCol);
|
|
466
490
|
if (duckGeoField) {
|
|
@@ -490,7 +514,7 @@ async function loadTable() {
|
|
|
490
514
|
// (native Parquet GEOMETRY without "geo" KV metadata), use DuckDB
|
|
491
515
|
if (metaFromHyparquet && needsDuckDbCrs && geoCol) {
|
|
492
516
|
try {
|
|
493
|
-
sourceCrs = await engine.detectCrs(
|
|
517
|
+
sourceCrs = await engine.detectCrs(cid, resolved, geoCol);
|
|
494
518
|
if (thisGen !== loadGeneration) return;
|
|
495
519
|
if (sourceCrs) {
|
|
496
520
|
loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
|
|
@@ -520,7 +544,7 @@ async function loadTable() {
|
|
|
520
544
|
];
|
|
521
545
|
|
|
522
546
|
if (engine.getSchemaAndCrs) {
|
|
523
|
-
const result = await engine.getSchemaAndCrs(
|
|
547
|
+
const result = await engine.getSchemaAndCrs(cid, resolved, findGeoColumn);
|
|
524
548
|
if (thisGen !== loadGeneration) return;
|
|
525
549
|
schema = result.schema;
|
|
526
550
|
columns = schema.map((f) => f.name);
|
|
@@ -547,7 +571,7 @@ async function loadTable() {
|
|
|
547
571
|
}
|
|
548
572
|
}
|
|
549
573
|
} else {
|
|
550
|
-
schema = await engine.getSchema(
|
|
574
|
+
schema = await engine.getSchema(cid, resolved);
|
|
551
575
|
if (thisGen !== loadGeneration) return;
|
|
552
576
|
columns = schema.map((f) => f.name);
|
|
553
577
|
const colPreview =
|
|
@@ -568,7 +592,7 @@ async function loadTable() {
|
|
|
568
592
|
...loadProgress,
|
|
569
593
|
{ label: t('progress.geometry'), value: `${detectedGeoCol} (${geoColType})` }
|
|
570
594
|
];
|
|
571
|
-
sourceCrs = await engine.detectCrs(
|
|
595
|
+
sourceCrs = await engine.detectCrs(cid, resolved, detectedGeoCol);
|
|
572
596
|
if (thisGen !== loadGeneration) return;
|
|
573
597
|
if (sourceCrs) {
|
|
574
598
|
loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
|
|
@@ -599,7 +623,7 @@ async function loadTable() {
|
|
|
599
623
|
// Retry with enable_geoparquet_conversion=false and BLOB handling.
|
|
600
624
|
if (!result && error && isParquet && geoCol && !isLegacyGeoParquet) {
|
|
601
625
|
try {
|
|
602
|
-
await engine.query(
|
|
626
|
+
await engine.query(cid, 'SET enable_geoparquet_conversion = false');
|
|
603
627
|
geoColType = 'BLOB';
|
|
604
628
|
sqlQuery = buildDefaultSql(0);
|
|
605
629
|
customSql = sqlQuery;
|
|
@@ -673,7 +697,7 @@ async function loadTable() {
|
|
|
673
697
|
} else {
|
|
674
698
|
loadStage = t('table.countingRows');
|
|
675
699
|
engine
|
|
676
|
-
.getRowCount(
|
|
700
|
+
.getRowCount(cid, resolved)
|
|
677
701
|
.then((count) => {
|
|
678
702
|
if (thisGen === loadGeneration) {
|
|
679
703
|
totalRows = count;
|
|
@@ -695,11 +719,14 @@ async function loadTable() {
|
|
|
695
719
|
}
|
|
696
720
|
|
|
697
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;
|
|
698
725
|
try {
|
|
699
726
|
const engine = await getQueryEngine();
|
|
700
727
|
|
|
701
728
|
if (engine.queryCancellable) {
|
|
702
|
-
const handle = engine.queryCancellable(
|
|
729
|
+
const handle = engine.queryCancellable(cid, sql);
|
|
703
730
|
activeHandle = handle;
|
|
704
731
|
try {
|
|
705
732
|
const result = await handle.result;
|
|
@@ -716,7 +743,7 @@ async function executeQuery(sql: string) {
|
|
|
716
743
|
}
|
|
717
744
|
}
|
|
718
745
|
|
|
719
|
-
const result = await engine.query(
|
|
746
|
+
const result = await engine.query(cid, sql);
|
|
720
747
|
columns = result.columns;
|
|
721
748
|
rows = result.rows;
|
|
722
749
|
return result;
|
|
@@ -746,6 +773,8 @@ async function loadPage(page: number) {
|
|
|
746
773
|
}
|
|
747
774
|
|
|
748
775
|
async function runCustomSql() {
|
|
776
|
+
// Snapshot before any await — see note in executeQuery.
|
|
777
|
+
const cid = connId;
|
|
749
778
|
queryRunning = true;
|
|
750
779
|
error = null;
|
|
751
780
|
isCustomQuery = true;
|
|
@@ -768,7 +797,7 @@ async function runCustomSql() {
|
|
|
768
797
|
timestamp: Date.now(),
|
|
769
798
|
durationMs: executionTimeMs,
|
|
770
799
|
rowCount: rows.length,
|
|
771
|
-
connectionId:
|
|
800
|
+
connectionId: cid || undefined
|
|
772
801
|
});
|
|
773
802
|
} catch (err) {
|
|
774
803
|
executionTimeMs = Math.round(performance.now() - start);
|
|
@@ -780,7 +809,7 @@ async function runCustomSql() {
|
|
|
780
809
|
durationMs: executionTimeMs,
|
|
781
810
|
rowCount: 0,
|
|
782
811
|
error: error ?? undefined,
|
|
783
|
-
connectionId:
|
|
812
|
+
connectionId: cid || undefined
|
|
784
813
|
});
|
|
785
814
|
} finally {
|
|
786
815
|
queryRunning = false;
|
|
@@ -984,7 +1013,7 @@ function setStacView() {
|
|
|
984
1013
|
<FileInfo
|
|
985
1014
|
entries={loadProgress}
|
|
986
1015
|
{schema}
|
|
987
|
-
parquetUrl={/\.parquet$/i.test(tab.path) ?
|
|
1016
|
+
parquetUrl={/\.parquet$/i.test(tab.path) ? parquetIframeUrl : ''}
|
|
988
1017
|
/>
|
|
989
1018
|
</div>
|
|
990
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 = {
|
|
@@ -452,8 +452,8 @@ async function addZarrLayer(map: maplibregl.Map) {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
function buildStoreUrl(): string {
|
|
456
|
-
const rawUrl =
|
|
455
|
+
async function buildStoreUrl(): Promise<string> {
|
|
456
|
+
const rawUrl = (await buildHttpsUrlAsync(tab)).replace(/\/+$/, '');
|
|
457
457
|
return extractZarrStoreUrl(rawUrl) ?? rawUrl;
|
|
458
458
|
}
|
|
459
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,
|
|
@@ -120,7 +120,7 @@ async function loadHierarchy() {
|
|
|
120
120
|
error = null;
|
|
121
121
|
|
|
122
122
|
try {
|
|
123
|
-
const rawUrl =
|
|
123
|
+
const rawUrl = (await buildHttpsUrlAsync(tab)).replace(/\/+$/, '');
|
|
124
124
|
const url = extractZarrStoreUrl(rawUrl) ?? rawUrl;
|
|
125
125
|
const storeName = tab.name.replace(/\.(zarr|zr3)$/, '');
|
|
126
126
|
|
|
@@ -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/i18n/ar.js
CHANGED
|
@@ -342,6 +342,7 @@ export const ar = {
|
|
|
342
342
|
'map.flatgeobufInfo': 'معلومات FlatGeobuf',
|
|
343
343
|
'map.cogInfo': 'معلومات COG',
|
|
344
344
|
'map.cogCorsError': 'تعذّر تحميل COG: الخادم لا يسمح بطلبات عبر النطاقات (CORS). يجب استضافة الملف مع تفعيل ترويسات CORS.',
|
|
345
|
+
'map.cogInvalidTiff': 'هذا الملف ليس ملف TIFF صالح. نوع المحتوى image/tiff لكن الأجزاء الأولى من الملف لا تطابق توقيع TIFF، قد يكون الملف تالفاً أو مشفّراً أو مُعنوناً بشكل خاطئ.',
|
|
345
346
|
'map.cogUnsupportedFormat': 'يستخدم هذا الملف صيغة {{type}} غير مدعومة لعرض الخريطة. يمكن عرض ملفات COG بصيغة RGB فقط.',
|
|
346
347
|
'map.noGeoColumn': 'لم يتم اكتشاف عمود هندسي في المخطط',
|
|
347
348
|
'map.noData': 'لا تتوفر بيانات لعرض الخريطة',
|