@smartnet360/svelte-components 0.0.87 → 0.0.89

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 (34) hide show
  1. package/dist/map-v3/core/components/MapStoreBridge.svelte +25 -0
  2. package/dist/map-v3/core/components/MapStoreBridge.svelte.d.ts +8 -0
  3. package/dist/map-v3/core/index.d.ts +1 -0
  4. package/dist/map-v3/core/index.js +1 -0
  5. package/dist/map-v3/demo/DemoMap.svelte +10 -0
  6. package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -6
  7. package/dist/map-v3/features/cells/components/CellFilterControl.svelte.d.ts +3 -0
  8. package/dist/map-v3/features/cells/constants/statusStyles.d.ts +14 -0
  9. package/dist/map-v3/features/cells/constants/statusStyles.js +49 -0
  10. package/dist/map-v3/features/cells/constants.d.ts +1 -0
  11. package/dist/map-v3/features/cells/constants.js +54 -13
  12. package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte +7 -2
  13. package/dist/map-v3/features/cells/layers/CellsLayer.svelte +28 -12
  14. package/dist/map-v3/features/cells/logic/geometry.d.ts +1 -1
  15. package/dist/map-v3/features/cells/logic/geometry.js +26 -5
  16. package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -0
  17. package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte +57 -26
  18. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +309 -0
  19. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +20 -0
  20. package/dist/map-v3/features/selection/index.d.ts +9 -0
  21. package/dist/map-v3/features/selection/index.js +10 -0
  22. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +209 -0
  23. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +13 -0
  24. package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +84 -0
  25. package/dist/map-v3/features/selection/stores/selection.store.svelte.js +174 -0
  26. package/dist/map-v3/features/selection/types.d.ts +25 -0
  27. package/dist/map-v3/features/selection/types.js +4 -0
  28. package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +14 -0
  29. package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte +57 -26
  30. package/dist/map-v3/index.d.ts +1 -0
  31. package/dist/map-v3/index.js +2 -0
  32. package/dist/map-v3/shared/controls/MapControl.svelte +37 -10
  33. package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +2 -0
  34. package/package.json +1 -1
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import type { MapStore } from '../stores/map.store.svelte';
4
+ import type { Map as MapboxMap } from 'mapbox-gl';
5
+
6
+ interface Props {
7
+ onMapChange?: (map: MapboxMap | null) => void;
8
+ children?: import('svelte').Snippet;
9
+ }
10
+
11
+ let { onMapChange, children }: Props = $props();
12
+
13
+ const mapStore = getContext<MapStore>('MAP_CONTEXT');
14
+ let ready = $state(false);
15
+
16
+ $effect(() => {
17
+ const map = mapStore.map;
18
+ onMapChange?.(map);
19
+ ready = map !== null;
20
+ });
21
+ </script>
22
+
23
+ {#if ready && children}
24
+ {@render children()}
25
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { Map as MapboxMap } from 'mapbox-gl';
2
+ interface Props {
3
+ onMapChange?: (map: MapboxMap | null) => void;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+ declare const MapStoreBridge: import("svelte").Component<Props, {}, "">;
7
+ type MapStoreBridge = ReturnType<typeof MapStoreBridge>;
8
+ export default MapStoreBridge;
@@ -2,5 +2,6 @@ export { MapStore, createMapStore } from './stores/map.store.svelte';
2
2
  export type { ViewportStore, ViewportState } from './stores/viewport.store.svelte';
3
3
  export { createViewportStore } from './stores/viewport.store.svelte';
4
4
  export { default as Map } from './components/Map.svelte';
5
+ export { default as MapStoreBridge } from './components/MapStoreBridge.svelte';
5
6
  export { default as MapStyleControl } from './controls/MapStyleControl.svelte';
6
7
  export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
@@ -1,5 +1,6 @@
1
1
  export { MapStore, createMapStore } from './stores/map.store.svelte';
2
2
  export { createViewportStore } from './stores/viewport.store.svelte';
3
3
  export { default as Map } from './components/Map.svelte';
4
+ export { default as MapStoreBridge } from './components/MapStoreBridge.svelte';
4
5
  export { default as MapStyleControl } from './controls/MapStyleControl.svelte';
5
6
  export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
@@ -19,6 +19,7 @@
19
19
  import SiteLabelsLayer from '../features/sites/layers/SiteLabelsLayer.svelte';
20
20
  import SiteFilterControl from '../features/sites/components/SiteFilterControl.svelte';
21
21
  import { createSiteRegistry } from '../features/sites/stores/site.registry.svelte';
22
+ import FeatureSelectionControl from '../features/selection/components/FeatureSelectionControl.svelte';
22
23
  import { demoCells } from './demo-cells';
23
24
  import { demoRepeaters } from './demo-repeaters';
24
25
 
@@ -93,6 +94,15 @@
93
94
  <CellLabelsLayer dataStore={cellData} registry={cellRegistry} displayStore={cellDisplay} />
94
95
  <RepeatersLayer dataStore={repeaterData} registry={repeaterRegistry} displayStore={repeaterDisplay} />
95
96
  <RepeaterLabelsLayer dataStore={repeaterData} registry={repeaterRegistry} displayStore={repeaterDisplay} />
97
+
98
+ <FeatureSelectionControl
99
+ position="bottom-left"
100
+ cellDataStore={cellData}
101
+ cellDisplayStore={cellDisplay}
102
+ title="Feature Selection"
103
+ actionButtonLabel="Export Selected"
104
+ onAction={(ids) => console.log('Selected features:', ids)}
105
+ />
96
106
  </Map>
97
107
  </div>
98
108
 
@@ -14,17 +14,25 @@
14
14
  registry: CellRegistry;
15
15
  displayStore: CellDisplayStore;
16
16
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
17
+ level1Options?: CellGroupingField[];
18
+ level2Options?: CellGroupingField[];
17
19
  }
18
20
 
19
- let { dataStore, registry, displayStore, position = 'top-left' }: Props = $props();
21
+ let {
22
+ dataStore,
23
+ registry,
24
+ displayStore,
25
+ position = 'top-left',
26
+ level1Options = ['tech', 'status', 'siteId'],
27
+ level2Options = ['fband', 'frq', 'status', 'none']
28
+ }: Props = $props();
20
29
 
21
30
  // Local State for Grouping
22
- // let level1 = $state<CellGroupingField>('tech');
23
- // let level2 = $state<CellGroupingField>('fband');
24
31
  let includePlanned = $state(false);
25
32
 
26
- const level1Options: CellGroupingField[] = ['tech', 'status', 'siteId'];
27
- const level2Options: CellGroupingField[] = ['fband', 'frq', 'status', 'none'];
33
+ function toggleLabels() {
34
+ displayStore.showLabels = !displayStore.showLabels;
35
+ }
28
36
 
29
37
  // Sync includePlanned with dataStore
30
38
  $effect(() => {
@@ -106,7 +114,17 @@
106
114
  }
107
115
  </script>
108
116
 
109
- <MapControl {position} title="Layers" icon="radioactive" controlWidth="320px">
117
+ <MapControl {position} title="Cells" icon="grid" controlWidth="320px">
118
+ {#snippet actions()}
119
+ <button
120
+ class="btn btn-sm btn-outline-secondary border-0 p-1 px-2"
121
+ title={displayStore.showLabels ? 'Hide Labels' : 'Show Labels'}
122
+ aria-label={displayStore.showLabels ? 'Hide Labels' : 'Show Labels'}
123
+ onclick={toggleLabels}
124
+ >
125
+ <i class="bi bi-tag{displayStore.showLabels ? '-fill' : ''}"></i>
126
+ </button>
127
+ {/snippet}
110
128
  <div class="cell-filter-control">
111
129
  <!-- Status Filter Checkbox -->
112
130
  <div class="mb-3 px-1">
@@ -1,11 +1,14 @@
1
1
  import type { CellDataStore } from '../stores/cell.data.svelte';
2
2
  import type { CellRegistry } from '../stores/cell.registry.svelte';
3
3
  import type { CellDisplayStore } from '../stores/cell.display.svelte';
4
+ import type { CellGroupingField } from '../types';
4
5
  interface Props {
5
6
  dataStore: CellDataStore;
6
7
  registry: CellRegistry;
7
8
  displayStore: CellDisplayStore;
8
9
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
10
+ level1Options?: CellGroupingField[];
11
+ level2Options?: CellGroupingField[];
9
12
  }
10
13
  declare const CellFilterControl: import("svelte").Component<Props, {}, "">;
11
14
  type CellFilterControl = ReturnType<typeof CellFilterControl>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Default Status Styles
3
+ *
4
+ * Visual styling for cell sector borders based on status
5
+ */
6
+ import type { CellStatus } from '../types';
7
+ export interface CellStatusStyle {
8
+ lineType: 'solid' | 'dashed' | 'dotted';
9
+ lineWidth: number;
10
+ lineColor: string;
11
+ opacity: number;
12
+ dashArray: number[];
13
+ }
14
+ export declare const DEFAULT_STATUS_STYLES: Record<CellStatus, CellStatusStyle>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Default Status Styles
3
+ *
4
+ * Visual styling for cell sector borders based on status
5
+ */
6
+ export const DEFAULT_STATUS_STYLES = {
7
+ On_Air: {
8
+ lineType: 'solid',
9
+ lineWidth: 2,
10
+ lineColor: '#000000',
11
+ opacity: 1.0,
12
+ dashArray: []
13
+ },
14
+ On_Air_UNDER_CONSTRUCTION: {
15
+ lineType: 'solid',
16
+ lineWidth: 2,
17
+ lineColor: '#FFA500',
18
+ opacity: 0.8,
19
+ dashArray: []
20
+ },
21
+ On_Air_Locked: {
22
+ lineType: 'solid',
23
+ lineWidth: 2,
24
+ lineColor: '#FF0000',
25
+ opacity: 0.9,
26
+ dashArray: []
27
+ },
28
+ RF_Plan_Ready: {
29
+ lineType: 'dashed',
30
+ lineWidth: 2,
31
+ lineColor: '#666666',
32
+ opacity: 0.6,
33
+ dashArray: [5, 5]
34
+ },
35
+ 'Re-Planned_RF_Plan_Ready': {
36
+ lineType: 'dashed',
37
+ lineWidth: 2,
38
+ lineColor: '#888888',
39
+ opacity: 0.5,
40
+ dashArray: [5, 5]
41
+ },
42
+ Tavlati_RF_Plan_Ready: {
43
+ lineType: 'dashed',
44
+ lineWidth: 2,
45
+ lineColor: '#999999',
46
+ opacity: 0.4,
47
+ dashArray: [5, 5]
48
+ }
49
+ };
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { TechnologyBandKey } from './types';
8
8
  export declare const Z_INDEX_BY_BAND: Record<TechnologyBandKey, number>;
9
+ export declare const BEAMWIDTH_BOOST_BY_BAND: Record<TechnologyBandKey, number>;
9
10
  /**
10
11
  * Base Z-Index for Mapbox layer ordering
11
12
  * Cells should render below sites but above base map features
@@ -5,20 +5,61 @@
5
5
  * Higher frequency bands typically rendered on top
6
6
  */
7
7
  export const Z_INDEX_BY_BAND = {
8
- // 2G bands
9
- '2G_900': 3,
10
- '2G_1800': 7,
11
- // 4G bands
8
+ // '2G_900': 1,
9
+ // '2G_1800': 3,
10
+ // '4G_700': 7,
11
+ // '4G_800': 9,
12
+ // '4G_900': 11,
13
+ // '4G_1800': 13,
14
+ // '4G_2100': 15,
15
+ // '4G_2600': 19,
16
+ // '5G_700': 23,
17
+ // '5G_2100': 25,
18
+ // '5G_3500': 27,
19
+ // //low
20
+ // '4G_700': 0,
21
+ // '5G_700': 2,
22
+ // '4G_800': 4,
23
+ // '2G_900': 6,
24
+ // '4G_900': 8,
25
+ // //mid
26
+ // '2G_1800': 14,
27
+ // '4G_1800': 16,
28
+ // '4G_2100': 18,
29
+ // '5G_2100': 20,
30
+ // '4G_2600': 22,
31
+ // //high
32
+ // '5G_3500': 28,
33
+ //low
34
+ '4G_700': 1,
35
+ '5G_700': 0.99,
36
+ '4G_800': 5,
37
+ '2G_900': 7,
38
+ '4G_900': 9,
39
+ //mid
40
+ '4G_1800': 15,
41
+ '2G_1800': 14.99,
42
+ '4G_2100': 19,
43
+ '5G_2100': 18.99,
44
+ '4G_2600': 23,
45
+ //high
46
+ '5G_3500': 25,
47
+ };
48
+ export const BEAMWIDTH_BOOST_BY_BAND = {
49
+ // Low band
12
50
  '4G_700': 0,
13
- '4G_800': 2,
14
- '4G_900': 4,
15
- '4G_1800': 8,
16
- '4G_2100': 9,
17
- '4G_2600': 11,
18
- // 5G bands
19
- '5G_700': 1,
20
- '5G_2100': 10,
21
- '5G_3500': 14
51
+ '5G_700': 15,
52
+ '4G_800': 0,
53
+ '2G_900': 0,
54
+ '4G_900': 0,
55
+ // Mid band
56
+ '2G_1800': 15,
57
+ '4G_1800': 0,
58
+ '4G_2100': 0,
59
+ '5G_2100': 15,
60
+ '4G_2600': 0,
61
+ // High band
62
+ '5G_3500': 0
22
63
  };
23
64
  /**
24
65
  * Base Z-Index for Mapbox layer ordering
@@ -214,13 +214,18 @@
214
214
  }
215
215
  };
216
216
 
217
+ const handleStyleLoad = () => {
218
+ addLayers();
219
+ updateLayer();
220
+ };
221
+
217
222
  addLayers();
218
- map.on('style.load', addLayers);
223
+ map.on('style.load', handleStyleLoad);
219
224
  map.on('moveend', updateLayer);
220
225
  map.on('zoomend', updateLayer);
221
226
 
222
227
  return () => {
223
- map.off('style.load', addLayers);
228
+ map.off('style.load', handleStyleLoad);
224
229
  map.off('moveend', updateLayer);
225
230
  map.off('zoomend', updateLayer);
226
231
  if (map.getLayer(layerId)) map.removeLayer(layerId);
@@ -6,7 +6,7 @@
6
6
  import type { CellDisplayStore } from '../stores/cell.display.svelte';
7
7
  import { groupCells, getColorForGroup } from '../logic/grouping';
8
8
  import { generateCellArc, calculateRadiusInMeters } from '../logic/geometry';
9
- import { Z_INDEX_BY_BAND } from '../constants';
9
+ import { Z_INDEX_BY_BAND, BEAMWIDTH_BOOST_BY_BAND } from '../constants';
10
10
  import type { TechnologyBandKey } from '../types';
11
11
  import type mapboxgl from 'mapbox-gl';
12
12
 
@@ -34,9 +34,8 @@
34
34
  if (map.getLayer(layerId)) {
35
35
  map.setPaintProperty(layerId, 'fill-opacity', displayStore.fillOpacity);
36
36
  }
37
- if (map.getLayer(lineLayerId)) {
38
- map.setPaintProperty(lineLayerId, 'line-width', displayStore.lineWidth);
39
- }
37
+ // Note: Line styling is now data-driven from status properties
38
+ // but we can still apply a global multiplier via lineWidth
40
39
  });
41
40
 
42
41
  $effect(() => {
@@ -68,15 +67,28 @@
68
67
  }
69
68
 
70
69
  if (!map.getLayer(lineLayerId)) {
71
- // Line Layer (Border)
70
+ // Line Layer (Border) - Status-based styling
72
71
  map.addLayer({
73
72
  id: lineLayerId,
74
73
  type: 'line',
75
74
  source: sourceId,
76
75
  paint: {
77
- 'line-color': '#000',
78
- 'line-width': displayStore.lineWidth,
79
- 'line-opacity': 0.3
76
+ 'line-color': ['get', 'lineColor'],
77
+ 'line-width': [
78
+ '*',
79
+ ['get', 'lineWidth'],
80
+ displayStore.lineWidth
81
+ ],
82
+ 'line-opacity': ['get', 'lineOpacity'],
83
+ 'line-dasharray': [
84
+ 'case',
85
+ ['>', ['length', ['get', 'dashArray']], 0],
86
+ ['get', 'dashArray'],
87
+ ['literal', []]
88
+ ]
89
+ },
90
+ layout: {
91
+ 'line-sort-key': ['get', 'zIndex']
80
92
  }
81
93
  });
82
94
  }
@@ -162,19 +174,23 @@
162
174
  // 4. BBox Filter (Simple point check)
163
175
  if (bounds.contains([cell.longitude, cell.latitude])) {
164
176
  // 5. Z-Index Lookup
165
- const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey; // Approx key
177
+ const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
166
178
  const zIndex = Z_INDEX_BY_BAND[zIndexKey] || 10;
167
179
 
168
180
  // 6. Calculate Scaled Radius based on Z-Index
169
181
  // Higher Z-index (Top layer) = Smaller radius
170
182
  // Lower Z-index (Bottom layer) = Larger radius
171
183
  // This ensures stacked cells are visible
172
- const MAX_Z = 15;
184
+ const MAX_Z = 30;
173
185
  const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08; // 8% size diff per layer
174
186
  const effectiveRadius = radiusMeters * scaleFactor;
175
187
 
176
- // 7. Generate Arc
177
- const feature = generateCellArc(cell, effectiveRadius, zIndex, style.color);
188
+ // 7. Apply beamwidth boost from constants
189
+ const beamwidthBoost = BEAMWIDTH_BOOST_BY_BAND[zIndexKey] || 0;
190
+ const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
191
+
192
+ // 8. Generate Arc
193
+ const feature = generateCellArc(cell, effectiveRadius, zIndex, style.color, adjustedBeamwidth);
178
194
  features.push(feature);
179
195
  }
180
196
  }
@@ -9,4 +9,4 @@ export declare function calculateRadiusInMeters(latitude: number, zoom: number,
9
9
  /**
10
10
  * Generates a sector arc GeoJSON feature for a cell
11
11
  */
12
- export declare function generateCellArc(cell: Cell, radiusMeters: number, zIndex: number, color: string): GeoJSON.Feature<GeoJSON.Polygon>;
12
+ export declare function generateCellArc(cell: Cell, radiusMeters: number, zIndex: number, color: string, beamwidthOverride?: number): GeoJSON.Feature<GeoJSON.Polygon>;
@@ -1,4 +1,5 @@
1
1
  import * as turf from '@turf/turf';
2
+ import { DEFAULT_STATUS_STYLES } from '../constants/statusStyles';
2
3
  /**
3
4
  * Calculates the radius in meters required to achieve a target pixel size
4
5
  * at a specific latitude and zoom level.
@@ -12,24 +13,44 @@ export function calculateRadiusInMeters(latitude, zoom, targetPixelSize) {
12
13
  /**
13
14
  * Generates a sector arc GeoJSON feature for a cell
14
15
  */
15
- export function generateCellArc(cell, radiusMeters, zIndex, color) {
16
+ export function generateCellArc(cell, radiusMeters, zIndex, color, beamwidthOverride) {
16
17
  const center = [cell.longitude, cell.latitude];
17
- const bearing1 = cell.azimuth - (cell.beamwidth / 2);
18
- const bearing2 = cell.azimuth + (cell.beamwidth / 2);
18
+ const beamwidth = beamwidthOverride ?? cell.beamwidth;
19
+ const bearing1 = cell.azimuth - (beamwidth / 2);
20
+ const bearing2 = cell.azimuth + (beamwidth / 2);
19
21
  // Use Turf to generate the sector
20
22
  // Note: turf.sector takes (center, radius, bearing1, bearing2)
21
23
  // Radius must be in kilometers for default turf units, or specify units
22
24
  const sector = turf.sector(center, radiusMeters / 1000, bearing1, bearing2, {
23
25
  steps: 10 // Low steps for performance, increase if jagged
24
26
  });
27
+ // Get status style
28
+ const statusStyle = DEFAULT_STATUS_STYLES[cell.status] || DEFAULT_STATUS_STYLES['On_Air'];
25
29
  // Attach properties for styling and interaction
26
30
  sector.properties = {
27
- id: cell.id,
31
+ id: cell.txId,
32
+ txId: cell.txId,
33
+ cellId: cell.txId,
34
+ cellID: cell.cellID,
28
35
  cellName: cell.cellName,
36
+ siteId: cell.siteId,
37
+ latitude: cell.latitude,
38
+ longitude: cell.longitude,
39
+ // Cell attributes
29
40
  tech: cell.tech,
41
+ band: cell.frq,
42
+ status: cell.status,
43
+ type: cell.type,
44
+ azimuth: cell.azimuth,
45
+ beamwidth: cell.beamwidth,
30
46
  fband: cell.fband,
31
47
  zIndex: zIndex,
32
- color: color
48
+ color: color,
49
+ // Status-based line styling
50
+ lineColor: statusStyle.lineColor,
51
+ lineWidth: statusStyle.lineWidth,
52
+ lineOpacity: statusStyle.opacity,
53
+ dashArray: statusStyle.dashArray
33
54
  };
34
55
  return sector;
35
56
  }
@@ -74,9 +74,23 @@
74
74
  };
75
75
  return labels[field] || field;
76
76
  }
77
+
78
+ function toggleLabels() {
79
+ displayStore.showLabels = !displayStore.showLabels;
80
+ }
77
81
  </script>
78
82
 
79
83
  <MapControl {position} title="Repeaters" icon="router" controlWidth="320px">
84
+ {#snippet actions()}
85
+ <button
86
+ class="btn btn-sm btn-outline-secondary border-0 p-1 px-2"
87
+ title={displayStore.showLabels ? 'Hide Labels' : 'Show Labels'}
88
+ aria-label={displayStore.showLabels ? 'Hide Labels' : 'Show Labels'}
89
+ onclick={toggleLabels}
90
+ >
91
+ <i class="bi bi-tag{displayStore.showLabels ? '-fill' : ''}"></i>
92
+ </button>
93
+ {/snippet}
80
94
  <div class="card shadow-sm border-0">
81
95
  <!-- <div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
82
96
  <h6 class="mb-0 fw-bold text-primary">
@@ -143,35 +143,66 @@
143
143
  features
144
144
  };
145
145
 
146
+ // 4. Update Source
146
147
  const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
147
148
  if (source) {
148
149
  source.setData(geojson);
149
- } else {
150
- map.addSource(sourceId, {
151
- type: 'geojson',
152
- data: geojson
153
- });
154
-
155
- map.addLayer({
156
- id: layerId,
157
- type: 'symbol',
158
- source: sourceId,
159
- layout: {
160
- 'text-field': ['get', 'label'],
161
- 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
162
- 'text-size': displayStore.labelFontSize,
163
- 'text-offset': ['get', 'offset'],
164
- 'text-anchor': 'center',
165
- 'text-justify': 'auto',
166
- 'text-allow-overlap': false,
167
- 'text-ignore-placement': false
168
- },
169
- paint: {
170
- 'text-color': displayStore.labelColor,
171
- 'text-halo-color': displayStore.labelHaloColor,
172
- 'text-halo-width': displayStore.labelHaloWidth
173
- }
174
- });
175
150
  }
176
151
  }
152
+
153
+ // Initial Setup
154
+ $effect(() => {
155
+ const map = mapStore.map;
156
+ if (!map) return;
157
+
158
+ const addLayers = () => {
159
+ if (!map.getSource(sourceId)) {
160
+ map.addSource(sourceId, {
161
+ type: 'geojson',
162
+ data: { type: 'FeatureCollection', features: [] }
163
+ });
164
+ }
165
+
166
+ if (!map.getLayer(layerId)) {
167
+ map.addLayer({
168
+ id: layerId,
169
+ type: 'symbol',
170
+ source: sourceId,
171
+ layout: {
172
+ 'text-field': ['get', 'label'],
173
+ 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
174
+ 'text-size': displayStore.labelFontSize,
175
+ 'text-offset': ['get', 'offset'],
176
+ 'text-anchor': 'center',
177
+ 'text-justify': 'auto',
178
+ 'text-allow-overlap': false,
179
+ 'text-ignore-placement': false
180
+ },
181
+ paint: {
182
+ 'text-color': displayStore.labelColor,
183
+ 'text-halo-color': displayStore.labelHaloColor,
184
+ 'text-halo-width': displayStore.labelHaloWidth
185
+ }
186
+ });
187
+ }
188
+ };
189
+
190
+ const handleStyleLoad = () => {
191
+ addLayers();
192
+ updateLayer();
193
+ };
194
+
195
+ addLayers();
196
+ map.on('style.load', handleStyleLoad);
197
+ map.on('moveend', updateLayer);
198
+ map.on('zoomend', updateLayer);
199
+
200
+ return () => {
201
+ map.off('style.load', handleStyleLoad);
202
+ map.off('moveend', updateLayer);
203
+ map.off('zoomend', updateLayer);
204
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
205
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
206
+ };
207
+ });
177
208
  </script>