@smartnet360/svelte-components 0.0.54 → 0.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/map-v2/core/controls/MapStyleControl.svelte +289 -0
- package/dist/map-v2/core/controls/MapStyleControl.svelte.d.ts +24 -0
- package/dist/{map → map-v2/core}/hooks/useMapbox.d.ts +1 -1
- package/dist/{map → map-v2/core}/hooks/useMapbox.js +1 -1
- package/dist/map-v2/core/index.d.ts +11 -0
- package/dist/map-v2/core/index.js +14 -0
- package/dist/map-v2/core/providers/MapboxProvider.svelte +140 -0
- package/dist/map-v2/core/providers/MapboxProvider.svelte.d.ts +33 -0
- package/dist/{map → map-v2/core}/stores/mapStore.d.ts +2 -2
- package/dist/{map → map-v2/core}/stores/mapStore.js +2 -2
- package/dist/map-v2/core/types.d.ts +13 -0
- package/dist/map-v2/core/types.js +7 -0
- package/dist/map-v2/demo/DemoMap.svelte +63 -0
- package/dist/{map → map-v2}/demo/DemoMap.svelte.d.ts +3 -4
- package/dist/map-v2/demo/demo-data.d.ts +8 -0
- package/dist/map-v2/demo/demo-data.js +128 -0
- package/dist/map-v2/demo/index.d.ts +7 -0
- package/dist/map-v2/demo/index.js +9 -0
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +147 -0
- package/dist/{map → map-v2/features/sites}/controls/SiteFilterControl.svelte.d.ts +4 -6
- package/dist/map-v2/features/sites/controls/SiteSizeSlider.svelte +236 -0
- package/dist/map-v2/features/sites/controls/SiteSizeSlider.svelte.d.ts +20 -0
- package/dist/map-v2/features/sites/index.d.ts +14 -0
- package/dist/map-v2/features/sites/index.js +16 -0
- package/dist/map-v2/features/sites/layers/SitesLayer.svelte +294 -0
- package/dist/map-v2/features/sites/layers/SitesLayer.svelte.d.ts +12 -0
- package/dist/map-v2/features/sites/stores/siteStore.d.ts +18 -0
- package/dist/map-v2/features/sites/stores/siteStore.js +36 -0
- package/dist/map-v2/features/sites/stores/siteStoreContext.svelte.d.ts +36 -0
- package/dist/map-v2/features/sites/stores/siteStoreContext.svelte.js +155 -0
- package/dist/map-v2/features/sites/types.d.ts +39 -0
- package/dist/map-v2/features/sites/types.js +4 -0
- package/dist/map-v2/features/sites/utils/siteGeoJSON.d.ts +33 -0
- package/dist/map-v2/features/sites/utils/siteGeoJSON.js +43 -0
- package/dist/map-v2/features/sites/utils/siteTreeUtils.d.ts +16 -0
- package/dist/{map → map-v2/features/sites}/utils/siteTreeUtils.js +9 -52
- package/dist/map-v2/index.d.ts +10 -0
- package/dist/map-v2/index.js +22 -0
- package/dist/{map → map-v2/shared}/controls/MapControl.svelte +1 -1
- package/dist/map-v2/shared/index.d.ts +7 -0
- package/dist/map-v2/shared/index.js +9 -0
- package/package.json +1 -1
- package/dist/map/controls/SiteFilterControl.svelte +0 -126
- package/dist/map/demo/DemoMap.svelte +0 -98
- package/dist/map/demo/demo-data.d.ts +0 -12
- package/dist/map/demo/demo-data.js +0 -220
- package/dist/map/hooks/useCellData.d.ts +0 -14
- package/dist/map/hooks/useCellData.js +0 -29
- package/dist/map/index.d.ts +0 -27
- package/dist/map/index.js +0 -47
- package/dist/map/layers/CellsLayer.svelte +0 -242
- package/dist/map/layers/CellsLayer.svelte.d.ts +0 -21
- package/dist/map/layers/CoverageLayer.svelte +0 -37
- package/dist/map/layers/CoverageLayer.svelte.d.ts +0 -9
- package/dist/map/layers/LayerBase.d.ts +0 -42
- package/dist/map/layers/LayerBase.js +0 -58
- package/dist/map/layers/SitesLayer.svelte +0 -282
- package/dist/map/layers/SitesLayer.svelte.d.ts +0 -19
- package/dist/map/providers/CellDataProvider.svelte +0 -43
- package/dist/map/providers/CellDataProvider.svelte.d.ts +0 -12
- package/dist/map/providers/MapboxProvider.svelte +0 -38
- package/dist/map/providers/MapboxProvider.svelte.d.ts +0 -9
- package/dist/map/providers/providerHelpers.d.ts +0 -17
- package/dist/map/providers/providerHelpers.js +0 -26
- package/dist/map/stores/cellDataStore.d.ts +0 -21
- package/dist/map/stores/cellDataStore.js +0 -53
- package/dist/map/stores/interactions.d.ts +0 -20
- package/dist/map/stores/interactions.js +0 -33
- package/dist/map/types.d.ts +0 -115
- package/dist/map/types.js +0 -10
- package/dist/map/utils/geojson.d.ts +0 -20
- package/dist/map/utils/geojson.js +0 -78
- package/dist/map/utils/math.d.ts +0 -40
- package/dist/map/utils/math.js +0 -95
- package/dist/map/utils/siteTreeUtils.d.ts +0 -27
- /package/dist/{map → map-v2/shared}/controls/MapControl.svelte.d.ts +0 -0
- /package/dist/{map → map-v2/shared}/utils/mapboxHelpers.d.ts +0 -0
- /package/dist/{map → map-v2/shared}/utils/mapboxHelpers.js +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SitesLayer - Visualizes cellular sites as circles on the map
|
|
4
|
+
*
|
|
5
|
+
* Reads from siteStore to get filtered sites and visual properties.
|
|
6
|
+
* Completely independent from cell data.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Displays sites as circles with configurable size, color, opacity
|
|
10
|
+
* - Supports labels (optional)
|
|
11
|
+
* - Click to show popup
|
|
12
|
+
* - Hover effects
|
|
13
|
+
*/
|
|
14
|
+
import { onMount, onDestroy } from 'svelte';
|
|
15
|
+
import mapboxgl from 'mapbox-gl';
|
|
16
|
+
import { useMapbox } from '../../../core/hooks/useMapbox';
|
|
17
|
+
import { sitesToGeoJSON } from '../utils/siteGeoJSON';
|
|
18
|
+
import {
|
|
19
|
+
addSourceIfMissing,
|
|
20
|
+
addLayerIfMissing,
|
|
21
|
+
updateGeoJSONSource,
|
|
22
|
+
removeLayerAndSource,
|
|
23
|
+
generateLayerId,
|
|
24
|
+
generateSourceId
|
|
25
|
+
} from '../../../shared/utils/mapboxHelpers';
|
|
26
|
+
import type { SiteStoreContext } from '../stores/siteStoreContext.svelte';
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Site store instance */
|
|
30
|
+
store: SiteStoreContext;
|
|
31
|
+
/** Namespace for layer IDs (default: 'sites') */
|
|
32
|
+
namespace?: string;
|
|
33
|
+
/** Enable click to show popup (default: true) */
|
|
34
|
+
clickable?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
store,
|
|
39
|
+
namespace = 'sites',
|
|
40
|
+
clickable = true
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
|
|
43
|
+
const mapStore = useMapbox();
|
|
44
|
+
|
|
45
|
+
let map: mapboxgl.Map | null = null;
|
|
46
|
+
let popup: mapboxgl.Popup | null = null;
|
|
47
|
+
let hoveredSiteId: string | null = null;
|
|
48
|
+
|
|
49
|
+
const layerId = generateLayerId(namespace, 'circles');
|
|
50
|
+
const labelLayerId = generateLayerId(namespace, 'labels');
|
|
51
|
+
const sourceId = generateSourceId(namespace, 'source');
|
|
52
|
+
|
|
53
|
+
// Watch store changes and update layer
|
|
54
|
+
$effect(() => {
|
|
55
|
+
// Access store properties to track them
|
|
56
|
+
const filteredSites = store.filteredSites;
|
|
57
|
+
const size = store.size;
|
|
58
|
+
const color = store.color;
|
|
59
|
+
const opacity = store.opacity;
|
|
60
|
+
const showLabels = store.showLabels;
|
|
61
|
+
const labelSize = store.labelSize;
|
|
62
|
+
const labelColor = store.labelColor;
|
|
63
|
+
const labelOffset = store.labelOffset;
|
|
64
|
+
const labelProperty = store.labelProperty;
|
|
65
|
+
const groupColorMap = store.groupColorMap; // Track color map changes
|
|
66
|
+
|
|
67
|
+
if (map) {
|
|
68
|
+
updateLayer();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
onMount(() => {
|
|
73
|
+
const mapUnsub = mapStore.subscribe((m: mapboxgl.Map | null) => {
|
|
74
|
+
if (!m) return;
|
|
75
|
+
map = m;
|
|
76
|
+
// Map is guaranteed to be ready (style loaded) by MapboxProvider
|
|
77
|
+
initializeLayer();
|
|
78
|
+
|
|
79
|
+
// Re-initialize layer when map style changes
|
|
80
|
+
// Mapbox removes all custom layers/sources when setStyle() is called
|
|
81
|
+
map.on('style.load', initializeLayer);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
mapUnsub();
|
|
86
|
+
cleanup();
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
onDestroy(() => {
|
|
91
|
+
cleanup();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function initializeLayer(): void {
|
|
95
|
+
if (!map) return;
|
|
96
|
+
|
|
97
|
+
// Add empty GeoJSON source
|
|
98
|
+
addSourceIfMissing(map, sourceId, {
|
|
99
|
+
type: 'geojson',
|
|
100
|
+
data: {
|
|
101
|
+
type: 'FeatureCollection',
|
|
102
|
+
features: []
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Add circle layer with current store values
|
|
107
|
+
addLayerIfMissing(map, {
|
|
108
|
+
id: layerId,
|
|
109
|
+
type: 'circle',
|
|
110
|
+
source: sourceId,
|
|
111
|
+
paint: {
|
|
112
|
+
'circle-radius': store.size,
|
|
113
|
+
'circle-color': [
|
|
114
|
+
'coalesce',
|
|
115
|
+
['get', 'groupColor'], // Use group-specific color if available
|
|
116
|
+
store.color // Fallback to global color
|
|
117
|
+
],
|
|
118
|
+
'circle-opacity': store.opacity,
|
|
119
|
+
'circle-stroke-width': 2,
|
|
120
|
+
'circle-stroke-color': [
|
|
121
|
+
'coalesce',
|
|
122
|
+
['get', 'groupColor'], // Use group-specific color if available
|
|
123
|
+
store.color // Fallback to global color
|
|
124
|
+
],
|
|
125
|
+
'circle-stroke-opacity': store.opacity
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Add label layer
|
|
130
|
+
addLayerIfMissing(map, {
|
|
131
|
+
id: labelLayerId,
|
|
132
|
+
type: 'symbol',
|
|
133
|
+
source: sourceId,
|
|
134
|
+
layout: {
|
|
135
|
+
'text-field': ['get', store.labelProperty],
|
|
136
|
+
'text-size': store.labelSize,
|
|
137
|
+
'text-offset': [0, store.labelOffset],
|
|
138
|
+
'text-anchor': 'top',
|
|
139
|
+
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular']
|
|
140
|
+
},
|
|
141
|
+
paint: {
|
|
142
|
+
'text-color': store.labelColor,
|
|
143
|
+
'text-halo-color': '#fff',
|
|
144
|
+
'text-halo-width': 1.5
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Set initial visibility
|
|
149
|
+
map.setLayoutProperty(labelLayerId, 'visibility', store.showLabels ? 'visible' : 'none');
|
|
150
|
+
|
|
151
|
+
// Add event handlers
|
|
152
|
+
if (clickable) {
|
|
153
|
+
map.on('click', layerId, handleClick);
|
|
154
|
+
map.on('mouseenter', layerId, handleMouseEnter);
|
|
155
|
+
map.on('mouseleave', layerId, handleMouseLeave);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Initial data load
|
|
159
|
+
updateLayer();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function updateLayer(): void {
|
|
163
|
+
if (!map) return;
|
|
164
|
+
|
|
165
|
+
// Update data with color information
|
|
166
|
+
const geojson = sitesToGeoJSON(store.filteredSites, store.groupColorMap);
|
|
167
|
+
updateGeoJSONSource(map, sourceId, geojson);
|
|
168
|
+
|
|
169
|
+
// Update circle visual properties
|
|
170
|
+
map.setPaintProperty(layerId, 'circle-radius', store.size);
|
|
171
|
+
map.setPaintProperty(layerId, 'circle-color', [
|
|
172
|
+
'coalesce',
|
|
173
|
+
['get', 'groupColor'],
|
|
174
|
+
store.color
|
|
175
|
+
]);
|
|
176
|
+
map.setPaintProperty(layerId, 'circle-opacity', store.opacity);
|
|
177
|
+
map.setPaintProperty(layerId, 'circle-stroke-opacity', store.opacity);
|
|
178
|
+
|
|
179
|
+
// Update label properties
|
|
180
|
+
map.setLayoutProperty(labelLayerId, 'text-field', ['get', store.labelProperty]);
|
|
181
|
+
map.setLayoutProperty(labelLayerId, 'text-size', store.labelSize);
|
|
182
|
+
map.setLayoutProperty(labelLayerId, 'text-offset', [0, store.labelOffset]);
|
|
183
|
+
map.setPaintProperty(labelLayerId, 'text-color', store.labelColor);
|
|
184
|
+
map.setLayoutProperty(labelLayerId, 'visibility', store.showLabels ? 'visible' : 'none');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleClick(e: mapboxgl.MapLayerMouseEvent): void {
|
|
188
|
+
if (!e.features || e.features.length === 0) return;
|
|
189
|
+
const feature = e.features[0];
|
|
190
|
+
const siteProps = feature.properties;
|
|
191
|
+
if (siteProps && e.lngLat) {
|
|
192
|
+
createSitePopup(siteProps, e.lngLat);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createSitePopup(siteProps: any, lngLat: mapboxgl.LngLat): void {
|
|
197
|
+
if (!map) return;
|
|
198
|
+
|
|
199
|
+
// Remove existing popup
|
|
200
|
+
if (popup) {
|
|
201
|
+
popup.remove();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Parse arrays if needed
|
|
205
|
+
const fbands = siteProps.fbands ? JSON.parse(siteProps.fbands) : [];
|
|
206
|
+
const cellNames = siteProps.cellNames ? JSON.parse(siteProps.cellNames) : [];
|
|
207
|
+
|
|
208
|
+
const fbandsHtml = fbands.length > 0
|
|
209
|
+
? `<div><strong>Bands:</strong> ${fbands.join(', ')} MHz</div>`
|
|
210
|
+
: '';
|
|
211
|
+
|
|
212
|
+
const html = `
|
|
213
|
+
<div style="font-family: system-ui, -apple-system, sans-serif; font-size: 13px;">
|
|
214
|
+
<h6 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600;">${siteProps.name}</h6>
|
|
215
|
+
<div style="color: #666; line-height: 1.6;">
|
|
216
|
+
<div><strong>ID:</strong> ${siteProps.id}</div>
|
|
217
|
+
<div><strong>Technology:</strong> <span class="badge bg-primary">${siteProps.technology}</span></div>
|
|
218
|
+
${fbandsHtml}
|
|
219
|
+
<div><strong>Provider:</strong> ${siteProps.provider}</div>
|
|
220
|
+
<div><strong>Group:</strong> ${siteProps.featureGroup}</div>
|
|
221
|
+
<div><strong>Cells:</strong> ${cellNames.length}</div>
|
|
222
|
+
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee; font-size: 11px;">
|
|
223
|
+
<strong>Coordinates:</strong><br/>
|
|
224
|
+
${Number(siteProps.latitude).toFixed(6)}, ${Number(siteProps.longitude).toFixed(6)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
// Create and add popup
|
|
231
|
+
popup = new mapboxgl.Popup({
|
|
232
|
+
closeButton: true,
|
|
233
|
+
closeOnClick: false,
|
|
234
|
+
maxWidth: '300px'
|
|
235
|
+
})
|
|
236
|
+
.setLngLat(lngLat)
|
|
237
|
+
.setHTML(html)
|
|
238
|
+
.addTo(map);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function handleMouseEnter(e: mapboxgl.MapLayerMouseEvent): void {
|
|
242
|
+
if (!map) return;
|
|
243
|
+
map.getCanvas().style.cursor = 'pointer';
|
|
244
|
+
|
|
245
|
+
if (!e.features || e.features.length === 0) return;
|
|
246
|
+
const feature = e.features[0];
|
|
247
|
+
const siteId = feature.properties?.id;
|
|
248
|
+
|
|
249
|
+
if (siteId) {
|
|
250
|
+
hoveredSiteId = siteId;
|
|
251
|
+
// Optional: Add hover effect via feature state
|
|
252
|
+
map.setFeatureState(
|
|
253
|
+
{ source: sourceId, id: siteId },
|
|
254
|
+
{ hovered: true }
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function handleMouseLeave(): void {
|
|
260
|
+
if (!map) return;
|
|
261
|
+
map.getCanvas().style.cursor = '';
|
|
262
|
+
|
|
263
|
+
if (hoveredSiteId) {
|
|
264
|
+
// Remove hover state
|
|
265
|
+
map.removeFeatureState(
|
|
266
|
+
{ source: sourceId, id: hoveredSiteId },
|
|
267
|
+
'hovered'
|
|
268
|
+
);
|
|
269
|
+
hoveredSiteId = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function cleanup(): void {
|
|
274
|
+
if (popup) {
|
|
275
|
+
popup.remove();
|
|
276
|
+
popup = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (map) {
|
|
280
|
+
// Remove style.load listener
|
|
281
|
+
map.off('style.load', initializeLayer);
|
|
282
|
+
|
|
283
|
+
if (clickable) {
|
|
284
|
+
map.off('click', layerId, handleClick);
|
|
285
|
+
map.off('mouseenter', layerId, handleMouseEnter);
|
|
286
|
+
map.off('mouseleave', layerId, handleMouseLeave);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Remove layers and source
|
|
290
|
+
removeLayerAndSource(map, labelLayerId, sourceId);
|
|
291
|
+
removeLayerAndSource(map, layerId, sourceId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SiteStoreContext } from '../stores/siteStoreContext.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Site store instance */
|
|
4
|
+
store: SiteStoreContext;
|
|
5
|
+
/** Namespace for layer IDs (default: 'sites') */
|
|
6
|
+
namespace?: string;
|
|
7
|
+
/** Enable click to show popup (default: true) */
|
|
8
|
+
clickable?: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare const SitesLayer: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type SitesLayer = ReturnType<typeof SitesLayer>;
|
|
12
|
+
export default SitesLayer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site store - State management for site feature
|
|
3
|
+
*/
|
|
4
|
+
import type { Writable } from 'svelte/store';
|
|
5
|
+
import type { Site, SiteStoreValue } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Creates a site store with default values
|
|
8
|
+
*/
|
|
9
|
+
export declare function createSiteStore(initialSites?: Site[]): Writable<SiteStoreValue> & {
|
|
10
|
+
setAllSites: (sites: Site[]) => void;
|
|
11
|
+
setFilteredSites: (sites: Site[]) => void;
|
|
12
|
+
setSize: (size: number) => void;
|
|
13
|
+
setColor: (color: string) => void;
|
|
14
|
+
setOpacity: (opacity: number) => void;
|
|
15
|
+
setShowLabels: (show: boolean) => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
};
|
|
18
|
+
export type SiteStore = ReturnType<typeof createSiteStore>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site store - State management for site feature
|
|
3
|
+
*/
|
|
4
|
+
import { writable } from 'svelte/store';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a site store with default values
|
|
7
|
+
*/
|
|
8
|
+
export function createSiteStore(initialSites = []) {
|
|
9
|
+
const defaultState = {
|
|
10
|
+
allSites: initialSites,
|
|
11
|
+
filteredSites: initialSites,
|
|
12
|
+
size: 10,
|
|
13
|
+
color: '#3b82f6',
|
|
14
|
+
opacity: 1.0,
|
|
15
|
+
showLabels: false,
|
|
16
|
+
showOnHover: true,
|
|
17
|
+
strokeWidth: 2,
|
|
18
|
+
strokeColor: '#ffffff'
|
|
19
|
+
};
|
|
20
|
+
const { subscribe, update, set } = writable(defaultState);
|
|
21
|
+
return {
|
|
22
|
+
subscribe,
|
|
23
|
+
set,
|
|
24
|
+
update,
|
|
25
|
+
// Data setters
|
|
26
|
+
setAllSites: (sites) => update((s) => ({ ...s, allSites: sites })),
|
|
27
|
+
setFilteredSites: (sites) => update((s) => ({ ...s, filteredSites: sites })),
|
|
28
|
+
// Visual property setters
|
|
29
|
+
setSize: (size) => update((s) => ({ ...s, size })),
|
|
30
|
+
setColor: (color) => update((s) => ({ ...s, color })),
|
|
31
|
+
setOpacity: (opacity) => update((s) => ({ ...s, opacity })),
|
|
32
|
+
setShowLabels: (show) => update((s) => ({ ...s, showLabels: show })),
|
|
33
|
+
// Reset to defaults
|
|
34
|
+
reset: () => set({ ...defaultState, allSites: initialSites, filteredSites: initialSites })
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Store Context - Bindable reactive state for site feature
|
|
3
|
+
*
|
|
4
|
+
* Uses Svelte 5 runes ($state) to create a directly bindable reactive object.
|
|
5
|
+
* This allows components to use bind:value instead of manual event handlers.
|
|
6
|
+
* Persists visual settings to localStorage.
|
|
7
|
+
*/
|
|
8
|
+
import type { Site, SiteStoreValue } from '../types';
|
|
9
|
+
export declare function createSiteStoreContext(initialSites?: Site[]): {
|
|
10
|
+
allSites: Site[];
|
|
11
|
+
filteredSites: Site[];
|
|
12
|
+
size: number;
|
|
13
|
+
color: string;
|
|
14
|
+
opacity: number;
|
|
15
|
+
showLabels: boolean;
|
|
16
|
+
showOnHover: boolean;
|
|
17
|
+
labelSize: number;
|
|
18
|
+
labelColor: string;
|
|
19
|
+
labelOffset: number;
|
|
20
|
+
labelProperty: string;
|
|
21
|
+
strokeWidth: number;
|
|
22
|
+
strokeColor: string;
|
|
23
|
+
groupColorMap: Map<string, string>;
|
|
24
|
+
setAllSites(sites: Site[]): void;
|
|
25
|
+
setFilteredSites(sites: Site[]): void;
|
|
26
|
+
setSize(size: number): void;
|
|
27
|
+
setColor(color: string): void;
|
|
28
|
+
setOpacity(opacity: number): void;
|
|
29
|
+
setShowLabels(show: boolean): void;
|
|
30
|
+
getGroupColor(groupKey: string): string | undefined;
|
|
31
|
+
setGroupColor(groupKey: string, color: string): void;
|
|
32
|
+
clearGroupColor(groupKey: string): void;
|
|
33
|
+
reset(): void;
|
|
34
|
+
getState(): SiteStoreValue;
|
|
35
|
+
};
|
|
36
|
+
export type SiteStoreContext = ReturnType<typeof createSiteStoreContext>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Store Context - Bindable reactive state for site feature
|
|
3
|
+
*
|
|
4
|
+
* Uses Svelte 5 runes ($state) to create a directly bindable reactive object.
|
|
5
|
+
* This allows components to use bind:value instead of manual event handlers.
|
|
6
|
+
* Persists visual settings to localStorage.
|
|
7
|
+
*/
|
|
8
|
+
const STORAGE_KEY = 'siteVisualSettings';
|
|
9
|
+
function loadSettings() {
|
|
10
|
+
if (typeof window === 'undefined')
|
|
11
|
+
return {};
|
|
12
|
+
try {
|
|
13
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
14
|
+
if (saved) {
|
|
15
|
+
return JSON.parse(saved);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.warn('Failed to load site settings from localStorage:', error);
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
function saveSettings(settings) {
|
|
24
|
+
if (typeof window === 'undefined')
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.warn('Failed to save site settings to localStorage:', error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function createSiteStoreContext(initialSites = []) {
|
|
34
|
+
// Load persisted settings
|
|
35
|
+
const persistedSettings = loadSettings();
|
|
36
|
+
// Convert persisted groupColors object back to Map
|
|
37
|
+
const initialColorMap = new Map();
|
|
38
|
+
if (persistedSettings.groupColors) {
|
|
39
|
+
Object.entries(persistedSettings.groupColors).forEach(([key, value]) => {
|
|
40
|
+
initialColorMap.set(key, value);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Internal reactive state
|
|
44
|
+
let state = $state({
|
|
45
|
+
allSites: initialSites,
|
|
46
|
+
filteredSites: initialSites,
|
|
47
|
+
size: persistedSettings.size ?? 10,
|
|
48
|
+
color: persistedSettings.color ?? '#3b82f6',
|
|
49
|
+
opacity: persistedSettings.opacity ?? 1.0,
|
|
50
|
+
showLabels: persistedSettings.showLabels ?? false,
|
|
51
|
+
showOnHover: true,
|
|
52
|
+
labelSize: persistedSettings.labelSize ?? 12,
|
|
53
|
+
labelColor: persistedSettings.labelColor ?? '#000000',
|
|
54
|
+
labelOffset: persistedSettings.labelOffset ?? 1.5,
|
|
55
|
+
labelProperty: persistedSettings.labelProperty ?? 'name',
|
|
56
|
+
groupColorMap: initialColorMap,
|
|
57
|
+
strokeWidth: persistedSettings.strokeWidth ?? 2,
|
|
58
|
+
strokeColor: persistedSettings.strokeColor ?? '#ffffff'
|
|
59
|
+
});
|
|
60
|
+
// Auto-save settings when they change
|
|
61
|
+
$effect(() => {
|
|
62
|
+
// Convert Map to plain object for serialization
|
|
63
|
+
const groupColorsObj = {};
|
|
64
|
+
state.groupColorMap.forEach((color, key) => {
|
|
65
|
+
groupColorsObj[key] = color;
|
|
66
|
+
});
|
|
67
|
+
const settings = {
|
|
68
|
+
size: state.size,
|
|
69
|
+
color: state.color,
|
|
70
|
+
opacity: state.opacity,
|
|
71
|
+
showLabels: state.showLabels,
|
|
72
|
+
labelSize: state.labelSize,
|
|
73
|
+
labelColor: state.labelColor,
|
|
74
|
+
labelOffset: state.labelOffset,
|
|
75
|
+
labelProperty: state.labelProperty,
|
|
76
|
+
strokeWidth: state.strokeWidth ?? 2,
|
|
77
|
+
strokeColor: state.strokeColor ?? '#ffffff',
|
|
78
|
+
groupColors: groupColorsObj
|
|
79
|
+
};
|
|
80
|
+
saveSettings(settings);
|
|
81
|
+
});
|
|
82
|
+
// Return object with getters/setters for direct binding
|
|
83
|
+
return {
|
|
84
|
+
// Bindable properties with getters/setters
|
|
85
|
+
get allSites() { return state.allSites; },
|
|
86
|
+
set allSites(value) { state.allSites = value; },
|
|
87
|
+
get filteredSites() { return state.filteredSites; },
|
|
88
|
+
set filteredSites(value) { state.filteredSites = value; },
|
|
89
|
+
get size() { return state.size; },
|
|
90
|
+
set size(value) { state.size = value; },
|
|
91
|
+
get color() { return state.color; },
|
|
92
|
+
set color(value) { state.color = value; },
|
|
93
|
+
get opacity() { return state.opacity; },
|
|
94
|
+
set opacity(value) { state.opacity = value; },
|
|
95
|
+
get showLabels() { return state.showLabels; },
|
|
96
|
+
set showLabels(value) { state.showLabels = value; },
|
|
97
|
+
get showOnHover() { return state.showOnHover; },
|
|
98
|
+
set showOnHover(value) { state.showOnHover = value; },
|
|
99
|
+
get labelSize() { return state.labelSize; },
|
|
100
|
+
set labelSize(value) { state.labelSize = value; },
|
|
101
|
+
get labelColor() { return state.labelColor; },
|
|
102
|
+
set labelColor(value) { state.labelColor = value; },
|
|
103
|
+
get labelOffset() { return state.labelOffset; },
|
|
104
|
+
set labelOffset(value) { state.labelOffset = value; },
|
|
105
|
+
get labelProperty() { return state.labelProperty; },
|
|
106
|
+
set labelProperty(value) { state.labelProperty = value; },
|
|
107
|
+
get strokeWidth() { return state.strokeWidth ?? 2; },
|
|
108
|
+
set strokeWidth(value) { state.strokeWidth = value; },
|
|
109
|
+
get strokeColor() { return state.strokeColor ?? '#ffffff'; },
|
|
110
|
+
set strokeColor(value) { state.strokeColor = value; },
|
|
111
|
+
get groupColorMap() { return state.groupColorMap; },
|
|
112
|
+
set groupColorMap(value) { state.groupColorMap = value; },
|
|
113
|
+
// Convenience methods (optional, but nice to have)
|
|
114
|
+
setAllSites(sites) { state.allSites = sites; },
|
|
115
|
+
setFilteredSites(sites) { state.filteredSites = sites; },
|
|
116
|
+
setSize(size) { state.size = size; },
|
|
117
|
+
setColor(color) { state.color = color; },
|
|
118
|
+
setOpacity(opacity) { state.opacity = opacity; },
|
|
119
|
+
setShowLabels(show) { state.showLabels = show; },
|
|
120
|
+
// Group color methods
|
|
121
|
+
getGroupColor(groupKey) {
|
|
122
|
+
return state.groupColorMap.get(groupKey);
|
|
123
|
+
},
|
|
124
|
+
setGroupColor(groupKey, color) {
|
|
125
|
+
state.groupColorMap.set(groupKey, color);
|
|
126
|
+
// Trigger reactivity by reassigning
|
|
127
|
+
state.groupColorMap = new Map(state.groupColorMap);
|
|
128
|
+
},
|
|
129
|
+
clearGroupColor(groupKey) {
|
|
130
|
+
state.groupColorMap.delete(groupKey);
|
|
131
|
+
state.groupColorMap = new Map(state.groupColorMap);
|
|
132
|
+
},
|
|
133
|
+
// Reset to defaults
|
|
134
|
+
reset() {
|
|
135
|
+
state.allSites = initialSites;
|
|
136
|
+
state.filteredSites = initialSites;
|
|
137
|
+
state.size = 10;
|
|
138
|
+
state.color = '#3b82f6';
|
|
139
|
+
state.opacity = 1.0;
|
|
140
|
+
state.showLabels = false;
|
|
141
|
+
state.showOnHover = true;
|
|
142
|
+
state.labelSize = 12;
|
|
143
|
+
state.labelColor = '#000000';
|
|
144
|
+
state.labelOffset = 1.5;
|
|
145
|
+
state.labelProperty = 'name';
|
|
146
|
+
state.strokeWidth = 2;
|
|
147
|
+
state.strokeColor = '#ffffff';
|
|
148
|
+
state.groupColorMap = new Map();
|
|
149
|
+
},
|
|
150
|
+
// Get snapshot of current state (useful for debugging)
|
|
151
|
+
getState() {
|
|
152
|
+
return { ...state };
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site feature types
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Cellular site/tower
|
|
6
|
+
*/
|
|
7
|
+
export interface Site {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
latitude: number;
|
|
11
|
+
longitude: number;
|
|
12
|
+
fbands?: string[];
|
|
13
|
+
technology: string;
|
|
14
|
+
properties: Record<string, any>;
|
|
15
|
+
cellNames: string[];
|
|
16
|
+
provider: string;
|
|
17
|
+
featureGroup: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Site store state - contains all site data and visual properties
|
|
21
|
+
*/
|
|
22
|
+
export interface SiteStoreValue {
|
|
23
|
+
allSites: Site[];
|
|
24
|
+
filteredSites: Site[];
|
|
25
|
+
size: number;
|
|
26
|
+
color: string;
|
|
27
|
+
opacity: number;
|
|
28
|
+
showLabels: boolean;
|
|
29
|
+
showOnHover: boolean;
|
|
30
|
+
labelSize: number;
|
|
31
|
+
labelColor: string;
|
|
32
|
+
labelOffset: number;
|
|
33
|
+
labelProperty: string;
|
|
34
|
+
groupColorMap: Map<string, string>;
|
|
35
|
+
hoverColor?: string;
|
|
36
|
+
selectedColor?: string;
|
|
37
|
+
strokeWidth?: number;
|
|
38
|
+
strokeColor?: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site GeoJSON utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { Site } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* GeoJSON Feature for a site (Point geometry)
|
|
7
|
+
*/
|
|
8
|
+
export interface SiteFeature {
|
|
9
|
+
type: 'Feature';
|
|
10
|
+
geometry: {
|
|
11
|
+
type: 'Point';
|
|
12
|
+
coordinates: [number, number];
|
|
13
|
+
};
|
|
14
|
+
properties: Site;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* GeoJSON FeatureCollection
|
|
18
|
+
*/
|
|
19
|
+
export interface FeatureCollection<T> {
|
|
20
|
+
type: 'FeatureCollection';
|
|
21
|
+
features: T[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts an array of sites to a GeoJSON FeatureCollection
|
|
25
|
+
* Sites are represented as Point features (circles will be drawn by Mapbox)
|
|
26
|
+
* @param sites - Array of sites to convert
|
|
27
|
+
* @param colorMap - Optional map of group keys (provider:featureGroup) to colors
|
|
28
|
+
*/
|
|
29
|
+
export declare function sitesToGeoJSON(sites: Site[], colorMap?: Map<string, string>): FeatureCollection<SiteFeature>;
|
|
30
|
+
/**
|
|
31
|
+
* Converts a single site to a GeoJSON Feature
|
|
32
|
+
*/
|
|
33
|
+
export declare function siteToFeature(site: Site): SiteFeature;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site GeoJSON utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Converts an array of sites to a GeoJSON FeatureCollection
|
|
6
|
+
* Sites are represented as Point features (circles will be drawn by Mapbox)
|
|
7
|
+
* @param sites - Array of sites to convert
|
|
8
|
+
* @param colorMap - Optional map of group keys (provider:featureGroup) to colors
|
|
9
|
+
*/
|
|
10
|
+
export function sitesToGeoJSON(sites, colorMap) {
|
|
11
|
+
const features = sites.map((site) => {
|
|
12
|
+
const groupKey = `${site.provider}:${site.featureGroup}`;
|
|
13
|
+
const groupColor = colorMap?.get(groupKey);
|
|
14
|
+
return {
|
|
15
|
+
type: 'Feature',
|
|
16
|
+
geometry: {
|
|
17
|
+
type: 'Point',
|
|
18
|
+
coordinates: [site.longitude, site.latitude]
|
|
19
|
+
},
|
|
20
|
+
properties: {
|
|
21
|
+
...site,
|
|
22
|
+
groupColor // Add groupColor property for Mapbox styling
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
type: 'FeatureCollection',
|
|
28
|
+
features
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Converts a single site to a GeoJSON Feature
|
|
33
|
+
*/
|
|
34
|
+
export function siteToFeature(site) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'Feature',
|
|
37
|
+
geometry: {
|
|
38
|
+
type: 'Point',
|
|
39
|
+
coordinates: [site.longitude, site.latitude]
|
|
40
|
+
},
|
|
41
|
+
properties: site
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for building and managing site filter tree
|
|
3
|
+
*/
|
|
4
|
+
import type { Site } from '../types';
|
|
5
|
+
import type { TreeNode } from '../../../../core/TreeView/tree.model';
|
|
6
|
+
/**
|
|
7
|
+
* Builds a hierarchical tree from flat site array
|
|
8
|
+
* Structure: All Sites -> Provider -> Feature Group
|
|
9
|
+
* @param sites - Array of sites to build tree from
|
|
10
|
+
* @param colorMap - Optional map of group keys (provider:featureGroup) to colors
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildSiteTree(sites: Site[], colorMap?: Map<string, string>): TreeNode;
|
|
13
|
+
/**
|
|
14
|
+
* Filters sites based on checked tree paths
|
|
15
|
+
*/
|
|
16
|
+
export declare function getFilteredSites(checkedPaths: string[], allSites: Site[]): Site[];
|