@walkthru-earth/objex 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/components/browser/FileBrowser.svelte +25 -14
- package/dist/components/browser/FileTreeSidebar.svelte +42 -6
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +43 -25
- package/dist/components/viewers/CodeViewer.svelte +23 -0
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +353 -1160
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/TableViewer.svelte +123 -41
- package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
- package/dist/components/viewers/ZarrViewer.svelte +1 -4
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +6 -2
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +12 -2
- package/dist/i18n/ar.js +24 -0
- package/dist/i18n/en.js +24 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +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 +30 -0
- package/dist/query/source.js +37 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +138 -85
- package/dist/storage/providers.d.ts +47 -0
- package/dist/storage/providers.js +160 -0
- package/dist/stores/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 +13 -9
- package/dist/utils/url.js +16 -25
- 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
package/dist/utils/zarr.js
CHANGED
|
@@ -238,20 +238,6 @@ export function formatCodecs(node) {
|
|
|
238
238
|
export function formatChunkKeys(node) {
|
|
239
239
|
return node.chunkKeyEncoding ?? null;
|
|
240
240
|
}
|
|
241
|
-
/** Find a node by slash-delimited path. */
|
|
242
|
-
export function findNodeByPath(root, path) {
|
|
243
|
-
if (path === '/' || path === '')
|
|
244
|
-
return root;
|
|
245
|
-
const parts = path.replace(/^\//, '').split('/');
|
|
246
|
-
let current = root;
|
|
247
|
-
for (const part of parts) {
|
|
248
|
-
const child = current.children.find((c) => c.name === part);
|
|
249
|
-
if (!child)
|
|
250
|
-
return null;
|
|
251
|
-
current = child;
|
|
252
|
-
}
|
|
253
|
-
return current;
|
|
254
|
-
}
|
|
255
241
|
// ---------------------------------------------------------------------------
|
|
256
242
|
// Tree builders
|
|
257
243
|
// ---------------------------------------------------------------------------
|
|
@@ -462,37 +448,14 @@ async function discoverV3Children(storeUrl, rootData, signal) {
|
|
|
462
448
|
candidates.add(String(entry.asset));
|
|
463
449
|
}
|
|
464
450
|
}
|
|
451
|
+
// If no candidates from conventions, discover via S3 XML listing
|
|
452
|
+
if (candidates.size === 0) {
|
|
453
|
+
const discovered = await listS3Children(storeUrl, signal);
|
|
454
|
+
for (const name of discovered)
|
|
455
|
+
candidates.add(name);
|
|
456
|
+
}
|
|
465
457
|
// Probe each candidate path for zarr.json
|
|
466
|
-
const probes = [...candidates].map(
|
|
467
|
-
try {
|
|
468
|
-
const res = await fetch(`${storeUrl}/${name}/zarr.json`, { signal });
|
|
469
|
-
if (!res.ok)
|
|
470
|
-
return null;
|
|
471
|
-
const data = await res.json();
|
|
472
|
-
if (data.node_type === 'array' && data.shape) {
|
|
473
|
-
const node = makeNode(`/${name}`, 'array', data.attributes ?? {});
|
|
474
|
-
node.shape = data.shape;
|
|
475
|
-
node.dtype = data.data_type ?? 'unknown';
|
|
476
|
-
node.dims = data.dimension_names ?? inferDims(name, data.shape);
|
|
477
|
-
node.chunks = data.chunk_grid?.configuration?.chunk_shape ?? [];
|
|
478
|
-
node.fillValue = data.fill_value;
|
|
479
|
-
node.codecs = data.codecs ?? [];
|
|
480
|
-
const cke = data.chunk_key_encoding;
|
|
481
|
-
if (cke) {
|
|
482
|
-
const sep = cke.configuration?.separator ?? '/';
|
|
483
|
-
node.chunkKeyEncoding = `${cke.name ?? 'default'} (sep: "${sep}")`;
|
|
484
|
-
}
|
|
485
|
-
return node;
|
|
486
|
-
}
|
|
487
|
-
if (data.node_type === 'group') {
|
|
488
|
-
return makeNode(`/${name}`, 'group', data.attributes ?? {});
|
|
489
|
-
}
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
catch {
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
});
|
|
458
|
+
const probes = [...candidates].map((name) => probeV3Child(storeUrl, name, signal));
|
|
496
459
|
const results = await Promise.all(probes);
|
|
497
460
|
for (const node of results) {
|
|
498
461
|
if (node) {
|
|
@@ -509,6 +472,72 @@ async function discoverV3Children(storeUrl, rootData, signal) {
|
|
|
509
472
|
spatialRefAttrs: null
|
|
510
473
|
};
|
|
511
474
|
}
|
|
475
|
+
/** Probe a single v3 child path and return a ZarrNode or null. */
|
|
476
|
+
async function probeV3Child(storeUrl, name, signal) {
|
|
477
|
+
try {
|
|
478
|
+
const res = await fetch(`${storeUrl}/${name}/zarr.json`, { signal });
|
|
479
|
+
if (!res.ok)
|
|
480
|
+
return null;
|
|
481
|
+
const data = await res.json();
|
|
482
|
+
if (data.node_type === 'array' && data.shape) {
|
|
483
|
+
const node = makeNode(`/${name}`, 'array', data.attributes ?? {});
|
|
484
|
+
node.shape = data.shape;
|
|
485
|
+
node.dtype = data.data_type ?? 'unknown';
|
|
486
|
+
node.dims = data.dimension_names ?? inferDims(name, data.shape);
|
|
487
|
+
node.chunks = data.chunk_grid?.configuration?.chunk_shape ?? [];
|
|
488
|
+
node.fillValue = data.fill_value;
|
|
489
|
+
node.codecs = data.codecs ?? [];
|
|
490
|
+
const cke = data.chunk_key_encoding;
|
|
491
|
+
if (cke) {
|
|
492
|
+
const sep = cke.configuration?.separator ?? '/';
|
|
493
|
+
node.chunkKeyEncoding = `${cke.name ?? 'default'} (sep: "${sep}")`;
|
|
494
|
+
}
|
|
495
|
+
return node;
|
|
496
|
+
}
|
|
497
|
+
if (data.node_type === 'group') {
|
|
498
|
+
return makeNode(`/${name}`, 'group', data.attributes ?? {});
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Discover child directory names via S3 XML listing.
|
|
508
|
+
* Parses the HTTPS store URL to extract bucket and prefix, then issues
|
|
509
|
+
* a `list-type=2&delimiter=/` request to find sub-prefixes.
|
|
510
|
+
* Returns directory names (without trailing slash) that are NOT hidden (no leading dot).
|
|
511
|
+
*/
|
|
512
|
+
async function listS3Children(storeUrl, signal) {
|
|
513
|
+
try {
|
|
514
|
+
const url = new URL(storeUrl);
|
|
515
|
+
// S3-style: bucket is first path segment, rest is prefix
|
|
516
|
+
const pathParts = url.pathname.replace(/^\//, '').split('/');
|
|
517
|
+
const bucket = pathParts[0];
|
|
518
|
+
const prefix = `${pathParts.slice(1).join('/').replace(/\/$/, '')}/`;
|
|
519
|
+
const listUrl = `${url.origin}/${bucket}/?list-type=2&prefix=${encodeURIComponent(prefix)}&delimiter=/`;
|
|
520
|
+
const res = await fetch(listUrl, { signal });
|
|
521
|
+
if (!res.ok)
|
|
522
|
+
return [];
|
|
523
|
+
const xml = await res.text();
|
|
524
|
+
const names = [];
|
|
525
|
+
// Parse <CommonPrefixes><Prefix>...</Prefix></CommonPrefixes>
|
|
526
|
+
const prefixRegex = /<Prefix>([^<]+)<\/Prefix>/g;
|
|
527
|
+
for (let match = prefixRegex.exec(xml); match; match = prefixRegex.exec(xml)) {
|
|
528
|
+
const fullPrefix = match[1];
|
|
529
|
+
// Strip parent prefix and trailing slash to get child name
|
|
530
|
+
const child = fullPrefix.slice(prefix.length).replace(/\/$/, '');
|
|
531
|
+
if (child && !child.startsWith('.')) {
|
|
532
|
+
names.push(child);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return names;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
512
541
|
/**
|
|
513
542
|
* Fallback: probe a Zarr store using zarrita when consolidated metadata is unavailable.
|
|
514
543
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkthru-earth/objex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Svelte 5 components and utilities for exploring geospatial object storage — S3, GCS, Azure, R2",
|
|
5
5
|
"author": "Youssef Harby <yharby@walkthru.earth>",
|
|
6
6
|
"license": "CC-BY-4.0",
|
|
@@ -123,81 +123,84 @@
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"devDependencies": {
|
|
126
|
-
"@biomejs/biome": "^2.4.
|
|
126
|
+
"@biomejs/biome": "^2.4.12",
|
|
127
127
|
"@changesets/changelog-github": "^0.5.2",
|
|
128
|
-
"@changesets/cli": "^2.
|
|
128
|
+
"@changesets/cli": "^2.31.0",
|
|
129
129
|
"@fontsource/cairo": "^5.2.7",
|
|
130
|
-
"@internationalized/date": "^3.12.
|
|
130
|
+
"@internationalized/date": "^3.12.1",
|
|
131
131
|
"@lucide/svelte": "^0.561.0",
|
|
132
132
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
133
|
-
"@sveltejs/kit": "^2.
|
|
133
|
+
"@sveltejs/kit": "^2.57.1",
|
|
134
134
|
"@sveltejs/package": "^2.5.7",
|
|
135
135
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
136
136
|
"@tailwindcss/forms": "^0.5.11",
|
|
137
137
|
"@tailwindcss/typography": "^0.5.19",
|
|
138
|
-
"@tailwindcss/vite": "^4.2.
|
|
139
|
-
"bits-ui": "^2.
|
|
138
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
139
|
+
"bits-ui": "^2.18.0",
|
|
140
140
|
"clsx": "^2.1.1",
|
|
141
|
-
"
|
|
141
|
+
"knip": "^6.6.0",
|
|
142
|
+
"lefthook": "^2.1.6",
|
|
142
143
|
"paneforge": "^1.0.2",
|
|
143
|
-
"posthog-js": "^1.
|
|
144
|
+
"posthog-js": "^1.369.5",
|
|
144
145
|
"publint": "^0.3.18",
|
|
145
|
-
"svelte": "^5.
|
|
146
|
-
"svelte-check": "^4.4.
|
|
146
|
+
"svelte": "^5.55.4",
|
|
147
|
+
"svelte-check": "^4.4.6",
|
|
147
148
|
"tailwind-merge": "^3.5.0",
|
|
148
149
|
"tailwind-variants": "^3.2.2",
|
|
149
|
-
"tailwindcss": "^4.2.
|
|
150
|
+
"tailwindcss": "^4.2.4",
|
|
150
151
|
"tw-animate-css": "^1.4.0",
|
|
151
152
|
"typescript": "^5.9.3",
|
|
152
|
-
"vite": "^7.3.
|
|
153
|
+
"vite": "^7.3.2"
|
|
153
154
|
},
|
|
154
155
|
"dependencies": {
|
|
155
|
-
"@babylonjs/core": "^8.
|
|
156
|
-
"@babylonjs/loaders": "^8.
|
|
157
|
-
"@carbonplan/zarr-layer": "^0.3
|
|
156
|
+
"@babylonjs/core": "^8.56.2",
|
|
157
|
+
"@babylonjs/loaders": "^8.56.2",
|
|
158
|
+
"@carbonplan/zarr-layer": "^0.4.3",
|
|
158
159
|
"@codemirror/autocomplete": "^6.20.1",
|
|
159
160
|
"@codemirror/commands": "^6.10.3",
|
|
160
161
|
"@codemirror/lang-sql": "^6.10.0",
|
|
161
|
-
"@codemirror/language": "^6.12.
|
|
162
|
+
"@codemirror/language": "^6.12.3",
|
|
162
163
|
"@codemirror/state": "^6.6.0",
|
|
163
164
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
164
|
-
"@codemirror/view": "^6.
|
|
165
|
-
"@deck.gl/core": "^9.
|
|
166
|
-
"@deck.gl/geo-layers": "^9.
|
|
167
|
-
"@deck.gl/layers": "^9.
|
|
168
|
-
"@deck.gl/mapbox": "^9.
|
|
169
|
-
"@deck.gl/mesh-layers": "^9.
|
|
170
|
-
"@developmentseed/deck.gl-geotiff": "^0.
|
|
171
|
-
"@
|
|
172
|
-
"@
|
|
173
|
-
"@
|
|
165
|
+
"@codemirror/view": "^6.41.1",
|
|
166
|
+
"@deck.gl/core": "^9.3.1",
|
|
167
|
+
"@deck.gl/geo-layers": "^9.3.1",
|
|
168
|
+
"@deck.gl/layers": "^9.3.1",
|
|
169
|
+
"@deck.gl/mapbox": "^9.3.1",
|
|
170
|
+
"@deck.gl/mesh-layers": "^9.3.1",
|
|
171
|
+
"@developmentseed/deck.gl-geotiff": "^0.5.0",
|
|
172
|
+
"@developmentseed/deck.gl-raster": "^0.5.0",
|
|
173
|
+
"@developmentseed/epsg": "^0.5.0",
|
|
174
|
+
"@developmentseed/geotiff": "^0.5.0",
|
|
175
|
+
"@developmentseed/proj": "^0.5.0",
|
|
176
|
+
"@duckdb/duckdb-wasm": "1.33.1-dev53.0",
|
|
177
|
+
"@geoarrow/deck.gl-layers": "^0.3.2",
|
|
178
|
+
"@luma.gl/core": "^9.3.3",
|
|
174
179
|
"@mapbox/vector-tile": "^2.0.4",
|
|
175
|
-
"@milkdown/kit": "^7.
|
|
176
|
-
"@milkdown/theme-nord": "^7.
|
|
180
|
+
"@milkdown/kit": "^7.20.0",
|
|
181
|
+
"@milkdown/theme-nord": "^7.20.0",
|
|
177
182
|
"@repeaterjs/repeater": "^3.0.6",
|
|
178
|
-
"@zip.js/zip.js": "^2.8.
|
|
183
|
+
"@zip.js/zip.js": "^2.8.26",
|
|
179
184
|
"ansi_up": "^6.0.6",
|
|
180
185
|
"apache-arrow": "^21.1.0",
|
|
181
186
|
"aws4fetch": "^1.0.20",
|
|
182
187
|
"chart.js": "^4.5.1",
|
|
183
|
-
"deck.gl": "^9.
|
|
188
|
+
"deck.gl": "^9.3.1",
|
|
184
189
|
"flatgeobuf": "^4.4.0",
|
|
185
|
-
"
|
|
186
|
-
"geotiff-geokeys-to-proj4": "^2024.4.13",
|
|
187
|
-
"hyparquet": "^1.25.1",
|
|
190
|
+
"hyparquet": "^1.25.6",
|
|
188
191
|
"hyparquet-compressors": "^1.1.1",
|
|
189
192
|
"lz-string": "^1.5.0",
|
|
190
|
-
"maplibre-gl": "^5.
|
|
191
|
-
"marked": "^17.0.
|
|
192
|
-
"mermaid": "^11.
|
|
193
|
+
"maplibre-gl": "^5.23.0",
|
|
194
|
+
"marked": "^17.0.6",
|
|
195
|
+
"mermaid": "^11.14.0",
|
|
193
196
|
"pbf": "^4.0.1",
|
|
194
|
-
"pdfjs-dist": "^5.
|
|
195
|
-
"pmtiles": "^4.4.
|
|
196
|
-
"proj4": "^2.20.
|
|
197
|
+
"pdfjs-dist": "^5.6.205",
|
|
198
|
+
"pmtiles": "^4.4.1",
|
|
199
|
+
"proj4": "^2.20.8",
|
|
197
200
|
"shiki": "^3.23.0",
|
|
198
|
-
"sql-formatter": "^15.7.
|
|
199
|
-
"yaml": "^2.8.
|
|
200
|
-
"zarrita": "^0.6.
|
|
201
|
+
"sql-formatter": "^15.7.3",
|
|
202
|
+
"yaml": "^2.8.3",
|
|
203
|
+
"zarrita": "^0.6.2"
|
|
201
204
|
},
|
|
202
205
|
"scripts": {
|
|
203
206
|
"dev": "vite dev",
|
|
@@ -210,6 +213,7 @@
|
|
|
210
213
|
"lint:fix": "biome lint --fix src/ packages/",
|
|
211
214
|
"format": "biome format --write src/ packages/",
|
|
212
215
|
"format:check": "biome format src/ packages/",
|
|
216
|
+
"deadcode": "knip",
|
|
213
217
|
"ci:publish": "pnpm run package && pnpm --filter @walkthru-earth/objex-utils run build && pnpm publish -r --access public --no-git-checks --provenance"
|
|
214
218
|
}
|
|
215
219
|
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import Root from './tabs.svelte';
|
|
2
|
-
import Content from './tabs-content.svelte';
|
|
3
|
-
import List from './tabs-list.svelte';
|
|
4
|
-
import Trigger from './tabs-trigger.svelte';
|
|
5
|
-
export { Root, Content, List, Trigger, Root as Tabs, Content as TabsContent, List as TabsList, Trigger as TabsTrigger };
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import Root from './tabs.svelte';
|
|
2
|
-
import Content from './tabs-content.svelte';
|
|
3
|
-
import List from './tabs-list.svelte';
|
|
4
|
-
import Trigger from './tabs-trigger.svelte';
|
|
5
|
-
export { Root, Content, List, Trigger,
|
|
6
|
-
//
|
|
7
|
-
Root as Tabs, Content as TabsContent, List as TabsList, Trigger as TabsTrigger };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Tabs as TabsPrimitive } from 'bits-ui';
|
|
3
|
-
import { cn } from '../../../utils.js';
|
|
4
|
-
|
|
5
|
-
let {
|
|
6
|
-
ref = $bindable(null),
|
|
7
|
-
class: className,
|
|
8
|
-
...restProps
|
|
9
|
-
}: TabsPrimitive.ContentProps = $props();
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<TabsPrimitive.Content
|
|
13
|
-
bind:ref
|
|
14
|
-
data-slot="tabs-content"
|
|
15
|
-
class={cn("flex-1 outline-none", className)}
|
|
16
|
-
{...restProps}
|
|
17
|
-
/>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Tabs as TabsPrimitive } from 'bits-ui';
|
|
3
|
-
import { cn } from '../../../utils.js';
|
|
4
|
-
|
|
5
|
-
let { ref = $bindable(null), class: className, ...restProps }: TabsPrimitive.ListProps = $props();
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<TabsPrimitive.List
|
|
9
|
-
bind:ref
|
|
10
|
-
data-slot="tabs-list"
|
|
11
|
-
class={cn(
|
|
12
|
-
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
13
|
-
className
|
|
14
|
-
)}
|
|
15
|
-
{...restProps}
|
|
16
|
-
/>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Tabs as TabsPrimitive } from 'bits-ui';
|
|
3
|
-
import { cn } from '../../../utils.js';
|
|
4
|
-
|
|
5
|
-
let {
|
|
6
|
-
ref = $bindable(null),
|
|
7
|
-
class: className,
|
|
8
|
-
...restProps
|
|
9
|
-
}: TabsPrimitive.TriggerProps = $props();
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<TabsPrimitive.Trigger
|
|
13
|
-
bind:ref
|
|
14
|
-
data-slot="tabs-trigger"
|
|
15
|
-
class={cn(
|
|
16
|
-
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
17
|
-
className
|
|
18
|
-
)}
|
|
19
|
-
{...restProps}
|
|
20
|
-
/>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Tabs as TabsPrimitive } from 'bits-ui';
|
|
3
|
-
import { cn } from '../../../utils.js';
|
|
4
|
-
|
|
5
|
-
let {
|
|
6
|
-
ref = $bindable(null),
|
|
7
|
-
value = $bindable(''),
|
|
8
|
-
class: className,
|
|
9
|
-
...restProps
|
|
10
|
-
}: TabsPrimitive.RootProps = $props();
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<TabsPrimitive.Root
|
|
14
|
-
bind:ref
|
|
15
|
-
bind:value
|
|
16
|
-
data-slot="tabs"
|
|
17
|
-
class={cn("flex flex-col gap-2", className)}
|
|
18
|
-
{...restProps}
|
|
19
|
-
/>
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type maplibregl from 'maplibre-gl';
|
|
3
|
-
import { onDestroy } from 'svelte';
|
|
4
|
-
import { t } from '../../i18n/index.svelte.js';
|
|
5
|
-
import { getAdapter } from '../../storage/index.js';
|
|
6
|
-
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
7
|
-
import type { Tab } from '../../types';
|
|
8
|
-
import { setupSelectionLayer, updateSelection } from '../../utils/map-selection.js';
|
|
9
|
-
import AttributeTable from './map/AttributeTable.svelte';
|
|
10
|
-
import MapContainer from './map/MapContainer.svelte';
|
|
11
|
-
|
|
12
|
-
let { tab }: { tab: Tab } = $props();
|
|
13
|
-
|
|
14
|
-
let abortController: AbortController | null = null;
|
|
15
|
-
let loading = $state(true);
|
|
16
|
-
let error = $state<string | null>(null);
|
|
17
|
-
let geojsonData = $state.raw<any>(null);
|
|
18
|
-
let selectedFeature = $state<Record<string, any> | null>(null);
|
|
19
|
-
let showAttributes = $state(false);
|
|
20
|
-
let bounds = $state<[number, number, number, number] | undefined>();
|
|
21
|
-
|
|
22
|
-
$effect(() => {
|
|
23
|
-
if (!tab) return;
|
|
24
|
-
loadGeoJson();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function cleanup() {
|
|
28
|
-
abortController?.abort();
|
|
29
|
-
abortController = null;
|
|
30
|
-
geojsonData = null;
|
|
31
|
-
selectedFeature = null;
|
|
32
|
-
bounds = undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
$effect(() => {
|
|
36
|
-
if (!tab) return;
|
|
37
|
-
const unregister = tabResources.register(tab.id, cleanup);
|
|
38
|
-
return unregister;
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
onDestroy(cleanup);
|
|
42
|
-
|
|
43
|
-
async function loadGeoJson() {
|
|
44
|
-
abortController?.abort();
|
|
45
|
-
abortController = new AbortController();
|
|
46
|
-
const { signal } = abortController;
|
|
47
|
-
|
|
48
|
-
loading = true;
|
|
49
|
-
error = null;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const adapter = getAdapter(tab.source, tab.connectionId);
|
|
53
|
-
const data = await adapter.read(tab.path, undefined, undefined, signal);
|
|
54
|
-
const text = new TextDecoder().decode(data);
|
|
55
|
-
geojsonData = JSON.parse(text);
|
|
56
|
-
|
|
57
|
-
// Compute bounds from features
|
|
58
|
-
if (geojsonData.bbox) {
|
|
59
|
-
bounds = geojsonData.bbox as [number, number, number, number];
|
|
60
|
-
} else {
|
|
61
|
-
bounds = computeBounds(geojsonData);
|
|
62
|
-
}
|
|
63
|
-
} catch (err) {
|
|
64
|
-
if (err instanceof DOMException && err.name === 'AbortError') return;
|
|
65
|
-
error = err instanceof Error ? err.message : String(err);
|
|
66
|
-
} finally {
|
|
67
|
-
loading = false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function computeBounds(geojson: any): [number, number, number, number] | undefined {
|
|
72
|
-
let minLng = Infinity,
|
|
73
|
-
minLat = Infinity,
|
|
74
|
-
maxLng = -Infinity,
|
|
75
|
-
maxLat = -Infinity;
|
|
76
|
-
let found = false;
|
|
77
|
-
|
|
78
|
-
function processCoord(coord: number[]) {
|
|
79
|
-
if (coord.length >= 2) {
|
|
80
|
-
found = true;
|
|
81
|
-
minLng = Math.min(minLng, coord[0]);
|
|
82
|
-
minLat = Math.min(minLat, coord[1]);
|
|
83
|
-
maxLng = Math.max(maxLng, coord[0]);
|
|
84
|
-
maxLat = Math.max(maxLat, coord[1]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function processCoords(coords: any) {
|
|
89
|
-
if (!coords) return;
|
|
90
|
-
if (typeof coords[0] === 'number') {
|
|
91
|
-
processCoord(coords);
|
|
92
|
-
} else {
|
|
93
|
-
for (const c of coords) processCoords(c);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function processGeometry(geom: any) {
|
|
98
|
-
if (!geom) return;
|
|
99
|
-
if (geom.coordinates) processCoords(geom.coordinates);
|
|
100
|
-
if (geom.geometries) geom.geometries.forEach(processGeometry);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (geojson.type === 'FeatureCollection') {
|
|
104
|
-
for (const f of geojson.features || []) processGeometry(f.geometry);
|
|
105
|
-
} else if (geojson.type === 'Feature') {
|
|
106
|
-
processGeometry(geojson.geometry);
|
|
107
|
-
} else {
|
|
108
|
-
processGeometry(geojson);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return found ? [minLng, minLat, maxLng, maxLat] : undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function onMapReady(map: maplibregl.Map) {
|
|
115
|
-
if (!geojsonData) return;
|
|
116
|
-
|
|
117
|
-
map.addSource('geojson-source', {
|
|
118
|
-
type: 'geojson',
|
|
119
|
-
data: geojsonData
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Fill layer for polygons (orange)
|
|
123
|
-
map.addLayer({
|
|
124
|
-
id: 'geojson-fill',
|
|
125
|
-
type: 'fill',
|
|
126
|
-
source: 'geojson-source',
|
|
127
|
-
filter: ['==', '$type', 'Polygon'],
|
|
128
|
-
paint: {
|
|
129
|
-
'fill-color': '#e8793d',
|
|
130
|
-
'fill-opacity': 0.35
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Outline layer for polygons (orange)
|
|
135
|
-
map.addLayer({
|
|
136
|
-
id: 'geojson-polygon-outline',
|
|
137
|
-
type: 'line',
|
|
138
|
-
source: 'geojson-source',
|
|
139
|
-
filter: ['==', '$type', 'Polygon'],
|
|
140
|
-
paint: {
|
|
141
|
-
'line-color': '#e65100',
|
|
142
|
-
'line-width': 2.5
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Line layer for linestrings (teal)
|
|
147
|
-
map.addLayer({
|
|
148
|
-
id: 'geojson-line',
|
|
149
|
-
type: 'line',
|
|
150
|
-
source: 'geojson-source',
|
|
151
|
-
filter: ['==', '$type', 'LineString'],
|
|
152
|
-
paint: {
|
|
153
|
-
'line-color': '#00838f',
|
|
154
|
-
'line-width': 2.5
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Circle layer for points (blue)
|
|
159
|
-
map.addLayer({
|
|
160
|
-
id: 'geojson-points',
|
|
161
|
-
type: 'circle',
|
|
162
|
-
source: 'geojson-source',
|
|
163
|
-
filter: ['==', '$type', 'Point'],
|
|
164
|
-
paint: {
|
|
165
|
-
'circle-radius': 7,
|
|
166
|
-
'circle-color': '#4285f4',
|
|
167
|
-
'circle-stroke-width': 1.5,
|
|
168
|
-
'circle-stroke-color': '#fff'
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Selection highlight
|
|
173
|
-
setupSelectionLayer(map);
|
|
174
|
-
|
|
175
|
-
// Click handler
|
|
176
|
-
for (const layerId of [
|
|
177
|
-
'geojson-fill',
|
|
178
|
-
'geojson-polygon-outline',
|
|
179
|
-
'geojson-line',
|
|
180
|
-
'geojson-points'
|
|
181
|
-
]) {
|
|
182
|
-
map.on('click', layerId, (e: any) => {
|
|
183
|
-
if (e.features && e.features.length > 0) {
|
|
184
|
-
selectedFeature = { ...e.features[0].properties };
|
|
185
|
-
showAttributes = true;
|
|
186
|
-
updateSelection(map, e.features[0] as GeoJSON.Feature);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
map.on('mouseenter', layerId, () => {
|
|
191
|
-
map.getCanvas().style.cursor = 'pointer';
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
map.on('mouseleave', layerId, () => {
|
|
195
|
-
map.getCanvas().style.cursor = '';
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
</script>
|
|
200
|
-
|
|
201
|
-
<div class="relative flex h-full overflow-hidden">
|
|
202
|
-
{#if loading}
|
|
203
|
-
<div class="flex flex-1 items-center justify-center">
|
|
204
|
-
<p class="text-sm text-zinc-400">{t('map.loadingData')}</p>
|
|
205
|
-
</div>
|
|
206
|
-
{:else if error}
|
|
207
|
-
<div class="flex flex-1 items-center justify-center">
|
|
208
|
-
<p class="text-sm text-red-400">{error}</p>
|
|
209
|
-
</div>
|
|
210
|
-
{:else}
|
|
211
|
-
<div class="flex-1">
|
|
212
|
-
<MapContainer {onMapReady} {bounds} />
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{#if selectedFeature}
|
|
216
|
-
<div class="absolute right-2 top-2 z-10 flex gap-1">
|
|
217
|
-
<button
|
|
218
|
-
class="rounded bg-card/80 px-2 py-1 text-xs text-card-foreground backdrop-blur-sm hover:bg-card"
|
|
219
|
-
class:ring-1={showAttributes}
|
|
220
|
-
class:ring-primary={showAttributes}
|
|
221
|
-
onclick={() => (showAttributes = !showAttributes)}
|
|
222
|
-
>
|
|
223
|
-
{t('map.attributes')}
|
|
224
|
-
</button>
|
|
225
|
-
</div>
|
|
226
|
-
{/if}
|
|
227
|
-
|
|
228
|
-
<AttributeTable
|
|
229
|
-
feature={selectedFeature}
|
|
230
|
-
visible={showAttributes}
|
|
231
|
-
onClose={() => (showAttributes = false)}
|
|
232
|
-
/>
|
|
233
|
-
{/if}
|
|
234
|
-
</div>
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { styleUrl, onclose }: { styleUrl: string; onclose: () => void } = $props();
|
|
3
|
-
|
|
4
|
-
const maputnikUrl = $derived(
|
|
5
|
-
`https://maplibre.org/maputnik/?style=${encodeURIComponent(styleUrl)}`
|
|
6
|
-
);
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<div class="fixed inset-0 z-50 flex flex-col bg-zinc-950">
|
|
10
|
-
<div
|
|
11
|
-
class="flex items-center justify-between border-b border-zinc-800 bg-zinc-900 px-4 py-2"
|
|
12
|
-
>
|
|
13
|
-
<span class="text-sm font-medium text-zinc-300">Maputnik Style Editor</span>
|
|
14
|
-
<button
|
|
15
|
-
class="rounded px-3 py-1 text-xs text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200"
|
|
16
|
-
onclick={onclose}
|
|
17
|
-
>
|
|
18
|
-
Close
|
|
19
|
-
</button>
|
|
20
|
-
</div>
|
|
21
|
-
<iframe
|
|
22
|
-
src={maputnikUrl}
|
|
23
|
-
class="flex-1 border-0"
|
|
24
|
-
title="Maputnik Style Editor"
|
|
25
|
-
allow="clipboard-read; clipboard-write"
|
|
26
|
-
></iframe>
|
|
27
|
-
</div>
|