@smartnet360/svelte-components 0.0.125 → 0.0.126

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 (30) hide show
  1. package/dist/core/Auth/auth.svelte.js +47 -2
  2. package/dist/map-v3/demo/DemoMap.svelte +18 -0
  3. package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
  4. package/dist/map-v3/demo/demo-custom-cells.js +48 -0
  5. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
  6. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
  7. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
  8. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
  9. package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
  10. package/dist/map-v3/features/cells/custom/components/index.js +5 -0
  11. package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
  12. package/dist/map-v3/features/cells/custom/index.js +35 -0
  13. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
  14. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
  15. package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
  16. package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
  17. package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
  18. package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
  19. package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
  20. package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
  21. package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
  22. package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
  23. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
  24. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
  25. package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
  26. package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
  27. package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
  28. package/dist/map-v3/features/cells/custom/types.js +23 -0
  29. package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
  30. package/package.json +1 -1
@@ -0,0 +1,306 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Cell Set Manager
4
+ *
5
+ * Upload CSV files, view import results, and manage custom cell sets.
6
+ * Acts as the main control panel for the custom cells feature.
7
+ * Includes filter controls for each loaded set.
8
+ */
9
+ import { MapControl } from '../../../../shared';
10
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
11
+ import type { CustomCellImportResult } from '../types';
12
+ import CustomCellFilterControl from './CustomCellFilterControl.svelte';
13
+
14
+ interface Props {
15
+ /** The custom cell sets store */
16
+ setsStore: CustomCellSetsStore;
17
+ /** Control position on map */
18
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
19
+ }
20
+
21
+ let {
22
+ setsStore,
23
+ position = 'top-left'
24
+ }: Props = $props();
25
+
26
+ // Derived for reactivity
27
+ let setsArray = $derived(setsStore.sets);
28
+
29
+ // State
30
+ let showImportModal = $state(false);
31
+ let importResult = $state<CustomCellImportResult | null>(null);
32
+ let importFileName = $state('');
33
+ let importError = $state('');
34
+ let fileInput: HTMLInputElement;
35
+
36
+ /**
37
+ * Handle file selection
38
+ */
39
+ function handleFileSelect(event: Event) {
40
+ const input = event.target as HTMLInputElement;
41
+ const file = input.files?.[0];
42
+
43
+ if (!file) return;
44
+
45
+ importFileName = file.name.replace(/\.csv$/i, '');
46
+ importError = '';
47
+
48
+ const reader = new FileReader();
49
+ reader.onload = (e) => {
50
+ try {
51
+ const content = e.target?.result as string;
52
+ importResult = setsStore.importFromCsv(content, file.name);
53
+ showImportModal = true;
54
+ } catch (err) {
55
+ importError = err instanceof Error ? err.message : 'Failed to parse CSV';
56
+ importResult = null;
57
+ }
58
+ };
59
+ reader.onerror = () => {
60
+ importError = 'Failed to read file';
61
+ importResult = null;
62
+ };
63
+ reader.readAsText(file);
64
+
65
+ // Reset input so same file can be selected again
66
+ input.value = '';
67
+ }
68
+
69
+ /**
70
+ * Confirm import and create set
71
+ */
72
+ function confirmImport() {
73
+ if (!importResult) return;
74
+
75
+ setsStore.createSet(importFileName, importResult);
76
+ closeModal();
77
+ }
78
+
79
+ /**
80
+ * Close the modal
81
+ */
82
+ function closeModal() {
83
+ showImportModal = false;
84
+ importResult = null;
85
+ importFileName = '';
86
+ importError = '';
87
+ }
88
+
89
+ /**
90
+ * Trigger file input click
91
+ */
92
+ function triggerFileInput() {
93
+ fileInput?.click();
94
+ }
95
+
96
+ /**
97
+ * Remove a set
98
+ */
99
+ function removeSet(setId: string) {
100
+ if (confirm('Remove this custom cell set?')) {
101
+ setsStore.removeSet(setId);
102
+ }
103
+ }
104
+ </script>
105
+
106
+ <MapControl {position} title="Custom Cells" icon="plus-circle" controlWidth="300px">
107
+ {#snippet actions()}
108
+ <button
109
+ class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
110
+ title="Import CSV"
111
+ onclick={triggerFileInput}
112
+ >
113
+ <i class="bi bi-upload"></i>
114
+ </button>
115
+ {/snippet}
116
+
117
+ <div class="custom-cell-manager">
118
+ <!-- Hidden file input -->
119
+ <input
120
+ type="file"
121
+ accept=".csv"
122
+ class="d-none"
123
+ bind:this={fileInput}
124
+ onchange={handleFileSelect}
125
+ />
126
+
127
+ <!-- Error message -->
128
+ {#if importError}
129
+ <div class="alert alert-danger py-1 px-2 mb-2 small">
130
+ <i class="bi bi-exclamation-triangle me-1"></i>
131
+ {importError}
132
+ </div>
133
+ {/if}
134
+
135
+ <!-- Empty state -->
136
+ {#if setsStore.sets.length === 0}
137
+ <div class="text-center p-3">
138
+ <i class="bi bi-layers fs-1 text-muted"></i>
139
+ <p class="text-muted small mt-2 mb-0">
140
+ No custom cell sets loaded.
141
+ </p>
142
+ <button
143
+ class="btn btn-sm btn-outline-primary mt-2"
144
+ onclick={triggerFileInput}
145
+ >
146
+ <i class="bi bi-upload me-1"></i> Import CSV
147
+ </button>
148
+ </div>
149
+ {:else}
150
+ <!-- List of sets -->
151
+ <ul class="list-group list-group-flush">
152
+ {#each setsStore.sets as set (set.id)}
153
+ <li class="list-group-item d-flex justify-content-between align-items-center py-2 px-2">
154
+ <div class="d-flex align-items-center">
155
+ <button
156
+ class="btn btn-sm p-0 me-2"
157
+ title={set.visible ? 'Hide' : 'Show'}
158
+ onclick={() => setsStore.toggleSetVisibility(set.id)}
159
+ >
160
+ <i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
161
+ </button>
162
+ <div>
163
+ <div class="fw-medium small">{set.name}</div>
164
+ <small class="text-muted">
165
+ {set.cells.length} cells · {set.groups.length} groups
166
+ </small>
167
+ </div>
168
+ </div>
169
+ <button
170
+ class="btn btn-sm btn-outline-danger border-0 p-1"
171
+ title="Remove"
172
+ onclick={() => removeSet(set.id)}
173
+ >
174
+ <i class="bi bi-trash"></i>
175
+ </button>
176
+ </li>
177
+ {/each}
178
+ </ul>
179
+ {/if}
180
+ </div>
181
+ </MapControl>
182
+
183
+ <!-- Import Result Modal -->
184
+ {#if showImportModal && importResult}
185
+ <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
186
+ <div class="modal-dialog modal-dialog-centered">
187
+ <div class="modal-content">
188
+ <div class="modal-header py-2">
189
+ <h6 class="modal-title">
190
+ <i class="bi bi-file-earmark-spreadsheet me-2"></i>
191
+ Import: {importFileName}
192
+ </h6>
193
+ <button type="button" class="btn-close" onclick={closeModal}></button>
194
+ </div>
195
+ <div class="modal-body">
196
+ <!-- Import Summary -->
197
+ <div class="mb-3">
198
+ <div class="d-flex justify-content-between align-items-center mb-2">
199
+ <span class="text-success">
200
+ <i class="bi bi-check-circle me-1"></i>
201
+ {importResult.cells.length} cells matched
202
+ </span>
203
+ <span class="badge bg-success">{importResult.totalRows} rows</span>
204
+ </div>
205
+
206
+ {#if importResult.unmatchedTxIds.length > 0}
207
+ <div class="text-warning small mb-2">
208
+ <i class="bi bi-exclamation-triangle me-1"></i>
209
+ {importResult.unmatchedTxIds.length} cells not found
210
+ <button
211
+ class="btn btn-link btn-sm p-0 ms-1"
212
+ type="button"
213
+ data-bs-toggle="collapse"
214
+ data-bs-target="#unmatchedList"
215
+ >
216
+ (show)
217
+ </button>
218
+ </div>
219
+ <div class="collapse" id="unmatchedList">
220
+ <div class="small bg-light p-2 rounded" style="max-height: 100px; overflow-y: auto;">
221
+ {importResult.unmatchedTxIds.slice(0, 20).join(', ')}
222
+ {#if importResult.unmatchedTxIds.length > 20}
223
+ <span class="text-muted">... and {importResult.unmatchedTxIds.length - 20} more</span>
224
+ {/if}
225
+ </div>
226
+ </div>
227
+ {/if}
228
+ </div>
229
+
230
+ <!-- Groups found -->
231
+ <div class="mb-3">
232
+ <label class="form-label small fw-medium">Groups Found ({importResult.groups.length})</label>
233
+ <div class="d-flex flex-wrap gap-1">
234
+ {#each importResult.groups as group}
235
+ <span class="badge bg-secondary">{group}</span>
236
+ {/each}
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Extra columns -->
241
+ {#if importResult.extraColumns.length > 0}
242
+ <div class="mb-3">
243
+ <label class="form-label small fw-medium">Extra Columns (for tooltips)</label>
244
+ <div class="d-flex flex-wrap gap-1">
245
+ {#each importResult.extraColumns as col}
246
+ <span class="badge bg-info">{col}</span>
247
+ {/each}
248
+ </div>
249
+ </div>
250
+ {/if}
251
+
252
+ <!-- Set Name -->
253
+ <div class="mb-2">
254
+ <label for="setName" class="form-label small fw-medium">Set Name</label>
255
+ <input
256
+ type="text"
257
+ class="form-control form-control-sm"
258
+ id="setName"
259
+ bind:value={importFileName}
260
+ />
261
+ </div>
262
+ </div>
263
+ <div class="modal-footer py-2">
264
+ <button type="button" class="btn btn-secondary btn-sm" onclick={closeModal}>
265
+ Cancel
266
+ </button>
267
+ <button
268
+ type="button"
269
+ class="btn btn-primary btn-sm"
270
+ onclick={confirmImport}
271
+ disabled={importResult.cells.length === 0}
272
+ >
273
+ <i class="bi bi-check-lg me-1"></i>
274
+ Import {importResult.cells.length} cells
275
+ </button>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ {/if}
281
+
282
+ <style>
283
+ .custom-cell-manager {
284
+ font-size: 0.875rem;
285
+ }
286
+
287
+ .list-group-item {
288
+ background: transparent;
289
+ }
290
+
291
+ .modal {
292
+ z-index: 2000;
293
+ }
294
+ </style>
295
+
296
+ <!-- Filter Controls for each set (rendered outside the manager control) -->
297
+ {#each setsArray as set (set.id)}
298
+ {#key set.id}
299
+ <CustomCellFilterControl
300
+ {position}
301
+ {setsStore}
302
+ {set}
303
+ onremove={(id) => setsStore.removeSet(id)}
304
+ />
305
+ {/key}
306
+ {/each}
@@ -0,0 +1,10 @@
1
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
2
+ interface Props {
3
+ /** The custom cell sets store */
4
+ setsStore: CustomCellSetsStore;
5
+ /** Control position on map */
6
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
7
+ }
8
+ declare const CustomCellSetManager: import("svelte").Component<Props, {}, "">;
9
+ type CustomCellSetManager = ReturnType<typeof CustomCellSetManager>;
10
+ export default CustomCellSetManager;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Custom Cells Components - Barrel Export
3
+ */
4
+ export { default as CustomCellFilterControl } from './CustomCellFilterControl.svelte';
5
+ export { default as CustomCellSetManager } from './CustomCellSetManager.svelte';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Custom Cells Components - Barrel Export
3
+ */
4
+ export { default as CustomCellFilterControl } from './CustomCellFilterControl.svelte';
5
+ export { default as CustomCellSetManager } from './CustomCellSetManager.svelte';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Custom Cells Feature - Main Barrel Export
3
+ *
4
+ * Ad-hoc cell layers loaded from CSV files.
5
+ * Reference existing cells by txId with custom grouping and sizing.
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import {
11
+ * CustomCellsLayer,
12
+ * CustomCellSetManager,
13
+ * CustomCellFilterControl,
14
+ * createCustomCellSetsStore
15
+ * } from './';
16
+ *
17
+ * const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
18
+ * </script>
19
+ *
20
+ * <CustomCellSetManager setsStore={customSetsStore} position="top-left" />
21
+ * <CustomCellsLayer setsStore={customSetsStore} />
22
+ * {#each customSetsStore.sets as set (set.id)}
23
+ * <CustomCellFilterControl setsStore={customSetsStore} {set} position="top-left" />
24
+ * {/each}
25
+ * ```
26
+ */
27
+ export type { CustomCell, CustomCellSet, CustomCellSetConfig, CustomCellImportResult } from './types';
28
+ export { CUSTOM_CELL_PALETTE } from './types';
29
+ export { CustomCellSetsStore, createCustomCellSetsStore } from './stores';
30
+ export { CustomCellFilterControl, CustomCellSetManager } from './components';
31
+ export { CustomCellsLayer } from './layers';
32
+ export { parseCustomCellsCsv, buildCellLookup, buildCustomCellTree, getGroupCounts } from './logic';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Custom Cells Feature - Main Barrel Export
3
+ *
4
+ * Ad-hoc cell layers loaded from CSV files.
5
+ * Reference existing cells by txId with custom grouping and sizing.
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import {
11
+ * CustomCellsLayer,
12
+ * CustomCellSetManager,
13
+ * CustomCellFilterControl,
14
+ * createCustomCellSetsStore
15
+ * } from './';
16
+ *
17
+ * const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
18
+ * </script>
19
+ *
20
+ * <CustomCellSetManager setsStore={customSetsStore} position="top-left" />
21
+ * <CustomCellsLayer setsStore={customSetsStore} />
22
+ * {#each customSetsStore.sets as set (set.id)}
23
+ * <CustomCellFilterControl setsStore={customSetsStore} {set} position="top-left" />
24
+ * {/each}
25
+ * ```
26
+ */
27
+ export { CUSTOM_CELL_PALETTE } from './types';
28
+ // Stores
29
+ export { CustomCellSetsStore, createCustomCellSetsStore } from './stores';
30
+ // Components
31
+ export { CustomCellFilterControl, CustomCellSetManager } from './components';
32
+ // Layers
33
+ export { CustomCellsLayer } from './layers';
34
+ // Logic (for advanced usage)
35
+ export { parseCustomCellsCsv, buildCellLookup, buildCustomCellTree, getGroupCounts } from './logic';
@@ -0,0 +1,262 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Cells Layer
4
+ *
5
+ * Renders custom cell sets on the map with sizeFactor support.
6
+ * Each set is rendered as a separate layer for independent styling.
7
+ */
8
+ import { getContext, onMount, onDestroy } from 'svelte';
9
+ import type { MapStore } from '../../../../core/stores/map.store.svelte';
10
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
11
+ import type { CustomCellSet, CustomCell } from '../types';
12
+ import { generateCellArc, calculateRadiusInMeters } from '../../logic/geometry';
13
+ import type mapboxgl from 'mapbox-gl';
14
+
15
+ interface Props {
16
+ /** The custom cell sets store */
17
+ setsStore: CustomCellSetsStore;
18
+ /** Optional: specific set ID to render (if not provided, renders all) */
19
+ setId?: string;
20
+ }
21
+
22
+ let { setsStore, setId }: Props = $props();
23
+
24
+ const mapStore = getContext<MapStore>('MAP_CONTEXT');
25
+
26
+ // Track active layer/source IDs for cleanup
27
+ let activeSources = new Set<string>();
28
+ let activeLayers = new Set<string>();
29
+
30
+ // Debounce timer
31
+ let updateTimeout: ReturnType<typeof setTimeout>;
32
+
33
+ /**
34
+ * Get source ID for a set
35
+ */
36
+ function getSourceId(setId: string): string {
37
+ return `custom-cells-${setId}`;
38
+ }
39
+
40
+ /**
41
+ * Get layer IDs for a set
42
+ */
43
+ function getLayerIds(setId: string): { fill: string; line: string } {
44
+ return {
45
+ fill: `custom-cells-fill-${setId}`,
46
+ line: `custom-cells-line-${setId}`
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Add source and layers for a set
52
+ */
53
+ function addSetLayers(map: mapboxgl.Map, set: CustomCellSet) {
54
+ const sourceId = getSourceId(set.id);
55
+ const { fill, line } = getLayerIds(set.id);
56
+
57
+ // Add source if not exists
58
+ if (!map.getSource(sourceId)) {
59
+ map.addSource(sourceId, {
60
+ type: 'geojson',
61
+ data: { type: 'FeatureCollection', features: [] }
62
+ });
63
+ activeSources.add(sourceId);
64
+ }
65
+
66
+ // Add fill layer
67
+ if (!map.getLayer(fill)) {
68
+ map.addLayer({
69
+ id: fill,
70
+ type: 'fill',
71
+ source: sourceId,
72
+ paint: {
73
+ 'fill-color': ['get', 'color'],
74
+ 'fill-opacity': set.opacity
75
+ }
76
+ });
77
+ activeLayers.add(fill);
78
+ }
79
+
80
+ // Add line layer
81
+ if (!map.getLayer(line)) {
82
+ map.addLayer({
83
+ id: line,
84
+ type: 'line',
85
+ source: sourceId,
86
+ paint: {
87
+ 'line-color': ['get', 'lineColor'],
88
+ 'line-width': ['get', 'lineWidth'],
89
+ 'line-opacity': ['get', 'lineOpacity']
90
+ }
91
+ });
92
+ activeLayers.add(line);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Remove layers and source for a set
98
+ */
99
+ function removeSetLayers(map: mapboxgl.Map, setIdToRemove: string) {
100
+ const sourceId = getSourceId(setIdToRemove);
101
+ const { fill, line } = getLayerIds(setIdToRemove);
102
+
103
+ if (map.getLayer(line)) {
104
+ map.removeLayer(line);
105
+ activeLayers.delete(line);
106
+ }
107
+ if (map.getLayer(fill)) {
108
+ map.removeLayer(fill);
109
+ activeLayers.delete(fill);
110
+ }
111
+ if (map.getSource(sourceId)) {
112
+ map.removeSource(sourceId);
113
+ activeSources.delete(sourceId);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Render cells for a specific set
119
+ */
120
+ function renderSet(map: mapboxgl.Map, set: CustomCellSet) {
121
+ const bounds = map.getBounds();
122
+ if (!bounds) return;
123
+
124
+ const zoom = map.getZoom();
125
+ const centerLat = map.getCenter().lat;
126
+
127
+ // Calculate base radius from pixel size
128
+ const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, set.baseSize);
129
+
130
+ const features: GeoJSON.Feature[] = [];
131
+
132
+ for (const customCell of set.cells) {
133
+ const cell = customCell.resolvedCell;
134
+ if (!cell) continue;
135
+
136
+ // Check group visibility
137
+ if (!set.visibleGroups.has(customCell.customGroup)) continue;
138
+
139
+ // Viewport filter
140
+ if (!bounds.contains([cell.longitude, cell.latitude])) continue;
141
+
142
+ // Get color for this group
143
+ const color = set.groupColors[customCell.customGroup] || set.defaultColor;
144
+
145
+ // Apply size factor
146
+ const radiusMeters = baseRadiusMeters * customCell.sizeFactor;
147
+
148
+ // Generate arc feature
149
+ const feature = generateCellArc(cell, radiusMeters, 50, color);
150
+
151
+ // Add custom properties for tooltips
152
+ if (feature.properties) {
153
+ feature.properties.customGroup = customCell.customGroup;
154
+ feature.properties.sizeFactor = customCell.sizeFactor;
155
+ feature.properties.setName = set.name;
156
+
157
+ // Add extra fields
158
+ for (const [key, value] of Object.entries(customCell.extraFields)) {
159
+ feature.properties[`extra_${key}`] = value;
160
+ }
161
+ }
162
+
163
+ features.push(feature);
164
+ }
165
+
166
+ // Update source
167
+ const sourceId = getSourceId(set.id);
168
+ const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
169
+ if (source) {
170
+ source.setData({
171
+ type: 'FeatureCollection',
172
+ features: features as any
173
+ });
174
+ }
175
+
176
+ // Update layer opacity
177
+ const { fill } = getLayerIds(set.id);
178
+ if (map.getLayer(fill)) {
179
+ map.setPaintProperty(fill, 'fill-opacity', set.opacity);
180
+ }
181
+
182
+ console.log(`[CustomCellsLayer] Rendered ${features.length} features for set "${set.name}"`);
183
+ }
184
+
185
+ /**
186
+ * Main update function
187
+ */
188
+ function updateLayers() {
189
+ const map = mapStore.map;
190
+ if (!map) return;
191
+
192
+ clearTimeout(updateTimeout);
193
+ updateTimeout = setTimeout(() => {
194
+ const setsToRender = setId
195
+ ? setsStore.sets.filter(s => s.id === setId)
196
+ : setsStore.sets;
197
+
198
+ // Track which sets we're rendering
199
+ const activeSetIds = new Set(setsToRender.map(s => s.id));
200
+
201
+ // Remove layers for sets that no longer exist
202
+ for (const sourceId of activeSources) {
203
+ const setIdFromSource = sourceId.replace('custom-cells-', '');
204
+ if (!activeSetIds.has(setIdFromSource)) {
205
+ removeSetLayers(map, setIdFromSource);
206
+ }
207
+ }
208
+
209
+ // Render each set
210
+ for (const set of setsToRender) {
211
+ if (set.visible) {
212
+ addSetLayers(map, set);
213
+ renderSet(map, set);
214
+ } else {
215
+ // Hide by clearing data
216
+ const sourceId = getSourceId(set.id);
217
+ const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
218
+ if (source) {
219
+ source.setData({ type: 'FeatureCollection', features: [] });
220
+ }
221
+ }
222
+ }
223
+ }, 100);
224
+ }
225
+
226
+ // Setup and reactive updates
227
+ $effect(() => {
228
+ const map = mapStore.map;
229
+ if (!map) return;
230
+
231
+ const onMapEvent = () => updateLayers();
232
+
233
+ // Initial render
234
+ updateLayers();
235
+
236
+ // Listen to map events
237
+ map.on('moveend', onMapEvent);
238
+ map.on('zoomend', onMapEvent);
239
+ map.on('style.load', onMapEvent);
240
+
241
+ return () => {
242
+ map.off('moveend', onMapEvent);
243
+ map.off('zoomend', onMapEvent);
244
+ map.off('style.load', onMapEvent);
245
+
246
+ // Cleanup all layers
247
+ for (const sourceId of activeSources) {
248
+ const setIdFromSource = sourceId.replace('custom-cells-', '');
249
+ removeSetLayers(map, setIdFromSource);
250
+ }
251
+ };
252
+ });
253
+
254
+ // React to store changes
255
+ $effect(() => {
256
+ // Read version to trigger on changes
257
+ const _version = setsStore.version;
258
+ const _sets = setsStore.sets;
259
+
260
+ updateLayers();
261
+ });
262
+ </script>
@@ -0,0 +1,10 @@
1
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
2
+ interface Props {
3
+ /** The custom cell sets store */
4
+ setsStore: CustomCellSetsStore;
5
+ /** Optional: specific set ID to render (if not provided, renders all) */
6
+ setId?: string;
7
+ }
8
+ declare const CustomCellsLayer: import("svelte").Component<Props, {}, "">;
9
+ type CustomCellsLayer = ReturnType<typeof CustomCellsLayer>;
10
+ export default CustomCellsLayer;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Custom Cells Layers - Barrel Export
3
+ */
4
+ export { default as CustomCellsLayer } from './CustomCellsLayer.svelte';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Custom Cells Layers - Barrel Export
3
+ */
4
+ export { default as CustomCellsLayer } from './CustomCellsLayer.svelte';