@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.
Files changed (72) hide show
  1. package/README.md +3 -1
  2. package/dist/components/browser/FileBrowser.svelte +25 -14
  3. package/dist/components/browser/FileTreeSidebar.svelte +42 -6
  4. package/dist/components/layout/ConnectionDialog.svelte +100 -1
  5. package/dist/components/layout/Sidebar.svelte +43 -25
  6. package/dist/components/viewers/CodeViewer.svelte +23 -0
  7. package/dist/components/viewers/CogControls.svelte +208 -0
  8. package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
  9. package/dist/components/viewers/CogViewer.svelte +353 -1160
  10. package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
  11. package/dist/components/viewers/DatabaseViewer.svelte +345 -37
  12. package/dist/components/viewers/MarkdownViewer.svelte +1 -1
  13. package/dist/components/viewers/TableViewer.svelte +123 -41
  14. package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
  15. package/dist/components/viewers/ZarrViewer.svelte +1 -4
  16. package/dist/constants.d.ts +6 -2
  17. package/dist/constants.js +6 -2
  18. package/dist/file-icons/index.d.ts +1 -1
  19. package/dist/file-icons/index.js +12 -2
  20. package/dist/i18n/ar.js +24 -0
  21. package/dist/i18n/en.js +24 -0
  22. package/dist/i18n/index.svelte.d.ts +0 -1
  23. package/dist/i18n/index.svelte.js +0 -3
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +1 -0
  26. package/dist/query/engine.d.ts +20 -4
  27. package/dist/query/index.d.ts +2 -1
  28. package/dist/query/index.js +1 -0
  29. package/dist/query/source.d.ts +30 -0
  30. package/dist/query/source.js +37 -0
  31. package/dist/query/wasm.d.ts +7 -5
  32. package/dist/query/wasm.js +138 -85
  33. package/dist/storage/providers.d.ts +47 -0
  34. package/dist/storage/providers.js +160 -0
  35. package/dist/stores/files.svelte.d.ts +1 -2
  36. package/dist/stores/files.svelte.js +1 -2
  37. package/dist/stores/tabs.svelte.d.ts +9 -2
  38. package/dist/stores/tabs.svelte.js +11 -2
  39. package/dist/types.d.ts +11 -0
  40. package/dist/utils/cog.d.ts +244 -0
  41. package/dist/utils/cog.js +1039 -0
  42. package/dist/utils/deck.d.ts +0 -18
  43. package/dist/utils/deck.js +0 -36
  44. package/dist/utils/geometry-type.d.ts +52 -0
  45. package/dist/utils/geometry-type.js +76 -0
  46. package/dist/utils/markdown-sql.d.ts +1 -1
  47. package/dist/utils/markdown-sql.js +3 -4
  48. package/dist/utils/pmtiles-tile.d.ts +0 -2
  49. package/dist/utils/pmtiles-tile.js +0 -8
  50. package/dist/utils/url-state.d.ts +6 -0
  51. package/dist/utils/url-state.js +34 -26
  52. package/dist/utils/url.d.ts +13 -9
  53. package/dist/utils/url.js +16 -25
  54. package/dist/utils/zarr-tab.d.ts +22 -0
  55. package/dist/utils/zarr-tab.js +30 -0
  56. package/dist/utils/zarr.d.ts +0 -2
  57. package/dist/utils/zarr.js +73 -44
  58. package/package.json +47 -43
  59. package/dist/components/ui/tabs/index.d.ts +0 -5
  60. package/dist/components/ui/tabs/index.js +0 -7
  61. package/dist/components/ui/tabs/tabs-content.svelte +0 -17
  62. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
  63. package/dist/components/ui/tabs/tabs-list.svelte +0 -16
  64. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
  65. package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
  66. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
  67. package/dist/components/ui/tabs/tabs.svelte +0 -19
  68. package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
  69. package/dist/components/viewers/MapViewer.svelte +0 -234
  70. package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
  71. package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
  72. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
@@ -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(async (name) => {
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.1.0",
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.7",
126
+ "@biomejs/biome": "^2.4.12",
127
127
  "@changesets/changelog-github": "^0.5.2",
128
- "@changesets/cli": "^2.30.0",
128
+ "@changesets/cli": "^2.31.0",
129
129
  "@fontsource/cairo": "^5.2.7",
130
- "@internationalized/date": "^3.12.0",
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.55.0",
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.1",
139
- "bits-ui": "^2.16.3",
138
+ "@tailwindcss/vite": "^4.2.4",
139
+ "bits-ui": "^2.18.0",
140
140
  "clsx": "^2.1.1",
141
- "lefthook": "^2.1.4",
141
+ "knip": "^6.6.0",
142
+ "lefthook": "^2.1.6",
142
143
  "paneforge": "^1.0.2",
143
- "posthog-js": "^1.360.2",
144
+ "posthog-js": "^1.369.5",
144
145
  "publint": "^0.3.18",
145
- "svelte": "^5.53.12",
146
- "svelte-check": "^4.4.5",
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.1",
150
+ "tailwindcss": "^4.2.4",
150
151
  "tw-animate-css": "^1.4.0",
151
152
  "typescript": "^5.9.3",
152
- "vite": "^7.3.1"
153
+ "vite": "^7.3.2"
153
154
  },
154
155
  "dependencies": {
155
- "@babylonjs/core": "^8.55.3",
156
- "@babylonjs/loaders": "^8.55.3",
157
- "@carbonplan/zarr-layer": "^0.3.1",
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.2",
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.40.0",
165
- "@deck.gl/core": "^9.2.11",
166
- "@deck.gl/geo-layers": "^9.2.11",
167
- "@deck.gl/layers": "^9.2.11",
168
- "@deck.gl/mapbox": "^9.2.11",
169
- "@deck.gl/mesh-layers": "^9.2.11",
170
- "@developmentseed/deck.gl-geotiff": "^0.2.0",
171
- "@duckdb/duckdb-wasm": "1.33.1-dev34.0",
172
- "@geoarrow/deck.gl-layers": "^0.3.1",
173
- "@luma.gl/core": "^9.2.6",
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.19.0",
176
- "@milkdown/theme-nord": "^7.19.0",
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.23",
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.2.11",
188
+ "deck.gl": "^9.3.1",
184
189
  "flatgeobuf": "^4.4.0",
185
- "geotiff": "^3.0.5",
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.20.1",
191
- "marked": "^17.0.4",
192
- "mermaid": "^11.13.0",
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.5.207",
195
- "pmtiles": "^4.4.0",
196
- "proj4": "^2.20.4",
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.2",
199
- "yaml": "^2.8.2",
200
- "zarrita": "^0.6.1"
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,4 +0,0 @@
1
- import { Tabs as TabsPrimitive } from 'bits-ui';
2
- declare const TabsContent: import("svelte").Component<TabsPrimitive.ContentProps, {}, "ref">;
3
- type TabsContent = ReturnType<typeof TabsContent>;
4
- export default TabsContent;
@@ -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,4 +0,0 @@
1
- import { Tabs as TabsPrimitive } from 'bits-ui';
2
- declare const TabsList: import("svelte").Component<TabsPrimitive.ListProps, {}, "ref">;
3
- type TabsList = ReturnType<typeof TabsList>;
4
- export default TabsList;
@@ -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,4 +0,0 @@
1
- import { Tabs as TabsPrimitive } from 'bits-ui';
2
- declare const TabsTrigger: import("svelte").Component<TabsPrimitive.TriggerProps, {}, "ref">;
3
- type TabsTrigger = ReturnType<typeof TabsTrigger>;
4
- export default TabsTrigger;
@@ -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,4 +0,0 @@
1
- import { Tabs as TabsPrimitive } from 'bits-ui';
2
- declare const Tabs: import("svelte").Component<TabsPrimitive.RootProps, {}, "value" | "ref">;
3
- type Tabs = ReturnType<typeof Tabs>;
4
- export default Tabs;
@@ -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,7 +0,0 @@
1
- import type { Tab } from '../../types';
2
- type $$ComponentProps = {
3
- tab: Tab;
4
- };
5
- declare const MapViewer: import("svelte").Component<$$ComponentProps, {}, "">;
6
- type MapViewer = ReturnType<typeof MapViewer>;
7
- export default MapViewer;
@@ -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>