@smartnet360/svelte-components 0.0.54 → 0.0.55
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 → map-v2/features/sites}/controls/SiteFilterControl.svelte +26 -40
- package/dist/{map → map-v2/features/sites}/controls/SiteFilterControl.svelte.d.ts +4 -6
- package/dist/map-v2/features/sites/controls/SiteSizeSlider.svelte +185 -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 +277 -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 +29 -0
- package/dist/map-v2/features/sites/stores/siteStoreContext.svelte.js +73 -0
- package/dist/map-v2/features/sites/types.d.ts +36 -0
- package/dist/map-v2/features/sites/types.js +4 -0
- package/dist/map-v2/features/sites/utils/siteGeoJSON.d.ts +31 -0
- package/dist/map-v2/features/sites/utils/siteGeoJSON.js +34 -0
- package/dist/map-v2/features/sites/utils/siteTreeUtils.d.ts +14 -0
- package/dist/{map → map-v2/features/sites}/utils/siteTreeUtils.js +3 -50
- 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/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,277 @@
|
|
|
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
|
+
|
|
64
|
+
if (map) {
|
|
65
|
+
updateLayer();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
onMount(() => {
|
|
70
|
+
const mapUnsub = mapStore.subscribe((m: mapboxgl.Map | null) => {
|
|
71
|
+
if (!m) return;
|
|
72
|
+
map = m;
|
|
73
|
+
// Map is guaranteed to be ready (style loaded) by MapboxProvider
|
|
74
|
+
initializeLayer();
|
|
75
|
+
|
|
76
|
+
// Re-initialize layer when map style changes
|
|
77
|
+
// Mapbox removes all custom layers/sources when setStyle() is called
|
|
78
|
+
map.on('style.load', initializeLayer);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
mapUnsub();
|
|
83
|
+
cleanup();
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
onDestroy(() => {
|
|
88
|
+
cleanup();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function initializeLayer(): void {
|
|
92
|
+
if (!map) return;
|
|
93
|
+
|
|
94
|
+
// Add empty GeoJSON source
|
|
95
|
+
addSourceIfMissing(map, sourceId, {
|
|
96
|
+
type: 'geojson',
|
|
97
|
+
data: {
|
|
98
|
+
type: 'FeatureCollection',
|
|
99
|
+
features: []
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Add circle layer with current store values
|
|
104
|
+
addLayerIfMissing(map, {
|
|
105
|
+
id: layerId,
|
|
106
|
+
type: 'circle',
|
|
107
|
+
source: sourceId,
|
|
108
|
+
paint: {
|
|
109
|
+
'circle-radius': store.size,
|
|
110
|
+
'circle-color': store.color,
|
|
111
|
+
'circle-opacity': store.opacity,
|
|
112
|
+
'circle-stroke-width': 2,
|
|
113
|
+
'circle-stroke-color': '#ffffff',
|
|
114
|
+
'circle-stroke-opacity': store.opacity
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Add label layer
|
|
119
|
+
addLayerIfMissing(map, {
|
|
120
|
+
id: labelLayerId,
|
|
121
|
+
type: 'symbol',
|
|
122
|
+
source: sourceId,
|
|
123
|
+
layout: {
|
|
124
|
+
'text-field': ['get', 'name'],
|
|
125
|
+
'text-size': store.labelSize,
|
|
126
|
+
'text-offset': [0, 1.5],
|
|
127
|
+
'text-anchor': 'top',
|
|
128
|
+
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular']
|
|
129
|
+
},
|
|
130
|
+
paint: {
|
|
131
|
+
'text-color': store.labelColor,
|
|
132
|
+
'text-halo-color': '#fff',
|
|
133
|
+
'text-halo-width': 1.5
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Set initial visibility
|
|
138
|
+
map.setLayoutProperty(labelLayerId, 'visibility', store.showLabels ? 'visible' : 'none');
|
|
139
|
+
|
|
140
|
+
// Add event handlers
|
|
141
|
+
if (clickable) {
|
|
142
|
+
map.on('click', layerId, handleClick);
|
|
143
|
+
map.on('mouseenter', layerId, handleMouseEnter);
|
|
144
|
+
map.on('mouseleave', layerId, handleMouseLeave);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Initial data load
|
|
148
|
+
updateLayer();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function updateLayer(): void {
|
|
152
|
+
if (!map) return;
|
|
153
|
+
|
|
154
|
+
// Update data
|
|
155
|
+
const geojson = sitesToGeoJSON(store.filteredSites);
|
|
156
|
+
updateGeoJSONSource(map, sourceId, geojson);
|
|
157
|
+
|
|
158
|
+
// Update circle visual properties
|
|
159
|
+
map.setPaintProperty(layerId, 'circle-radius', store.size);
|
|
160
|
+
map.setPaintProperty(layerId, 'circle-color', store.color);
|
|
161
|
+
map.setPaintProperty(layerId, 'circle-opacity', store.opacity);
|
|
162
|
+
map.setPaintProperty(layerId, 'circle-stroke-opacity', store.opacity);
|
|
163
|
+
|
|
164
|
+
// Update label properties
|
|
165
|
+
map.setLayoutProperty(labelLayerId, 'text-size', store.labelSize);
|
|
166
|
+
map.setPaintProperty(labelLayerId, 'text-color', store.labelColor);
|
|
167
|
+
map.setLayoutProperty(labelLayerId, 'visibility', store.showLabels ? 'visible' : 'none');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleClick(e: mapboxgl.MapLayerMouseEvent): void {
|
|
171
|
+
if (!e.features || e.features.length === 0) return;
|
|
172
|
+
const feature = e.features[0];
|
|
173
|
+
const siteProps = feature.properties;
|
|
174
|
+
if (siteProps && e.lngLat) {
|
|
175
|
+
createSitePopup(siteProps, e.lngLat);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createSitePopup(siteProps: any, lngLat: mapboxgl.LngLat): void {
|
|
180
|
+
if (!map) return;
|
|
181
|
+
|
|
182
|
+
// Remove existing popup
|
|
183
|
+
if (popup) {
|
|
184
|
+
popup.remove();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Parse arrays if needed
|
|
188
|
+
const fbands = siteProps.fbands ? JSON.parse(siteProps.fbands) : [];
|
|
189
|
+
const cellNames = siteProps.cellNames ? JSON.parse(siteProps.cellNames) : [];
|
|
190
|
+
|
|
191
|
+
const fbandsHtml = fbands.length > 0
|
|
192
|
+
? `<div><strong>Bands:</strong> ${fbands.join(', ')} MHz</div>`
|
|
193
|
+
: '';
|
|
194
|
+
|
|
195
|
+
const html = `
|
|
196
|
+
<div style="font-family: system-ui, -apple-system, sans-serif; font-size: 13px;">
|
|
197
|
+
<h6 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600;">${siteProps.name}</h6>
|
|
198
|
+
<div style="color: #666; line-height: 1.6;">
|
|
199
|
+
<div><strong>ID:</strong> ${siteProps.id}</div>
|
|
200
|
+
<div><strong>Technology:</strong> <span class="badge bg-primary">${siteProps.technology}</span></div>
|
|
201
|
+
${fbandsHtml}
|
|
202
|
+
<div><strong>Provider:</strong> ${siteProps.provider}</div>
|
|
203
|
+
<div><strong>Group:</strong> ${siteProps.featureGroup}</div>
|
|
204
|
+
<div><strong>Cells:</strong> ${cellNames.length}</div>
|
|
205
|
+
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee; font-size: 11px;">
|
|
206
|
+
<strong>Coordinates:</strong><br/>
|
|
207
|
+
${Number(siteProps.latitude).toFixed(6)}, ${Number(siteProps.longitude).toFixed(6)}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
// Create and add popup
|
|
214
|
+
popup = new mapboxgl.Popup({
|
|
215
|
+
closeButton: true,
|
|
216
|
+
closeOnClick: false,
|
|
217
|
+
maxWidth: '300px'
|
|
218
|
+
})
|
|
219
|
+
.setLngLat(lngLat)
|
|
220
|
+
.setHTML(html)
|
|
221
|
+
.addTo(map);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleMouseEnter(e: mapboxgl.MapLayerMouseEvent): void {
|
|
225
|
+
if (!map) return;
|
|
226
|
+
map.getCanvas().style.cursor = 'pointer';
|
|
227
|
+
|
|
228
|
+
if (!e.features || e.features.length === 0) return;
|
|
229
|
+
const feature = e.features[0];
|
|
230
|
+
const siteId = feature.properties?.id;
|
|
231
|
+
|
|
232
|
+
if (siteId) {
|
|
233
|
+
hoveredSiteId = siteId;
|
|
234
|
+
// Optional: Add hover effect via feature state
|
|
235
|
+
map.setFeatureState(
|
|
236
|
+
{ source: sourceId, id: siteId },
|
|
237
|
+
{ hovered: true }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleMouseLeave(): void {
|
|
243
|
+
if (!map) return;
|
|
244
|
+
map.getCanvas().style.cursor = '';
|
|
245
|
+
|
|
246
|
+
if (hoveredSiteId) {
|
|
247
|
+
// Remove hover state
|
|
248
|
+
map.removeFeatureState(
|
|
249
|
+
{ source: sourceId, id: hoveredSiteId },
|
|
250
|
+
'hovered'
|
|
251
|
+
);
|
|
252
|
+
hoveredSiteId = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function cleanup(): void {
|
|
257
|
+
if (popup) {
|
|
258
|
+
popup.remove();
|
|
259
|
+
popup = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (map) {
|
|
263
|
+
// Remove style.load listener
|
|
264
|
+
map.off('style.load', initializeLayer);
|
|
265
|
+
|
|
266
|
+
if (clickable) {
|
|
267
|
+
map.off('click', layerId, handleClick);
|
|
268
|
+
map.off('mouseenter', layerId, handleMouseEnter);
|
|
269
|
+
map.off('mouseleave', layerId, handleMouseLeave);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Remove layers and source
|
|
273
|
+
removeLayerAndSource(map, labelLayerId, sourceId);
|
|
274
|
+
removeLayerAndSource(map, layerId, sourceId);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
</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,29 @@
|
|
|
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
|
+
*/
|
|
7
|
+
import type { Site, SiteStoreValue } from '../types';
|
|
8
|
+
export declare function createSiteStoreContext(initialSites?: Site[]): {
|
|
9
|
+
allSites: Site[];
|
|
10
|
+
filteredSites: Site[];
|
|
11
|
+
size: number;
|
|
12
|
+
color: string;
|
|
13
|
+
opacity: number;
|
|
14
|
+
showLabels: boolean;
|
|
15
|
+
showOnHover: boolean;
|
|
16
|
+
labelSize: number;
|
|
17
|
+
labelColor: string;
|
|
18
|
+
strokeWidth: number;
|
|
19
|
+
strokeColor: string;
|
|
20
|
+
setAllSites(sites: Site[]): void;
|
|
21
|
+
setFilteredSites(sites: Site[]): void;
|
|
22
|
+
setSize(size: number): void;
|
|
23
|
+
setColor(color: string): void;
|
|
24
|
+
setOpacity(opacity: number): void;
|
|
25
|
+
setShowLabels(show: boolean): void;
|
|
26
|
+
reset(): void;
|
|
27
|
+
getState(): SiteStoreValue;
|
|
28
|
+
};
|
|
29
|
+
export type SiteStoreContext = ReturnType<typeof createSiteStoreContext>;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/
|
|
7
|
+
export function createSiteStoreContext(initialSites = []) {
|
|
8
|
+
// Internal reactive state
|
|
9
|
+
let state = $state({
|
|
10
|
+
allSites: initialSites,
|
|
11
|
+
filteredSites: initialSites,
|
|
12
|
+
size: 10,
|
|
13
|
+
color: '#3b82f6',
|
|
14
|
+
opacity: 1.0,
|
|
15
|
+
showLabels: false,
|
|
16
|
+
showOnHover: true,
|
|
17
|
+
labelSize: 12,
|
|
18
|
+
labelColor: '#000000',
|
|
19
|
+
strokeWidth: 2,
|
|
20
|
+
strokeColor: '#ffffff'
|
|
21
|
+
});
|
|
22
|
+
// Return object with getters/setters for direct binding
|
|
23
|
+
return {
|
|
24
|
+
// Bindable properties with getters/setters
|
|
25
|
+
get allSites() { return state.allSites; },
|
|
26
|
+
set allSites(value) { state.allSites = value; },
|
|
27
|
+
get filteredSites() { return state.filteredSites; },
|
|
28
|
+
set filteredSites(value) { state.filteredSites = value; },
|
|
29
|
+
get size() { return state.size; },
|
|
30
|
+
set size(value) { state.size = value; },
|
|
31
|
+
get color() { return state.color; },
|
|
32
|
+
set color(value) { state.color = value; },
|
|
33
|
+
get opacity() { return state.opacity; },
|
|
34
|
+
set opacity(value) { state.opacity = value; },
|
|
35
|
+
get showLabels() { return state.showLabels; },
|
|
36
|
+
set showLabels(value) { state.showLabels = value; },
|
|
37
|
+
get showOnHover() { return state.showOnHover; },
|
|
38
|
+
set showOnHover(value) { state.showOnHover = value; },
|
|
39
|
+
get labelSize() { return state.labelSize; },
|
|
40
|
+
set labelSize(value) { state.labelSize = value; },
|
|
41
|
+
get labelColor() { return state.labelColor; },
|
|
42
|
+
set labelColor(value) { state.labelColor = value; },
|
|
43
|
+
get strokeWidth() { return state.strokeWidth ?? 2; },
|
|
44
|
+
set strokeWidth(value) { state.strokeWidth = value; },
|
|
45
|
+
get strokeColor() { return state.strokeColor ?? '#ffffff'; },
|
|
46
|
+
set strokeColor(value) { state.strokeColor = value; },
|
|
47
|
+
// Convenience methods (optional, but nice to have)
|
|
48
|
+
setAllSites(sites) { state.allSites = sites; },
|
|
49
|
+
setFilteredSites(sites) { state.filteredSites = sites; },
|
|
50
|
+
setSize(size) { state.size = size; },
|
|
51
|
+
setColor(color) { state.color = color; },
|
|
52
|
+
setOpacity(opacity) { state.opacity = opacity; },
|
|
53
|
+
setShowLabels(show) { state.showLabels = show; },
|
|
54
|
+
// Reset to defaults
|
|
55
|
+
reset() {
|
|
56
|
+
state.allSites = initialSites;
|
|
57
|
+
state.filteredSites = initialSites;
|
|
58
|
+
state.size = 10;
|
|
59
|
+
state.color = '#3b82f6';
|
|
60
|
+
state.opacity = 1.0;
|
|
61
|
+
state.showLabels = false;
|
|
62
|
+
state.showOnHover = true;
|
|
63
|
+
state.labelSize = 12;
|
|
64
|
+
state.labelColor = '#000000';
|
|
65
|
+
state.strokeWidth = 2;
|
|
66
|
+
state.strokeColor = '#ffffff';
|
|
67
|
+
},
|
|
68
|
+
// Get snapshot of current state (useful for debugging)
|
|
69
|
+
getState() {
|
|
70
|
+
return { ...state };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
hoverColor?: string;
|
|
33
|
+
selectedColor?: string;
|
|
34
|
+
strokeWidth?: number;
|
|
35
|
+
strokeColor?: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
*/
|
|
27
|
+
export declare function sitesToGeoJSON(sites: Site[]): FeatureCollection<SiteFeature>;
|
|
28
|
+
/**
|
|
29
|
+
* Converts a single site to a GeoJSON Feature
|
|
30
|
+
*/
|
|
31
|
+
export declare function siteToFeature(site: Site): SiteFeature;
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
*/
|
|
8
|
+
export function sitesToGeoJSON(sites) {
|
|
9
|
+
const features = sites.map((site) => ({
|
|
10
|
+
type: 'Feature',
|
|
11
|
+
geometry: {
|
|
12
|
+
type: 'Point',
|
|
13
|
+
coordinates: [site.longitude, site.latitude]
|
|
14
|
+
},
|
|
15
|
+
properties: site
|
|
16
|
+
}));
|
|
17
|
+
return {
|
|
18
|
+
type: 'FeatureCollection',
|
|
19
|
+
features
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Converts a single site to a GeoJSON Feature
|
|
24
|
+
*/
|
|
25
|
+
export function siteToFeature(site) {
|
|
26
|
+
return {
|
|
27
|
+
type: 'Feature',
|
|
28
|
+
geometry: {
|
|
29
|
+
type: 'Point',
|
|
30
|
+
coordinates: [site.longitude, site.latitude]
|
|
31
|
+
},
|
|
32
|
+
properties: site
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
*/
|
|
10
|
+
export declare function buildSiteTree(sites: Site[]): TreeNode;
|
|
11
|
+
/**
|
|
12
|
+
* Filters sites based on checked tree paths
|
|
13
|
+
*/
|
|
14
|
+
export declare function getFilteredSites(checkedPaths: string[], allSites: Site[]): Site[];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utilities for building and managing site filter tree
|
|
3
3
|
*/
|
|
4
|
-
const STORAGE_PREFIX = 'cellular:siteFilter';
|
|
5
4
|
/**
|
|
6
5
|
* Builds a hierarchical tree from flat site array
|
|
7
6
|
* Structure: All Sites -> Provider -> Feature Group
|
|
@@ -19,8 +18,6 @@ export function buildSiteTree(sites) {
|
|
|
19
18
|
}
|
|
20
19
|
featureGroups.get(site.featureGroup).push(site);
|
|
21
20
|
});
|
|
22
|
-
// Load saved state from localStorage
|
|
23
|
-
const savedState = loadTreeState();
|
|
24
21
|
// Build tree structure
|
|
25
22
|
const children = [];
|
|
26
23
|
// Sort providers alphabetically
|
|
@@ -36,7 +33,7 @@ export function buildSiteTree(sites) {
|
|
|
36
33
|
providerChildren.push({
|
|
37
34
|
id: nodeId,
|
|
38
35
|
label: `${featureGroup} (${groupSites.length})`,
|
|
39
|
-
defaultChecked:
|
|
36
|
+
defaultChecked: true,
|
|
40
37
|
children: [],
|
|
41
38
|
metadata: {
|
|
42
39
|
type: 'featureGroup',
|
|
@@ -50,7 +47,7 @@ export function buildSiteTree(sites) {
|
|
|
50
47
|
children.push({
|
|
51
48
|
id: providerId,
|
|
52
49
|
label: `${provider} (${Array.from(featureGroups.values()).flat().length})`,
|
|
53
|
-
defaultChecked:
|
|
50
|
+
defaultChecked: true,
|
|
54
51
|
children: providerChildren,
|
|
55
52
|
metadata: {
|
|
56
53
|
type: 'provider',
|
|
@@ -62,7 +59,7 @@ export function buildSiteTree(sites) {
|
|
|
62
59
|
return {
|
|
63
60
|
id: 'all-sites',
|
|
64
61
|
label: `All Sites (${sites.length})`,
|
|
65
|
-
defaultChecked:
|
|
62
|
+
defaultChecked: true,
|
|
66
63
|
children,
|
|
67
64
|
metadata: {
|
|
68
65
|
type: 'root'
|
|
@@ -118,47 +115,3 @@ export function getFilteredSites(checkedPaths, allSites) {
|
|
|
118
115
|
console.log('Filtered:', filtered.length, 'of', allSites.length, 'sites');
|
|
119
116
|
return filtered;
|
|
120
117
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Saves tree checked paths to localStorage
|
|
123
|
-
*/
|
|
124
|
-
export function saveTreeState(checkedPaths) {
|
|
125
|
-
try {
|
|
126
|
-
localStorage.setItem(STORAGE_PREFIX, JSON.stringify(checkedPaths));
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
console.warn('Failed to save tree state to localStorage:', error);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Loads tree state from localStorage
|
|
134
|
-
* Returns map of node IDs to checked state
|
|
135
|
-
*/
|
|
136
|
-
export function loadTreeState() {
|
|
137
|
-
try {
|
|
138
|
-
const saved = localStorage.getItem(STORAGE_PREFIX);
|
|
139
|
-
if (saved) {
|
|
140
|
-
const paths = JSON.parse(saved);
|
|
141
|
-
// Convert paths to map for easier lookup during tree building
|
|
142
|
-
const stateMap = {};
|
|
143
|
-
paths.forEach((path) => {
|
|
144
|
-
stateMap[path] = true;
|
|
145
|
-
});
|
|
146
|
-
return stateMap;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
console.warn('Failed to load tree state from localStorage:', error);
|
|
151
|
-
}
|
|
152
|
-
return {};
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Clears saved tree state
|
|
156
|
-
*/
|
|
157
|
-
export function clearTreeState() {
|
|
158
|
-
try {
|
|
159
|
-
localStorage.removeItem(STORAGE_PREFIX);
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
console.warn('Failed to clear tree state from localStorage:', error);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map V2 - Feature-Based Cellular Visualization Library
|
|
3
|
+
*
|
|
4
|
+
* A decoupled, feature-based architecture for Mapbox cellular network visualization.
|
|
5
|
+
* Each feature (sites, cells) is completely independent with its own store, layers, and controls.
|
|
6
|
+
*/
|
|
7
|
+
export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, MapStyleControl, createMapStore, useMapbox, tryUseMapbox } from './core';
|
|
8
|
+
export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
|
|
9
|
+
export { type Site, type SiteStoreValue, createSiteStore, SitesLayer, SiteFilterControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
|
|
10
|
+
export { DemoMap, demoSites } from './demo';
|