@smartnet360/svelte-components 0.0.57 → 0.0.59

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 (44) hide show
  1. package/dist/map-v2/core/components/ViewportSync.svelte +79 -0
  2. package/dist/map-v2/core/components/ViewportSync.svelte.d.ts +8 -0
  3. package/dist/map-v2/core/index.d.ts +2 -0
  4. package/dist/map-v2/core/index.js +3 -0
  5. package/dist/map-v2/core/stores/viewportStore.svelte.d.ts +51 -0
  6. package/dist/map-v2/core/stores/viewportStore.svelte.js +120 -0
  7. package/dist/map-v2/demo/DemoMap.svelte +30 -3
  8. package/dist/map-v2/demo/demo-cells.d.ts +12 -0
  9. package/dist/map-v2/demo/demo-cells.js +114 -0
  10. package/dist/map-v2/demo/index.d.ts +1 -0
  11. package/dist/map-v2/demo/index.js +1 -0
  12. package/dist/map-v2/features/cells/constants/colors.d.ts +7 -0
  13. package/dist/map-v2/features/cells/constants/colors.js +21 -0
  14. package/dist/map-v2/features/cells/constants/radiusMultipliers.d.ts +8 -0
  15. package/dist/map-v2/features/cells/constants/radiusMultipliers.js +22 -0
  16. package/dist/map-v2/features/cells/constants/statusStyles.d.ts +7 -0
  17. package/dist/map-v2/features/cells/constants/statusStyles.js +49 -0
  18. package/dist/map-v2/features/cells/constants/zIndex.d.ts +14 -0
  19. package/dist/map-v2/features/cells/constants/zIndex.js +28 -0
  20. package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +242 -0
  21. package/dist/map-v2/features/cells/controls/CellFilterControl.svelte.d.ts +14 -0
  22. package/dist/map-v2/features/cells/controls/CellStyleControl.svelte +139 -0
  23. package/dist/map-v2/features/cells/controls/CellStyleControl.svelte.d.ts +14 -0
  24. package/dist/map-v2/features/cells/index.d.ts +20 -0
  25. package/dist/map-v2/features/cells/index.js +24 -0
  26. package/dist/map-v2/features/cells/layers/CellsLayer.svelte +235 -0
  27. package/dist/map-v2/features/cells/layers/CellsLayer.svelte.d.ts +10 -0
  28. package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.d.ts +46 -0
  29. package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.js +137 -0
  30. package/dist/map-v2/features/cells/types.d.ts +99 -0
  31. package/dist/map-v2/features/cells/types.js +12 -0
  32. package/dist/map-v2/features/cells/utils/arcGeometry.d.ts +36 -0
  33. package/dist/map-v2/features/cells/utils/arcGeometry.js +55 -0
  34. package/dist/map-v2/features/cells/utils/cellGeoJSON.d.ts +22 -0
  35. package/dist/map-v2/features/cells/utils/cellGeoJSON.js +81 -0
  36. package/dist/map-v2/features/cells/utils/cellTree.d.ts +25 -0
  37. package/dist/map-v2/features/cells/utils/cellTree.js +226 -0
  38. package/dist/map-v2/features/cells/utils/techBandParser.d.ts +11 -0
  39. package/dist/map-v2/features/cells/utils/techBandParser.js +17 -0
  40. package/dist/map-v2/features/cells/utils/zoomScaling.d.ts +42 -0
  41. package/dist/map-v2/features/cells/utils/zoomScaling.js +53 -0
  42. package/dist/map-v2/index.d.ts +3 -2
  43. package/dist/map-v2/index.js +6 -2
  44. package/package.json +1 -1
@@ -0,0 +1,242 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CellFilterControl - Dynamic hierarchical filter control for cells
4
+ *
5
+ * Features:
6
+ * - Level1 & Level2 grouping dropdown selectors
7
+ * - Dynamic tree rebuild on grouping config change
8
+ * - TreeView with checkboxes for filtering
9
+ * - Color pickers on leaf nodes (group-level customization)
10
+ * - Persists tree state and grouping config to localStorage
11
+ */
12
+
13
+ import { onMount } from 'svelte';
14
+ import type { Writable } from 'svelte/store';
15
+ import MapControl from '../../../shared/controls/MapControl.svelte';
16
+ import TreeView from '../../../../core/TreeView/TreeView.svelte';
17
+ import { createTreeStore } from '../../../../core/TreeView/tree.store';
18
+ import { buildCellTree, getFilteredCells } from '../utils/cellTree';
19
+ import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
20
+ import type { CellGroupingField } from '../types';
21
+ import type { TreeStoreValue } from '../../../../core/TreeView/tree.model';
22
+
23
+ interface Props {
24
+ /** Cell store context */
25
+ store: CellStoreContext;
26
+ /** Control position */
27
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
28
+ /** Control title */
29
+ title?: string;
30
+ /** Initially collapsed? */
31
+ initiallyCollapsed?: boolean;
32
+ }
33
+
34
+ let {
35
+ store,
36
+ position = 'top-left',
37
+ title = 'Cell Filter',
38
+ initiallyCollapsed = false
39
+ }: Props = $props();
40
+
41
+ // Grouping options (excluding 'none' from level1)
42
+ const GROUPING_OPTIONS: Exclude<CellGroupingField, 'none'>[] = [
43
+ 'tech',
44
+ 'band',
45
+ 'status',
46
+ 'siteId',
47
+ 'customSubgroup',
48
+ 'type',
49
+ 'planner'
50
+ ];
51
+
52
+ // Level2 options include 'none' for flat tree
53
+ const LEVEL2_OPTIONS: CellGroupingField[] = ['none', ...GROUPING_OPTIONS];
54
+
55
+ let treeStore = $state<Writable<TreeStoreValue> | null>(null);
56
+ let level1 = $state<Exclude<CellGroupingField, 'none'>>(store.groupingConfig.level1);
57
+ let level2 = $state<CellGroupingField>(store.groupingConfig.level2);
58
+
59
+ // Rebuild tree when grouping config or cells change
60
+ function rebuildTree() {
61
+ if (store.cells.length === 0) return;
62
+
63
+ console.log('CellFilterControl: Building tree with config:', { level1, level2 });
64
+
65
+ const treeNodes = buildCellTree(
66
+ store.cells,
67
+ { level1, level2 },
68
+ store.groupColorMap
69
+ );
70
+
71
+ // Create or recreate tree store
72
+ treeStore = createTreeStore({
73
+ nodes: [treeNodes],
74
+ namespace: 'cellular-cell-filter',
75
+ persistState: true,
76
+ defaultExpandAll: false
77
+ });
78
+
79
+ console.log('CellFilterControl: Tree store created');
80
+
81
+ // Subscribe to tree changes and update filtered cells
82
+ if (treeStore) {
83
+ const unsub = treeStore.subscribe((treeValue: TreeStoreValue) => {
84
+ const checkedPaths = treeValue.getCheckedPaths();
85
+ console.log('TreeStore updated, checked paths:', checkedPaths.length);
86
+
87
+ // Convert string[] to Set<string>
88
+ const checkedPathsSet = new Set(checkedPaths);
89
+ const newFilteredCells = getFilteredCells(checkedPathsSet, store.cells);
90
+ console.log('Filtered cells count:', newFilteredCells.length, 'of', store.cells.length);
91
+
92
+ // Update the cell store directly
93
+ store.setFilteredCells(newFilteredCells);
94
+ });
95
+
96
+ return () => unsub();
97
+ }
98
+ }
99
+
100
+ onMount(() => {
101
+ console.log('CellFilterControl: Mounted with', store.cells.length, 'cells');
102
+ rebuildTree();
103
+ });
104
+
105
+ // Handle grouping config changes
106
+ function handleLevel1Change(event: Event) {
107
+ const target = event.currentTarget as HTMLSelectElement;
108
+ level1 = target.value as Exclude<CellGroupingField, 'none'>;
109
+
110
+ // Update store config
111
+ store.setGroupingConfig({ level1, level2 });
112
+
113
+ // Rebuild tree
114
+ rebuildTree();
115
+ }
116
+
117
+ function handleLevel2Change(event: Event) {
118
+ const target = event.currentTarget as HTMLSelectElement;
119
+ level2 = target.value as CellGroupingField;
120
+
121
+ // Update store config
122
+ store.setGroupingConfig({ level1, level2 });
123
+
124
+ // Rebuild tree
125
+ rebuildTree();
126
+ }
127
+
128
+ function handleColorChange(groupKey: string, color: string) {
129
+ store.setGroupColor(groupKey, color);
130
+ }
131
+
132
+ // Label mappings for display
133
+ const FIELD_LABELS: Record<CellGroupingField | 'none', string> = {
134
+ tech: 'Technology',
135
+ band: 'Frequency Band',
136
+ status: 'Status',
137
+ siteId: 'Site ID',
138
+ customSubgroup: 'Custom Subgroup',
139
+ type: 'Type',
140
+ planner: 'Planner',
141
+ none: 'None (2-Level Tree)'
142
+ };
143
+ </script>
144
+
145
+ <MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
146
+ <div class="cell-filter-control">
147
+ <!-- Grouping Configuration -->
148
+ <div class="grouping-config">
149
+ <div class="mb-2">
150
+ <label for="level1-select" class="form-label small mb-1">Level 1 Grouping</label>
151
+ <select
152
+ id="level1-select"
153
+ class="form-select form-select-sm"
154
+ value={level1}
155
+ onchange={handleLevel1Change}
156
+ >
157
+ {#each GROUPING_OPTIONS as option}
158
+ <option value={option}>{FIELD_LABELS[option]}</option>
159
+ {/each}
160
+ </select>
161
+ </div>
162
+
163
+ <div class="mb-3">
164
+ <label for="level2-select" class="form-label small mb-1">Level 2 Grouping</label>
165
+ <select
166
+ id="level2-select"
167
+ class="form-select form-select-sm"
168
+ value={level2}
169
+ onchange={handleLevel2Change}
170
+ >
171
+ {#each LEVEL2_OPTIONS as option}
172
+ <option value={option}>{FIELD_LABELS[option]}</option>
173
+ {/each}
174
+ </select>
175
+ </div>
176
+ </div>
177
+
178
+ <!-- Tree View -->
179
+ {#if treeStore && $treeStore}
180
+ <div class="cell-filter-tree">
181
+ <TreeView store={$treeStore} showControls={false}>
182
+ {#snippet children({ node, state })}
183
+ <!-- Custom node rendering with color picker for leaf nodes -->
184
+ {#if node.metadata?.isLeafGroup}
185
+ <input
186
+ type="color"
187
+ class="color-picker"
188
+ value={node.metadata.color || '#888888'}
189
+ oninput={(e) => handleColorChange(node.id, e.currentTarget.value)}
190
+ onclick={(e) => e.stopPropagation()}
191
+ title="Choose color for this cell group"
192
+ />
193
+ {/if}
194
+ {/snippet}
195
+ </TreeView>
196
+
197
+ <div class="cell-filter-stats">
198
+ <small class="text-muted">
199
+ Showing {store.filteredCells.length} of {store.cells.length} cells
200
+ </small>
201
+ </div>
202
+ </div>
203
+ {:else}
204
+ <div class="text-muted small">Loading cells...</div>
205
+ {/if}
206
+ </div>
207
+ </MapControl>
208
+
209
+ <style>
210
+ .cell-filter-control {
211
+ min-width: 280px;
212
+ max-width: 320px;
213
+ }
214
+
215
+ .grouping-config {
216
+ padding-bottom: 0.5rem;
217
+ border-bottom: 1px solid #dee2e6;
218
+ margin-bottom: 0.75rem;
219
+ }
220
+
221
+ .cell-filter-tree {
222
+ max-height: 400px;
223
+ overflow-y: auto;
224
+ }
225
+
226
+ .cell-filter-stats {
227
+ margin-top: 8px;
228
+ padding-top: 8px;
229
+ border-top: 1px solid #dee2e6;
230
+ text-align: center;
231
+ }
232
+
233
+ .color-picker {
234
+ width: 24px;
235
+ height: 24px;
236
+ border: 1px solid #ccc;
237
+ border-radius: 4px;
238
+ cursor: pointer;
239
+ margin-left: 8px;
240
+ vertical-align: middle;
241
+ }
242
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
2
+ interface Props {
3
+ /** Cell store context */
4
+ store: CellStoreContext;
5
+ /** Control position */
6
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
7
+ /** Control title */
8
+ title?: string;
9
+ /** Initially collapsed? */
10
+ initiallyCollapsed?: boolean;
11
+ }
12
+ declare const CellFilterControl: import("svelte").Component<Props, {}, "">;
13
+ type CellFilterControl = ReturnType<typeof CellFilterControl>;
14
+ export default CellFilterControl;
@@ -0,0 +1,139 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CellStyleControl - Visual settings control for cells
4
+ *
5
+ * Sliders for:
6
+ * - Base radius
7
+ * - Line width
8
+ * - Fill opacity
9
+ * - Show/hide cells
10
+ */
11
+
12
+ import MapControl from '../../../shared/controls/MapControl.svelte';
13
+ import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
14
+
15
+ interface Props {
16
+ /** Cell store context */
17
+ store: CellStoreContext;
18
+ /** Control position */
19
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
20
+ /** Control title */
21
+ title?: string;
22
+ /** Initially collapsed? */
23
+ initiallyCollapsed?: boolean;
24
+ }
25
+
26
+ let {
27
+ store,
28
+ position = 'bottom-left',
29
+ title = 'Cell Display',
30
+ initiallyCollapsed = false
31
+ }: Props = $props();
32
+ </script>
33
+
34
+ <MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
35
+ <div class="cell-style-controls">
36
+ <!-- Show cells toggle -->
37
+ <div class="control-row">
38
+ <label for="cell-show-toggle" class="control-label">
39
+ Show Cells
40
+ </label>
41
+ <div class="form-check form-switch">
42
+ <input
43
+ id="cell-show-toggle"
44
+ type="checkbox"
45
+ class="form-check-input"
46
+ role="switch"
47
+ bind:checked={store.showCells}
48
+ />
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Base radius slider -->
53
+ <div class="control-row">
54
+ <label for="cell-radius-slider" class="control-label">
55
+ Base Radius: <strong>{store.baseRadius}m</strong>
56
+ </label>
57
+ <input
58
+ id="cell-radius-slider"
59
+ type="range"
60
+ class="form-range"
61
+ min="100"
62
+ max="2000"
63
+ step="50"
64
+ bind:value={store.baseRadius}
65
+ />
66
+ </div>
67
+
68
+ <!-- Line width slider -->
69
+ <div class="control-row">
70
+ <label for="cell-line-width-slider" class="control-label">
71
+ Line Width: <strong>{store.lineWidth}px</strong>
72
+ </label>
73
+ <input
74
+ id="cell-line-width-slider"
75
+ type="range"
76
+ class="form-range"
77
+ min="1"
78
+ max="5"
79
+ step="0.5"
80
+ bind:value={store.lineWidth}
81
+ />
82
+ </div>
83
+
84
+ <!-- Fill opacity slider -->
85
+ <div class="control-row">
86
+ <label for="cell-opacity-slider" class="control-label">
87
+ Fill Opacity: <strong>{Math.round(store.fillOpacity * 100)}%</strong>
88
+ </label>
89
+ <input
90
+ id="cell-opacity-slider"
91
+ type="range"
92
+ class="form-range"
93
+ min="0"
94
+ max="1"
95
+ step="0.1"
96
+ bind:value={store.fillOpacity}
97
+ />
98
+ </div>
99
+ </div>
100
+ </MapControl>
101
+
102
+ <style>
103
+ .cell-style-controls {
104
+ min-width: 250px;
105
+ padding: 0.5rem 0;
106
+ }
107
+
108
+ .control-row {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ gap: 1rem;
113
+ margin-bottom: 1rem;
114
+ }
115
+
116
+ .control-row:last-child {
117
+ margin-bottom: 0;
118
+ }
119
+
120
+ .control-label {
121
+ font-size: 0.875rem;
122
+ font-weight: 500;
123
+ margin: 0;
124
+ white-space: nowrap;
125
+ }
126
+
127
+ .form-range {
128
+ flex: 1;
129
+ min-width: 120px;
130
+ }
131
+
132
+ .form-check {
133
+ margin: 0;
134
+ }
135
+
136
+ .form-switch .form-check-input {
137
+ cursor: pointer;
138
+ }
139
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
2
+ interface Props {
3
+ /** Cell store context */
4
+ store: CellStoreContext;
5
+ /** Control position */
6
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
7
+ /** Control title */
8
+ title?: string;
9
+ /** Initially collapsed? */
10
+ initiallyCollapsed?: boolean;
11
+ }
12
+ declare const CellStyleControl: import("svelte").Component<Props, {}, "">;
13
+ type CellStyleControl = ReturnType<typeof CellStyleControl>;
14
+ export default CellStyleControl;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cell Feature - Public API
3
+ *
4
+ * Exports all cell-related types, components, utilities, and constants
5
+ */
6
+ export type { Cell, CellStatus, CellStatusStyle, TechnologyBandKey, ParsedTechBand, CellGroupingField, CellTreeConfig } from './types';
7
+ export { DEFAULT_CELL_TREE_CONFIG } from './types';
8
+ export { TECHNOLOGY_BAND_COLORS } from './constants/colors';
9
+ export { RADIUS_MULTIPLIER } from './constants/radiusMultipliers';
10
+ export { Z_INDEX_BY_BAND, CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from './constants/zIndex';
11
+ export { DEFAULT_STATUS_STYLES } from './constants/statusStyles';
12
+ export { createCellStoreContext, type CellStoreContext, type CellStoreValue } from './stores/cellStoreContext.svelte';
13
+ export { default as CellsLayer } from './layers/CellsLayer.svelte';
14
+ export { default as CellFilterControl } from './controls/CellFilterControl.svelte';
15
+ export { default as CellStyleControl } from './controls/CellStyleControl.svelte';
16
+ export { parseTechBand } from './utils/techBandParser';
17
+ export { getZoomFactor, calculateRadius } from './utils/zoomScaling';
18
+ export { createArcPolygon } from './utils/arcGeometry';
19
+ export { buildCellTree, getFilteredCells } from './utils/cellTree';
20
+ export { cellsToGeoJSON } from './utils/cellGeoJSON';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Cell Feature - Public API
3
+ *
4
+ * Exports all cell-related types, components, utilities, and constants
5
+ */
6
+ export { DEFAULT_CELL_TREE_CONFIG } from './types';
7
+ // Constants
8
+ export { TECHNOLOGY_BAND_COLORS } from './constants/colors';
9
+ export { RADIUS_MULTIPLIER } from './constants/radiusMultipliers';
10
+ export { Z_INDEX_BY_BAND, CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from './constants/zIndex';
11
+ export { DEFAULT_STATUS_STYLES } from './constants/statusStyles';
12
+ // Store
13
+ export { createCellStoreContext } from './stores/cellStoreContext.svelte';
14
+ // Layers
15
+ export { default as CellsLayer } from './layers/CellsLayer.svelte';
16
+ // Controls
17
+ export { default as CellFilterControl } from './controls/CellFilterControl.svelte';
18
+ export { default as CellStyleControl } from './controls/CellStyleControl.svelte';
19
+ // Utilities
20
+ export { parseTechBand } from './utils/techBandParser';
21
+ export { getZoomFactor, calculateRadius } from './utils/zoomScaling';
22
+ export { createArcPolygon } from './utils/arcGeometry';
23
+ export { buildCellTree, getFilteredCells } from './utils/cellTree';
24
+ export { cellsToGeoJSON } from './utils/cellGeoJSON';
@@ -0,0 +1,235 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CellsLayer - Renders cell sectors as arc polygons
4
+ *
5
+ * Features:
6
+ * - Mapbox fill layer (colored arcs by tech-band)
7
+ * - Mapbox line layer (borders styled by status)
8
+ * - Zoom-reactive: Regenerates arcs on zoomend
9
+ * - Updates when filteredCells or store settings change
10
+ */
11
+
12
+ import { getContext, onDestroy, onMount } from 'svelte';
13
+ import type { Map as MapboxMap } from 'mapbox-gl';
14
+ import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
15
+ import { useMapbox } from '../../../core/hooks/useMapbox';
16
+ import { cellsToGeoJSON } from '../utils/cellGeoJSON';
17
+ import { CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from '../constants/zIndex';
18
+
19
+ interface Props {
20
+ /** Cell store context */
21
+ store: CellStoreContext;
22
+ /** Unique namespace for layer IDs */
23
+ namespace: string;
24
+ }
25
+
26
+ let { store, namespace }: Props = $props();
27
+
28
+ const FILL_LAYER_ID = `${namespace}-cells-fill`;
29
+ const LINE_LAYER_ID = `${namespace}-cells-line`;
30
+ const SOURCE_ID = `${namespace}-cells`;
31
+
32
+ // Get map from mapbox hook
33
+ const mapStore = useMapbox();
34
+
35
+ let map = $state<MapboxMap | null>(null);
36
+ let mounted = $state(false);
37
+ let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
38
+
39
+ // Viewport change handler (pan/zoom/move) with debouncing
40
+ function handleMoveEnd() {
41
+ if (!map) return;
42
+
43
+ // Clear any existing timer
44
+ if (viewportUpdateTimer) {
45
+ clearTimeout(viewportUpdateTimer);
46
+ }
47
+
48
+ // Debounce: wait 200ms after last move event before re-rendering
49
+ viewportUpdateTimer = setTimeout(() => {
50
+ if (!map) return;
51
+ const newZoom = map.getZoom();
52
+ console.log('CellsLayer: Viewport changed (zoom/pan), updating to zoom:', newZoom);
53
+ // This will trigger $effect to re-run with new viewport bounds and zoom
54
+ store.setCurrentZoom(newZoom);
55
+ }, 200);
56
+ }
57
+
58
+ onMount(() => {
59
+ console.log('CellsLayer: onMount, waiting for map...');
60
+
61
+ // Subscribe to map store
62
+ const unsubscribe = mapStore.subscribe((mapInstance) => {
63
+ if (mapInstance && !map) {
64
+ console.log('CellsLayer: Map available, initializing...');
65
+ map = mapInstance;
66
+ mounted = true;
67
+
68
+ // Set initial zoom
69
+ store.setCurrentZoom(mapInstance.getZoom());
70
+
71
+ // Listen to viewport changes (pan + zoom, debounced)
72
+ mapInstance.on('moveend', handleMoveEnd);
73
+ }
74
+ });
75
+
76
+ return () => {
77
+ unsubscribe();
78
+ };
79
+ });
80
+
81
+ onDestroy(() => {
82
+ if (!map) return;
83
+
84
+ // Clean up timer
85
+ if (viewportUpdateTimer) {
86
+ clearTimeout(viewportUpdateTimer);
87
+ }
88
+
89
+ map.off('moveend', handleMoveEnd);
90
+
91
+ // Clean up layers and source
92
+ if (map.getLayer(LINE_LAYER_ID)) {
93
+ map.removeLayer(LINE_LAYER_ID);
94
+ }
95
+ if (map.getLayer(FILL_LAYER_ID)) {
96
+ map.removeLayer(FILL_LAYER_ID);
97
+ }
98
+ if (map.getSource(SOURCE_ID)) {
99
+ map.removeSource(SOURCE_ID);
100
+ }
101
+ });
102
+
103
+ // Reactive: Update GeoJSON when cells/zoom/filters/settings change
104
+ $effect(() => {
105
+ console.log('CellsLayer $effect triggered:', {
106
+ mounted,
107
+ hasMap: !!map,
108
+ showCells: store.showCells,
109
+ filteredCellsCount: store.filteredCells.length,
110
+ currentZoom: store.currentZoom,
111
+ baseRadius: store.baseRadius
112
+ });
113
+
114
+ if (!mounted || !map || !store.showCells) {
115
+ // Remove layers if showCells is false
116
+ if (mounted && map) {
117
+ console.log('CellsLayer: Removing layers (showCells=false or not mounted)');
118
+ if (map.getLayer(LINE_LAYER_ID)) {
119
+ map.removeLayer(LINE_LAYER_ID);
120
+ }
121
+ if (map.getLayer(FILL_LAYER_ID)) {
122
+ map.removeLayer(FILL_LAYER_ID);
123
+ }
124
+ if (map.getSource(SOURCE_ID)) {
125
+ map.removeSource(SOURCE_ID);
126
+ }
127
+ }
128
+ return;
129
+ }
130
+
131
+ // Filter cells by viewport bounds (only render visible cells)
132
+ const bounds = map.getBounds();
133
+ if (!bounds) {
134
+ console.warn('CellsLayer: Cannot get map bounds, skipping viewport filter');
135
+ return;
136
+ }
137
+
138
+ const visibleCells = store.filteredCells.filter(cell =>
139
+ bounds.contains([cell.longitude, cell.latitude])
140
+ );
141
+
142
+ console.log('CellsLayer: Viewport filtering:', {
143
+ totalFiltered: store.filteredCells.length,
144
+ visibleInViewport: visibleCells.length,
145
+ bounds: {
146
+ north: bounds.getNorth(),
147
+ south: bounds.getSouth(),
148
+ east: bounds.getEast(),
149
+ west: bounds.getWest()
150
+ }
151
+ });
152
+
153
+ // Generate GeoJSON from visible cells only
154
+ const geoJSON = cellsToGeoJSON(
155
+ visibleCells,
156
+ store.currentZoom,
157
+ store.baseRadius,
158
+ store.groupColorMap
159
+ );
160
+
161
+ console.log('CellsLayer: Generated GeoJSON:', {
162
+ featureCount: geoJSON.features.length,
163
+ firstFeature: geoJSON.features[0]
164
+ });
165
+
166
+ // Update or create source
167
+ const source = map.getSource(SOURCE_ID);
168
+ if (source && source.type === 'geojson') {
169
+ console.log('CellsLayer: Updating existing source');
170
+ source.setData(geoJSON);
171
+ } else {
172
+ console.log('CellsLayer: Creating new source');
173
+ map.addSource(SOURCE_ID, {
174
+ type: 'geojson',
175
+ data: geoJSON
176
+ });
177
+ }
178
+
179
+ // Add fill layer if not exists
180
+ if (!map.getLayer(FILL_LAYER_ID)) {
181
+ console.log('CellsLayer: Creating fill layer');
182
+ map.addLayer({
183
+ id: FILL_LAYER_ID,
184
+ type: 'fill',
185
+ source: SOURCE_ID,
186
+ paint: {
187
+ 'fill-color': [
188
+ 'coalesce',
189
+ ['get', 'groupColor'],
190
+ ['get', 'techBandColor'],
191
+ '#888888' // Fallback
192
+ ],
193
+ 'fill-opacity': store.fillOpacity
194
+ },
195
+ metadata: {
196
+ zIndex: CELL_FILL_Z_INDEX
197
+ }
198
+ });
199
+ } else {
200
+ // Update fill opacity
201
+ console.log('CellsLayer: Updating fill opacity:', store.fillOpacity);
202
+ map.setPaintProperty(FILL_LAYER_ID, 'fill-opacity', store.fillOpacity);
203
+ }
204
+
205
+ // Add line layer if not exists
206
+ if (!map.getLayer(LINE_LAYER_ID)) {
207
+ console.log('CellsLayer: Creating line layer');
208
+ map.addLayer({
209
+ id: LINE_LAYER_ID,
210
+ type: 'line',
211
+ source: SOURCE_ID,
212
+ paint: {
213
+ 'line-color': ['get', 'lineColor'],
214
+ 'line-width': store.lineWidth,
215
+ 'line-opacity': ['get', 'lineOpacity'],
216
+ 'line-dasharray': [
217
+ 'case',
218
+ ['has', 'dashArray'],
219
+ ['get', 'dashArray'],
220
+ ['literal', [1, 0]] // Solid line default
221
+ ]
222
+ },
223
+ metadata: {
224
+ zIndex: CELL_LINE_Z_INDEX
225
+ }
226
+ });
227
+ } else {
228
+ // Update line width
229
+ console.log('CellsLayer: Updating line width:', store.lineWidth);
230
+ map.setPaintProperty(LINE_LAYER_ID, 'line-width', store.lineWidth);
231
+ }
232
+ });
233
+ </script>
234
+
235
+ <!-- This component doesn't render DOM, only Mapbox layers -->
@@ -0,0 +1,10 @@
1
+ import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
2
+ interface Props {
3
+ /** Cell store context */
4
+ store: CellStoreContext;
5
+ /** Unique namespace for layer IDs */
6
+ namespace: string;
7
+ }
8
+ declare const CellsLayer: import("svelte").Component<Props, {}, "">;
9
+ type CellsLayer = ReturnType<typeof CellsLayer>;
10
+ export default CellsLayer;