@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.
Files changed (89) hide show
  1. package/README.md +3 -1
  2. package/dist/components/browser/FileBrowser.svelte +25 -14
  3. package/dist/components/browser/FileTreeSidebar.svelte +43 -7
  4. package/dist/components/layout/ConnectionDialog.svelte +100 -1
  5. package/dist/components/layout/Sidebar.svelte +70 -25
  6. package/dist/components/viewers/ArchiveViewer.svelte +4 -4
  7. package/dist/components/viewers/CodeViewer.svelte +44 -5
  8. package/dist/components/viewers/CogControls.svelte +208 -0
  9. package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
  10. package/dist/components/viewers/CogViewer.svelte +373 -1162
  11. package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
  12. package/dist/components/viewers/CopcViewer.svelte +20 -2
  13. package/dist/components/viewers/DatabaseViewer.svelte +345 -37
  14. package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
  15. package/dist/components/viewers/MarkdownViewer.svelte +1 -1
  16. package/dist/components/viewers/PmtilesViewer.svelte +2 -2
  17. package/dist/components/viewers/StacMapViewer.svelte +25 -9
  18. package/dist/components/viewers/TableViewer.svelte +162 -51
  19. package/dist/components/viewers/ZarrMapViewer.svelte +33 -4
  20. package/dist/components/viewers/ZarrViewer.svelte +3 -6
  21. package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
  22. package/dist/constants.d.ts +6 -2
  23. package/dist/constants.js +6 -2
  24. package/dist/file-icons/index.d.ts +1 -1
  25. package/dist/file-icons/index.js +12 -2
  26. package/dist/i18n/ar.js +25 -0
  27. package/dist/i18n/en.js +25 -0
  28. package/dist/i18n/index.svelte.d.ts +0 -1
  29. package/dist/i18n/index.svelte.js +0 -3
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +1 -0
  32. package/dist/query/engine.d.ts +20 -4
  33. package/dist/query/index.d.ts +2 -1
  34. package/dist/query/index.js +1 -0
  35. package/dist/query/source.d.ts +42 -0
  36. package/dist/query/source.js +54 -0
  37. package/dist/query/wasm.d.ts +7 -5
  38. package/dist/query/wasm.js +267 -107
  39. package/dist/storage/adapter.d.ts +9 -0
  40. package/dist/storage/adapter.js +13 -1
  41. package/dist/storage/browser-azure.d.ts +1 -1
  42. package/dist/storage/browser-azure.js +4 -0
  43. package/dist/storage/browser-cloud.d.ts +1 -1
  44. package/dist/storage/browser-cloud.js +7 -0
  45. package/dist/storage/presign.d.ts +13 -0
  46. package/dist/storage/presign.js +55 -0
  47. package/dist/storage/providers.d.ts +53 -0
  48. package/dist/storage/providers.js +171 -0
  49. package/dist/stores/browser.svelte.d.ts +2 -0
  50. package/dist/stores/browser.svelte.js +17 -1
  51. package/dist/stores/files.svelte.d.ts +1 -2
  52. package/dist/stores/files.svelte.js +1 -2
  53. package/dist/stores/tabs.svelte.d.ts +9 -2
  54. package/dist/stores/tabs.svelte.js +11 -2
  55. package/dist/types.d.ts +11 -0
  56. package/dist/utils/cog.d.ts +244 -0
  57. package/dist/utils/cog.js +1039 -0
  58. package/dist/utils/deck.d.ts +0 -18
  59. package/dist/utils/deck.js +0 -36
  60. package/dist/utils/geometry-type.d.ts +52 -0
  61. package/dist/utils/geometry-type.js +76 -0
  62. package/dist/utils/markdown-sql.d.ts +1 -1
  63. package/dist/utils/markdown-sql.js +3 -4
  64. package/dist/utils/pmtiles-tile.d.ts +0 -2
  65. package/dist/utils/pmtiles-tile.js +0 -8
  66. package/dist/utils/url-state.d.ts +6 -0
  67. package/dist/utils/url-state.js +34 -26
  68. package/dist/utils/url.d.ts +26 -9
  69. package/dist/utils/url.js +52 -25
  70. package/dist/utils/wkb.js +22 -8
  71. package/dist/utils/zarr-tab.d.ts +22 -0
  72. package/dist/utils/zarr-tab.js +30 -0
  73. package/dist/utils/zarr.d.ts +0 -2
  74. package/dist/utils/zarr.js +73 -44
  75. package/package.json +47 -43
  76. package/dist/components/ui/tabs/index.d.ts +0 -5
  77. package/dist/components/ui/tabs/index.js +0 -7
  78. package/dist/components/ui/tabs/tabs-content.svelte +0 -17
  79. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
  80. package/dist/components/ui/tabs/tabs-list.svelte +0 -16
  81. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
  82. package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
  83. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
  84. package/dist/components/ui/tabs/tabs.svelte +0 -19
  85. package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
  86. package/dist/components/viewers/MapViewer.svelte +0 -234
  87. package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
  88. package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
  89. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
@@ -33,24 +33,6 @@ export declare function loadDeckModules(): Promise<{
33
33
  MapboxOverlay: typeof import("@deck.gl/mapbox").MapboxOverlay;
34
34
  GeoJsonLayer: typeof import("@deck.gl/layers").GeoJsonLayer;
35
35
  }>;
36
- export interface DeckOverlayOptions {
37
- layerId: string;
38
- data: GeoJSON.FeatureCollection;
39
- /** Called with properties and the full GeoJSON Feature (for selection highlight). */
40
- onClick?: (properties: Record<string, any>, feature: GeoJSON.Feature) => void;
41
- /** Layer-level onHover — use hoverCursor(map) to toggle pointer on MapLibre canvas. */
42
- onHover?: (info: {
43
- picked?: boolean;
44
- }) => void;
45
- }
46
- /**
47
- * Create a MapboxOverlay with a single GeoJsonLayer.
48
- * Colors are assigned per-feature based on geometry type.
49
- */
50
- export declare function createDeckOverlay(modules: {
51
- MapboxOverlay: any;
52
- GeoJsonLayer: any;
53
- }, options: DeckOverlayOptions): any;
54
36
  /** Lazy-load GeoArrow deck.gl layers + MapboxOverlay + GeoJsonLayer (for selection). */
55
37
  export declare function loadGeoArrowModules(): Promise<{
56
38
  GeoArrowArcLayer: typeof import("@geoarrow/deck.gl-layers").GeoArrowArcLayer;
@@ -46,42 +46,6 @@ export async function loadDeckModules() {
46
46
  ]);
47
47
  return { MapboxOverlay, GeoJsonLayer };
48
48
  }
49
- /**
50
- * Create a MapboxOverlay with a single GeoJsonLayer.
51
- * Colors are assigned per-feature based on geometry type.
52
- */
53
- export function createDeckOverlay(modules, options) {
54
- const { MapboxOverlay, GeoJsonLayer } = modules;
55
- const { layerId, data, onClick, onHover } = options;
56
- return new MapboxOverlay({
57
- interleaved: false,
58
- layers: [
59
- new GeoJsonLayer({
60
- id: layerId,
61
- data,
62
- pickable: true,
63
- stroked: true,
64
- filled: true,
65
- pointType: 'circle',
66
- getFillColor: geojsonFillColor,
67
- getLineColor: geojsonLineColor,
68
- getPointRadius: 6,
69
- getLineWidth: 2.5,
70
- lineWidthMinPixels: 1.5,
71
- pointRadiusMinPixels: 4,
72
- pointRadiusMaxPixels: 12,
73
- autoHighlight: true,
74
- highlightColor: [255, 255, 255, 100],
75
- onHover,
76
- onClick: (info) => {
77
- if (info.object?.properties && onClick) {
78
- onClick({ ...info.object.properties }, info.object);
79
- }
80
- }
81
- })
82
- ]
83
- });
84
- }
85
49
  // ─── GeoArrow overlay (GeoParquetMapViewer) ──────────────────────────
86
50
  /** Lazy-load GeoArrow deck.gl layers + MapboxOverlay + GeoJsonLayer (for selection). */
87
51
  export async function loadGeoArrowModules() {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Helpers for parsing DuckDB v1.5 parameterized GEOMETRY type strings.
3
+ *
4
+ * DuckDB v1.5 made GEOMETRY a core type with an optional CRS parameter:
5
+ * GEOMETRY — no CRS attached
6
+ * GEOMETRY('EPSG:4326') — EPSG form
7
+ * GEOMETRY('OGC:CRS84') — OGC form (canonical for GeoParquet 1.1+)
8
+ * GEOMETRY('EPSG:27700') — projected CRS
9
+ *
10
+ * Type strings may come from `DESCRIBE`, from the Arrow schema, or from a
11
+ * legacy code path that still reports `BLOB`. Use these helpers everywhere
12
+ * instead of ad-hoc regex so behaviour stays consistent.
13
+ */
14
+ export interface GeometryTypeInfo {
15
+ /** True if the type is some form of GEOMETRY (with or without CRS). */
16
+ isGeometry: boolean;
17
+ /** True if the type carries a CRS parameter, e.g. GEOMETRY('EPSG:4326'). */
18
+ hasCrs: boolean;
19
+ /** The CRS string if present, otherwise null. Raw value, including WGS84. */
20
+ rawCrs: string | null;
21
+ /**
22
+ * The CRS string if present and NOT a WGS84 variant (EPSG:4326, EPSG:4979,
23
+ * OGC:CRS84). Returns null for WGS84 so callers can skip ST_Transform.
24
+ */
25
+ nonWgs84Crs: string | null;
26
+ }
27
+ /**
28
+ * Parse a DuckDB type string and report whether it is a GEOMETRY type, and
29
+ * whether a CRS parameter is attached.
30
+ */
31
+ export declare function parseGeometryTypeCrs(typeStr: string | null | undefined): GeometryTypeInfo;
32
+ /** True for EPSG:4326, EPSG:4979, OGC:CRS84 and equivalent strings. */
33
+ export declare function isWgs84Crs(crs: string | null | undefined): boolean;
34
+ /**
35
+ * Build a `ST_Transform(...)` SQL expression choosing the 2-arg form when the
36
+ * input already carries its CRS in the GEOMETRY type (DuckDB v1.5), and the
37
+ * 3-arg form otherwise.
38
+ *
39
+ * `geometry_always_xy` is set globally at DB init, so no per-call `always_xy`
40
+ * argument is needed.
41
+ */
42
+ export declare function buildTransformExpr(innerExpr: string, sourceType: string, sourceCrs: string, targetCrs: string): string;
43
+ /**
44
+ * Wrap a bare WKB expression with `ST_SetCRS(ST_GeomFromWKB(...))` so that the
45
+ * resulting GEOMETRY value carries a CRS through the rest of the pipeline.
46
+ * Used in the legacy GeoParquet fallback where we read the geometry column as
47
+ * BLOB but still know the source CRS from hyparquet metadata or the GeoParquet
48
+ * footer.
49
+ *
50
+ * If `sourceCrs` is null/empty, returns a plain `ST_GeomFromWKB(...)`.
51
+ */
52
+ export declare function wrapWkbWithCrs(wkbExpr: string, sourceCrs: string | null | undefined): string;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Helpers for parsing DuckDB v1.5 parameterized GEOMETRY type strings.
3
+ *
4
+ * DuckDB v1.5 made GEOMETRY a core type with an optional CRS parameter:
5
+ * GEOMETRY — no CRS attached
6
+ * GEOMETRY('EPSG:4326') — EPSG form
7
+ * GEOMETRY('OGC:CRS84') — OGC form (canonical for GeoParquet 1.1+)
8
+ * GEOMETRY('EPSG:27700') — projected CRS
9
+ *
10
+ * Type strings may come from `DESCRIBE`, from the Arrow schema, or from a
11
+ * legacy code path that still reports `BLOB`. Use these helpers everywhere
12
+ * instead of ad-hoc regex so behaviour stays consistent.
13
+ */
14
+ import { WGS84_CODES } from '../constants.js';
15
+ const GEOMETRY_PREFIX = /^GEOMETRY(\s*\(\s*'?([^')]+)'?\s*\))?/i;
16
+ /**
17
+ * Parse a DuckDB type string and report whether it is a GEOMETRY type, and
18
+ * whether a CRS parameter is attached.
19
+ */
20
+ export function parseGeometryTypeCrs(typeStr) {
21
+ if (!typeStr)
22
+ return { isGeometry: false, hasCrs: false, rawCrs: null, nonWgs84Crs: null };
23
+ const match = typeStr.match(GEOMETRY_PREFIX);
24
+ if (!match)
25
+ return { isGeometry: false, hasCrs: false, rawCrs: null, nonWgs84Crs: null };
26
+ const rawCrs = match[2]?.trim() ?? null;
27
+ if (!rawCrs)
28
+ return { isGeometry: true, hasCrs: false, rawCrs: null, nonWgs84Crs: null };
29
+ return {
30
+ isGeometry: true,
31
+ hasCrs: true,
32
+ rawCrs,
33
+ nonWgs84Crs: isWgs84Crs(rawCrs) ? null : rawCrs
34
+ };
35
+ }
36
+ /** True for EPSG:4326, EPSG:4979, OGC:CRS84 and equivalent strings. */
37
+ export function isWgs84Crs(crs) {
38
+ if (!crs)
39
+ return true;
40
+ const trimmed = crs.trim();
41
+ if (trimmed === 'OGC:CRS84' || trimmed === 'OGC:CRS83')
42
+ return true;
43
+ const epsgMatch = trimmed.match(/^EPSG:(\d+)$/i);
44
+ if (epsgMatch && WGS84_CODES.has(Number(epsgMatch[1])))
45
+ return true;
46
+ return false;
47
+ }
48
+ /**
49
+ * Build a `ST_Transform(...)` SQL expression choosing the 2-arg form when the
50
+ * input already carries its CRS in the GEOMETRY type (DuckDB v1.5), and the
51
+ * 3-arg form otherwise.
52
+ *
53
+ * `geometry_always_xy` is set globally at DB init, so no per-call `always_xy`
54
+ * argument is needed.
55
+ */
56
+ export function buildTransformExpr(innerExpr, sourceType, sourceCrs, targetCrs) {
57
+ const info = parseGeometryTypeCrs(sourceType);
58
+ if (info.hasCrs) {
59
+ return `ST_Transform(${innerExpr}, '${targetCrs}')`;
60
+ }
61
+ return `ST_Transform(${innerExpr}, '${sourceCrs}', '${targetCrs}')`;
62
+ }
63
+ /**
64
+ * Wrap a bare WKB expression with `ST_SetCRS(ST_GeomFromWKB(...))` so that the
65
+ * resulting GEOMETRY value carries a CRS through the rest of the pipeline.
66
+ * Used in the legacy GeoParquet fallback where we read the geometry column as
67
+ * BLOB but still know the source CRS from hyparquet metadata or the GeoParquet
68
+ * footer.
69
+ *
70
+ * If `sourceCrs` is null/empty, returns a plain `ST_GeomFromWKB(...)`.
71
+ */
72
+ export function wrapWkbWithCrs(wkbExpr, sourceCrs) {
73
+ if (!sourceCrs)
74
+ return `ST_GeomFromWKB(${wkbExpr})`;
75
+ return `ST_SetCRS(ST_GeomFromWKB(${wkbExpr}), '${sourceCrs}')`;
76
+ }
@@ -17,7 +17,7 @@ export interface ParsedMarkdownDocument {
17
17
  * SELECT * FROM table
18
18
  * ```
19
19
  */
20
- export declare function parseMarkdownDocument(markdown: string): ParsedMarkdownDocument;
20
+ export declare function parseMarkdownDocument(markdown: string): Promise<ParsedMarkdownDocument>;
21
21
  /**
22
22
  * Interpolate template variables in markdown text.
23
23
  * Supports {queryName.rows[0].columnName} syntax.
@@ -1,4 +1,3 @@
1
- import YAML from 'yaml';
2
1
  /**
3
2
  * Parse a markdown document with YAML frontmatter and SQL code blocks.
4
3
  *
@@ -7,17 +6,17 @@ import YAML from 'yaml';
7
6
  * SELECT * FROM table
8
7
  * ```
9
8
  */
10
- export function parseMarkdownDocument(markdown) {
9
+ export async function parseMarkdownDocument(markdown) {
11
10
  let frontmatter = {};
12
11
  let content = markdown;
13
- // Extract YAML frontmatter
14
12
  const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---\n/);
15
13
  if (fmMatch) {
16
14
  try {
15
+ const { default: YAML } = await import('yaml');
17
16
  frontmatter = YAML.parse(fmMatch[1]) || {};
18
17
  }
19
18
  catch {
20
- // Invalid YAML ignore
19
+ // Invalid YAML or yaml peer dep not installed, ignore frontmatter
21
20
  }
22
21
  content = markdown.slice(fmMatch[0].length);
23
22
  }
@@ -30,8 +30,6 @@ export interface DecodedFeature {
30
30
  * Returns null if the tile does not exist or for raster archives.
31
31
  */
32
32
  export declare function decodeMvtTile(pmtiles: PMTiles, z: number, x: number, y: number): Promise<DecodedTile | null>;
33
- /** Convert tile bytes to a Blob URL for raster tile preview. */
34
- export declare function tileToImageUrl(pmtiles: PMTiles, z: number, x: number, y: number, mimeType: string): Promise<string | null>;
35
33
  /** MIME type for a PMTiles tile format string. */
36
34
  export declare function tileMimeType(format: string): string;
37
35
  /** Compute the hue for layer index i (same palette as buildPmtilesLayers). */
@@ -41,14 +41,6 @@ export async function decodeMvtTile(pmtiles, z, x, y) {
41
41
  }
42
42
  return { z, x, y, layers, rawSize };
43
43
  }
44
- /** Convert tile bytes to a Blob URL for raster tile preview. */
45
- export async function tileToImageUrl(pmtiles, z, x, y, mimeType) {
46
- const resp = await pmtiles.getZxy(z, x, y);
47
- if (!resp)
48
- return null;
49
- const blob = new Blob([resp.data], { type: mimeType });
50
- return URL.createObjectURL(blob);
51
- }
52
44
  /** MIME type for a PMTiles tile format string. */
53
45
  export function tileMimeType(format) {
54
46
  const map = {
@@ -34,6 +34,12 @@ export declare function getUrlView(): string;
34
34
  * Read the prefix (file/folder path) from the ?url= param.
35
35
  */
36
36
  export declare function getUrlPrefix(): string;
37
+ /**
38
+ * True when a `?url=` param is present. Single source of truth, used by
39
+ * the tab-sync effect and Sidebar auto-detection to decide whether an
40
+ * auto-migration is in progress (see `+page.svelte` tab-sync effect).
41
+ */
42
+ export declare function hasUrlParam(): boolean;
37
43
  /**
38
44
  * Clear all URL state params.
39
45
  */
@@ -28,43 +28,43 @@ export function buildUrlParam(conn, prefix) {
28
28
  return `${base}/${prefix.replace(/^\//, '')}`;
29
29
  }
30
30
  /**
31
- * Set the ?url= param to a raw URL string (for direct URL tabs).
31
+ * Apply a URL mutation, skipping `replaceState` if nothing changed.
32
+ * Every public mutator below funnels through this to avoid `replaceState`
33
+ * thrash when the tab-sync effect re-fires on unrelated reactive changes.
32
34
  */
33
- export function setRawUrlParam(rawUrl) {
35
+ function writeLocation(mutate) {
34
36
  try {
35
37
  const url = new URL(window.location.href);
36
- url.searchParams.set('url', rawUrl);
37
- replaceState(url.pathname + url.search + url.hash, {});
38
+ const before = url.pathname + url.search + url.hash;
39
+ mutate(url);
40
+ const after = url.pathname + url.search + url.hash;
41
+ if (before === after)
42
+ return;
43
+ replaceState(after, {});
38
44
  }
39
45
  catch {
40
46
  /* ignore */
41
47
  }
42
48
  }
49
+ /**
50
+ * Set the ?url= param to a raw URL string (for direct URL tabs).
51
+ */
52
+ export function setRawUrlParam(rawUrl) {
53
+ writeLocation((url) => url.searchParams.set('url', rawUrl));
54
+ }
43
55
  /**
44
56
  * Sync the ?url= param in the browser URL.
45
57
  */
46
58
  export function syncUrlParam(conn, prefix) {
47
- try {
48
- const url = new URL(window.location.href);
49
- url.searchParams.set('url', buildUrlParam(conn, prefix));
50
- replaceState(url.pathname + url.search + url.hash, {});
51
- }
52
- catch {
53
- /* ignore */
54
- }
59
+ writeLocation((url) => url.searchParams.set('url', buildUrlParam(conn, prefix)));
55
60
  }
56
61
  /**
57
62
  * Update the #hash in the URL to reflect the current view mode.
58
63
  */
59
64
  export function updateUrlView(view) {
60
- try {
61
- const url = new URL(window.location.href);
65
+ writeLocation((url) => {
62
66
  url.hash = view || '';
63
- replaceState(url.pathname + url.search + url.hash, {});
64
- }
65
- catch {
66
- /* ignore */
67
- }
67
+ });
68
68
  }
69
69
  /**
70
70
  * Read the current #hash view mode from the URL.
@@ -93,16 +93,24 @@ export function getUrlPrefix() {
93
93
  }
94
94
  }
95
95
  /**
96
- * Clear all URL state params.
96
+ * True when a `?url=` param is present. Single source of truth, used by
97
+ * the tab-sync effect and Sidebar auto-detection to decide whether an
98
+ * auto-migration is in progress (see `+page.svelte` tab-sync effect).
97
99
  */
98
- export function clearUrlState() {
100
+ export function hasUrlParam() {
99
101
  try {
100
- const url = new URL(window.location.href);
101
- url.searchParams.delete('url');
102
- url.hash = '';
103
- replaceState(url.pathname + url.search, {});
102
+ return new URL(window.location.href).searchParams.has('url');
104
103
  }
105
104
  catch {
106
- /* ignore */
105
+ return false;
107
106
  }
108
107
  }
108
+ /**
109
+ * Clear all URL state params.
110
+ */
111
+ export function clearUrlState() {
112
+ writeLocation((url) => {
113
+ url.searchParams.delete('url');
114
+ url.hash = '';
115
+ });
116
+ }
@@ -4,22 +4,39 @@ import type { Tab } from '../types.js';
4
4
  * Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
5
5
  */
6
6
  export declare function buildHttpsUrl(tab: Tab): string;
7
+ /**
8
+ * Async counterpart of `buildHttpsUrl`. For `signed-s3` connections, returns a
9
+ * presigned HTTPS URL (SigV4 query-string auth). For public or SAS connections
10
+ * it returns the same URL as the sync version.
11
+ */
12
+ export declare function buildHttpsUrlAsync(tab: Tab, expiresIn?: number): Promise<string>;
7
13
  /**
8
14
  * Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
9
15
  */
10
16
  export declare function buildStorageUrl(tab: Tab): string;
11
17
  /**
12
- * Build the URL that DuckDB should use for queries.
13
- * - Azure: always HTTPS URL with SAS token appended
14
- * - Anonymous with custom endpoint (Storj, R2, etc.): HTTPS URL — no S3 config needed,
15
- * avoids endpoint/auth complexity, works directly via httpfs
16
- * - AWS S3 (no endpoint): s3:// DuckDB routes via configured region
17
- * - Authenticated with endpoint: s3:// needs S3 endpoint config for SigV4 signing
18
+ * Build the URL DuckDB should query. Derived from the connection's access mode:
19
+ *
20
+ * | Access mode | DuckDB URL | Why |
21
+ * |-----------------|-----------------------------------|-------------------------------------------|
22
+ * | `sas-https` | HTTPS with SAS token | No DuckDB Azure support; SAS in URL works |
23
+ * | `public-https` | HTTPS (no auth) | httpfs fetches directly, no signing needed|
24
+ * | `signed-s3` | `s3://bucket/key` | DuckDB signs with configured S3 settings |
25
+ *
26
+ * Path is percent-decoded so DuckDB's httpfs doesn't double-encode
27
+ * (e.g. Arabic filenames `%D9%85` → `%25D9%2585`).
18
28
  */
19
29
  export declare function buildDuckDbUrl(tab: Tab): string;
20
30
  /**
21
- * Check if a tab's file can be loaded directly via HTTPS URL (streaming).
22
- * True for URL-sourced tabs, anonymous buckets, and Azure (SAS token in URL).
23
- * False for authenticated S3 (needs signed URLs or blob download via adapter).
31
+ * Async counterpart of `buildDuckDbUrl`. Returns a presigned HTTPS URL for
32
+ * `signed-s3` connections so DuckDB httpfs can fetch with `Range` only, no
33
+ * `Authorization` preflight (which breaks on GCS's S3-compat endpoint when
34
+ * the bucket CORS `responseHeader` list desyncs from the browser's request).
35
+ */
36
+ export declare function buildDuckDbUrlAsync(tab: Tab, expiresIn?: number): Promise<string>;
37
+ /**
38
+ * True when any HTTP client (fetch/img/video/deck.gl/COG/Zarr/PMTiles) can
39
+ * load the tab's file directly via its HTTPS URL. False when SigV4 signing
40
+ * is required and the viewer must go through the storage adapter instead.
24
41
  */
25
42
  export declare function canStreamDirectly(tab: Tab): boolean;
package/dist/utils/url.js CHANGED
@@ -1,4 +1,5 @@
1
- import { buildProviderBaseUrl } from '../storage/providers.js';
1
+ import { presignHttpsUrl } from '../storage/presign.js';
2
+ import { buildProviderBaseUrl, isPubliclyStreamable } from '../storage/providers.js';
2
3
  import { connections } from '../stores/connections.svelte.js';
3
4
  import { credentialStore } from '../stores/credentials.svelte.js';
4
5
  import { getNativeScheme, safeDecodeURIComponent } from './cloud-url.js';
@@ -20,6 +21,15 @@ export function buildHttpsUrl(tab) {
20
21
  }
21
22
  return `${buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region)}/${cleanPath}`;
22
23
  }
24
+ /**
25
+ * Async counterpart of `buildHttpsUrl`. For `signed-s3` connections, returns a
26
+ * presigned HTTPS URL (SigV4 query-string auth). For public or SAS connections
27
+ * it returns the same URL as the sync version.
28
+ */
29
+ export async function buildHttpsUrlAsync(tab, expiresIn) {
30
+ const presigned = await tryPresignTab(tab, expiresIn);
31
+ return presigned ?? buildHttpsUrl(tab);
32
+ }
23
33
  /**
24
34
  * Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
25
35
  */
@@ -31,37 +41,58 @@ export function buildStorageUrl(tab) {
31
41
  return `${scheme}://${conn.bucket}/${tab.path.replace(/^\//, '')}`;
32
42
  }
33
43
  /**
34
- * Build the URL that DuckDB should use for queries.
35
- * - Azure: always HTTPS URL with SAS token appended
36
- * - Anonymous with custom endpoint (Storj, R2, etc.): HTTPS URL — no S3 config needed,
37
- * avoids endpoint/auth complexity, works directly via httpfs
38
- * - AWS S3 (no endpoint): s3:// DuckDB routes via configured region
39
- * - Authenticated with endpoint: s3:// needs S3 endpoint config for SigV4 signing
44
+ * Build the URL DuckDB should query. Derived from the connection's access mode:
45
+ *
46
+ * | Access mode | DuckDB URL | Why |
47
+ * |-----------------|-----------------------------------|-------------------------------------------|
48
+ * | `sas-https` | HTTPS with SAS token | No DuckDB Azure support; SAS in URL works |
49
+ * | `public-https` | HTTPS (no auth) | httpfs fetches directly, no signing needed|
50
+ * | `signed-s3` | `s3://bucket/key` | DuckDB signs with configured S3 settings |
51
+ *
52
+ * Path is percent-decoded so DuckDB's httpfs doesn't double-encode
53
+ * (e.g. Arabic filenames `%D9%85` → `%25D9%2585`).
40
54
  */
41
55
  export function buildDuckDbUrl(tab) {
42
56
  const conn = tab.connectionId ? connections.getById(tab.connectionId) : null;
43
57
  if (!conn)
44
58
  return tab.path;
45
- // Azure always uses HTTPS (DuckDB doesn't have native Azure Blob support)
46
- if (conn.provider === 'azure') {
59
+ if (isPubliclyStreamable(conn))
47
60
  return buildHttpsUrl(tab);
48
- }
49
- // Anonymous connections with custom endpoints (Storj, R2, Wasabi, etc.)
50
- // use HTTPS directly — simpler and avoids S3 endpoint configuration.
51
- if (conn.anonymous && conn.endpoint) {
52
- return buildHttpsUrl(tab);
53
- }
54
- // S3-compatible with credentials: use s3:// protocol so DuckDB uses its
55
- // configured S3 settings (region, endpoint, url_style) for SigV4 signing.
56
61
  // Decode percent-encoded paths (e.g. Arabic filenames) so DuckDB's httpfs
57
62
  // doesn't double-encode them (%D9%85 → %25D9%2585).
58
63
  const rawPath = safeDecodeURIComponent(tab.path.replace(/^\//, ''));
59
64
  return `s3://${conn.bucket}/${rawPath}`;
60
65
  }
61
66
  /**
62
- * Check if a tab's file can be loaded directly via HTTPS URL (streaming).
63
- * True for URL-sourced tabs, anonymous buckets, and Azure (SAS token in URL).
64
- * False for authenticated S3 (needs signed URLs or blob download via adapter).
67
+ * Async counterpart of `buildDuckDbUrl`. Returns a presigned HTTPS URL for
68
+ * `signed-s3` connections so DuckDB httpfs can fetch with `Range` only, no
69
+ * `Authorization` preflight (which breaks on GCS's S3-compat endpoint when
70
+ * the bucket CORS `responseHeader` list desyncs from the browser's request).
71
+ */
72
+ export async function buildDuckDbUrlAsync(tab, expiresIn) {
73
+ const presigned = await tryPresignTab(tab, expiresIn);
74
+ return presigned ?? buildDuckDbUrl(tab);
75
+ }
76
+ /** Presign the tab's HTTPS URL for `signed-s3` connections; null otherwise. */
77
+ async function tryPresignTab(tab, expiresIn) {
78
+ const conn = tab.connectionId ? connections.getById(tab.connectionId) : null;
79
+ if (!conn || isPubliclyStreamable(conn))
80
+ return null;
81
+ try {
82
+ return await presignHttpsUrl(conn, tab.path, expiresIn);
83
+ }
84
+ catch (err) {
85
+ // Silent fallback would route the caller back to `s3://...` + SigV4
86
+ // header signing — exactly the CORS preflight path presigning was added
87
+ // to avoid. Surface the failure so it is debuggable.
88
+ console.warn('[presign] falling back to signed-s3 path:', err);
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * True when any HTTP client (fetch/img/video/deck.gl/COG/Zarr/PMTiles) can
94
+ * load the tab's file directly via its HTTPS URL. False when SigV4 signing
95
+ * is required and the viewer must go through the storage adapter instead.
65
96
  */
66
97
  export function canStreamDirectly(tab) {
67
98
  if (tab.source === 'url')
@@ -69,11 +100,7 @@ export function canStreamDirectly(tab) {
69
100
  const conn = tab.connectionId ? connections.getById(tab.connectionId) : null;
70
101
  if (!conn)
71
102
  return true;
72
- if (conn.anonymous)
73
- return true;
74
- if (conn.provider === 'azure')
75
- return true;
76
- return false;
103
+ return isPubliclyStreamable(conn);
77
104
  }
78
105
  /**
79
106
  * Append Azure SAS token to a URL if available.
package/dist/utils/wkb.js CHANGED
@@ -190,8 +190,22 @@ const GEO_TYPE_KEYWORDS = [
190
190
  'geometrycollection',
191
191
  'sdo_geometry'
192
192
  ];
193
- /** Substrings in column names that hint at geometry content. */
194
- const GEO_NAME_HINTS = ['geom', 'geometry', 'geo_', '_geo', 'wkb', 'wkt', 'shape', 'spatial'];
193
+ /**
194
+ * Tokens in column names that hint at geometry content. Matched against
195
+ * snake_case / kebab-case / camelCase tokens only — never as loose substrings
196
+ * (e.g. `_geographic_` must not match `geo` via the `_geo` substring, because
197
+ * count columns like `n_geographic_entities` are INT, not geometry).
198
+ */
199
+ const GEO_NAME_HINTS = ['geom', 'geometry', 'wkb', 'wkt', 'shape', 'spatial', 'geo'];
200
+ /** Split a column name into lowercase tokens for hint matching. */
201
+ function tokenizeColumnName(name) {
202
+ return name
203
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
204
+ .replace(/([a-z])([0-9])/g, '$1_$2')
205
+ .toLowerCase()
206
+ .split(/[^a-z0-9]+/)
207
+ .filter(Boolean);
208
+ }
195
209
  /** Valid GeoJSON geometry type names. */
196
210
  const GEOJSON_TYPES = [
197
211
  'Point',
@@ -242,18 +256,18 @@ export function findGeoColumn(schema) {
242
256
  if (GEO_NAMES.includes(f.name.toLowerCase()))
243
257
  return f.name;
244
258
  }
245
- // Priority 4: name contains geo hint with binary type
259
+ // Priority 4: name token matches geo hint with binary type
246
260
  for (const f of schema) {
247
- const n = f.name.toLowerCase();
261
+ const tokens = tokenizeColumnName(f.name);
248
262
  const t = f.type.toLowerCase();
249
263
  const isBinary = t.includes('blob') || t.includes('binary') || t.includes('bytea');
250
- if (isBinary && GEO_NAME_HINTS.some((hint) => n.includes(hint)))
264
+ if (isBinary && tokens.some((tok) => GEO_NAME_HINTS.includes(tok)))
251
265
  return f.name;
252
266
  }
253
- // Priority 5: name contains geo hint, any type
267
+ // Priority 5: name token matches geo hint, any type
254
268
  for (const f of schema) {
255
- const n = f.name.toLowerCase();
256
- if (GEO_NAME_HINTS.some((hint) => n.includes(hint)))
269
+ const tokens = tokenizeColumnName(f.name);
270
+ if (tokens.some((tok) => GEO_NAME_HINTS.includes(tok)))
257
271
  return f.name;
258
272
  }
259
273
  return null;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Zarr tab-opening helper.
3
+ * Centralizes Zarr store tab creation to avoid duplicating the same logic
4
+ * across FileBrowser, FileRow, FileTreeSidebar, and +page.svelte.
5
+ *
6
+ * Kept separate from zarr.ts to avoid adding a store dependency to a pure utility.
7
+ */
8
+ interface ZarrTabContext {
9
+ /** 'remote' for object-storage connections, 'url' for direct URL tabs. */
10
+ source: 'remote' | 'url';
11
+ /** Connection ID — required for remote sources. */
12
+ connectionId?: string;
13
+ /** Fallback name when the path has no meaningful last segment (e.g. bucket root). */
14
+ bucketFallback?: string;
15
+ }
16
+ /**
17
+ * Open a directory path as a Zarr store tab.
18
+ * Normalizes the path, derives a display name, and deduplicates via a
19
+ * deterministic tab ID so calling this twice with the same path is a no-op.
20
+ */
21
+ export declare function openZarrTab(dirPath: string, ctx: ZarrTabContext): void;
22
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Zarr tab-opening helper.
3
+ * Centralizes Zarr store tab creation to avoid duplicating the same logic
4
+ * across FileBrowser, FileRow, FileTreeSidebar, and +page.svelte.
5
+ *
6
+ * Kept separate from zarr.ts to avoid adding a store dependency to a pure utility.
7
+ */
8
+ import { tabs } from '../stores/tabs.svelte.js';
9
+ /**
10
+ * Open a directory path as a Zarr store tab.
11
+ * Normalizes the path, derives a display name, and deduplicates via a
12
+ * deterministic tab ID so calling this twice with the same path is a no-op.
13
+ */
14
+ export function openZarrTab(dirPath, ctx) {
15
+ const path = ctx.source === 'remote'
16
+ ? dirPath.endsWith('/')
17
+ ? dirPath
18
+ : `${dirPath}/`
19
+ : dirPath.replace(/\/+$/, '');
20
+ const name = path.replace(/\/+$/, '').split('/').pop()?.split('?')[0] || ctx.bucketFallback || 'zarr';
21
+ const id = ctx.connectionId ? `${ctx.connectionId}:${path}` : `url:${path}`;
22
+ tabs.open({
23
+ id,
24
+ name,
25
+ path,
26
+ source: ctx.source,
27
+ ...(ctx.connectionId ? { connectionId: ctx.connectionId } : {}),
28
+ extension: 'zarr'
29
+ });
30
+ }
@@ -62,8 +62,6 @@ export declare function computeUncompressed(shape: number[] | undefined, dtype:
62
62
  export declare function formatCodecs(node: ZarrNode): string | null;
63
63
  /** Format chunk_key_encoding for display: `"default (sep: "/")"` */
64
64
  export declare function formatChunkKeys(node: ZarrNode): string | null;
65
- /** Find a node by slash-delimited path. */
66
- export declare function findNodeByPath(root: ZarrNode, path: string): ZarrNode | null;
67
65
  /** Build tree from Zarr v3 consolidated metadata (zarr.json). */
68
66
  export declare function buildV3Tree(data: any): ZarrHierarchy;
69
67
  /** Build tree from Zarr v2 consolidated metadata (.zmetadata). */