@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
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* CellsLayer - Visualizes cellular sectors/cells as polygons on the map
|
|
4
|
-
*
|
|
5
|
-
* Consumes:
|
|
6
|
-
* - Map instance from MapboxProvider
|
|
7
|
-
* - Cells and sites data from CellDataProvider
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Displays cell coverage as sector polygons
|
|
11
|
-
* - Supports selection and hover states
|
|
12
|
-
* - Click to select a cell
|
|
13
|
-
*/
|
|
14
|
-
import { onMount, onDestroy } from 'svelte';
|
|
15
|
-
import type mapboxgl from 'mapbox-gl';
|
|
16
|
-
import { useMapbox } from '../hooks/useMapbox';
|
|
17
|
-
import { useCellData } from '../hooks/useCellData';
|
|
18
|
-
import { cellsToGeoJSON } from '../utils/geojson';
|
|
19
|
-
import { LayerBase } from './LayerBase';
|
|
20
|
-
import type { Cell, Site } from '../types';
|
|
21
|
-
|
|
22
|
-
interface Props {
|
|
23
|
-
/** Namespace for layer IDs (default: 'cellular') */
|
|
24
|
-
namespace?: string;
|
|
25
|
-
/** Enable click to select cells (default: true) */
|
|
26
|
-
clickable?: boolean;
|
|
27
|
-
/** Default cell radius in meters (default: 500) */
|
|
28
|
-
defaultRadius?: number;
|
|
29
|
-
/** Cell fill color (default: '#3b82f6') */
|
|
30
|
-
fillColor?: string;
|
|
31
|
-
/** Cell fill opacity (default: 0.3) */
|
|
32
|
-
fillOpacity?: number;
|
|
33
|
-
/** Selected cell fill color (default: '#ef4444') */
|
|
34
|
-
selectedFillColor?: string;
|
|
35
|
-
/** Cell outline color (default: '#1e40af') */
|
|
36
|
-
outlineColor?: string;
|
|
37
|
-
/** Cell outline width (default: 2) */
|
|
38
|
-
outlineWidth?: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let {
|
|
42
|
-
namespace = 'cellular',
|
|
43
|
-
clickable = true,
|
|
44
|
-
defaultRadius = 500,
|
|
45
|
-
fillColor = '#3b82f6',
|
|
46
|
-
fillOpacity = 0.3,
|
|
47
|
-
selectedFillColor = '#ef4444',
|
|
48
|
-
outlineColor = '#1e40af',
|
|
49
|
-
outlineWidth = 2
|
|
50
|
-
}: Props = $props();
|
|
51
|
-
|
|
52
|
-
const mapStore = useMapbox();
|
|
53
|
-
const cellDataContext = useCellData();
|
|
54
|
-
|
|
55
|
-
let map: mapboxgl.Map | null = null;
|
|
56
|
-
let layerManager: LayerBase | null = null;
|
|
57
|
-
let unsubscribers: (() => void)[] = [];
|
|
58
|
-
|
|
59
|
-
onMount(() => {
|
|
60
|
-
// Subscribe to map store
|
|
61
|
-
const mapUnsub = mapStore.subscribe((m) => {
|
|
62
|
-
if (!m) return;
|
|
63
|
-
map = m;
|
|
64
|
-
initializeLayer();
|
|
65
|
-
});
|
|
66
|
-
unsubscribers.push(mapUnsub);
|
|
67
|
-
|
|
68
|
-
// Subscribe to cells data
|
|
69
|
-
const cellsUnsub = cellDataContext.cells.subscribe(() => {
|
|
70
|
-
if (map && layerManager) {
|
|
71
|
-
updateLayerData();
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
unsubscribers.push(cellsUnsub);
|
|
75
|
-
|
|
76
|
-
// Subscribe to sites data (cells need site locations)
|
|
77
|
-
const sitesUnsub = cellDataContext.sites.subscribe(() => {
|
|
78
|
-
if (map && layerManager) {
|
|
79
|
-
updateLayerData();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
unsubscribers.push(sitesUnsub);
|
|
83
|
-
|
|
84
|
-
// Subscribe to selection state
|
|
85
|
-
const selectionUnsub = cellDataContext.selectedCellId.subscribe(() => {
|
|
86
|
-
if (map) {
|
|
87
|
-
updateLayerStyle();
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
unsubscribers.push(selectionUnsub);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
onDestroy(() => {
|
|
94
|
-
cleanup();
|
|
95
|
-
unsubscribers.forEach((unsub) => unsub());
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
function initializeLayer(): void {
|
|
99
|
-
if (!map) return;
|
|
100
|
-
|
|
101
|
-
layerManager = new LayerBase(map, {
|
|
102
|
-
namespace,
|
|
103
|
-
layerName: 'cells',
|
|
104
|
-
sourceName: 'cells'
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Add empty GeoJSON source
|
|
108
|
-
layerManager['addGeoJSONSource']({
|
|
109
|
-
type: 'FeatureCollection',
|
|
110
|
-
features: []
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Add fill layer
|
|
114
|
-
layerManager['addLayer']({
|
|
115
|
-
id: layerManager.getLayerId(),
|
|
116
|
-
type: 'fill',
|
|
117
|
-
source: layerManager.getSourceId(),
|
|
118
|
-
paint: {
|
|
119
|
-
'fill-color': [
|
|
120
|
-
'case',
|
|
121
|
-
['boolean', ['feature-state', 'selected'], false],
|
|
122
|
-
selectedFillColor,
|
|
123
|
-
fillColor
|
|
124
|
-
],
|
|
125
|
-
'fill-opacity': fillOpacity
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Add outline layer
|
|
130
|
-
layerManager['addLayer']({
|
|
131
|
-
id: `${layerManager.getLayerId()}-outline`,
|
|
132
|
-
type: 'line',
|
|
133
|
-
source: layerManager.getSourceId(),
|
|
134
|
-
paint: {
|
|
135
|
-
'line-color': outlineColor,
|
|
136
|
-
'line-width': outlineWidth
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Add click handler
|
|
141
|
-
if (clickable) {
|
|
142
|
-
map.on('click', layerManager.getLayerId(), handleClick);
|
|
143
|
-
map.on('mouseenter', layerManager.getLayerId(), handleMouseEnter);
|
|
144
|
-
map.on('mouseleave', layerManager.getLayerId(), handleMouseLeave);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Initial data load
|
|
148
|
-
updateLayerData();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function updateLayerData(): void {
|
|
152
|
-
if (!layerManager) return;
|
|
153
|
-
|
|
154
|
-
let currentCells: Cell[] = [];
|
|
155
|
-
let currentSites: Site[] = [];
|
|
156
|
-
|
|
157
|
-
cellDataContext.cells.subscribe((c) => {
|
|
158
|
-
currentCells = c;
|
|
159
|
-
})();
|
|
160
|
-
cellDataContext.sites.subscribe((s) => {
|
|
161
|
-
currentSites = s;
|
|
162
|
-
})();
|
|
163
|
-
|
|
164
|
-
const geojson = cellsToGeoJSON(currentCells, currentSites, defaultRadius);
|
|
165
|
-
layerManager['updateSource'](geojson);
|
|
166
|
-
updateLayerStyle();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function updateLayerStyle(): void {
|
|
170
|
-
if (!map || !layerManager) return;
|
|
171
|
-
|
|
172
|
-
let currentSelectedId: string | null = null;
|
|
173
|
-
cellDataContext.selectedCellId.subscribe((id) => {
|
|
174
|
-
currentSelectedId = id;
|
|
175
|
-
})();
|
|
176
|
-
|
|
177
|
-
// Update feature states
|
|
178
|
-
let currentCells: Cell[] = [];
|
|
179
|
-
cellDataContext.cells.subscribe((c) => {
|
|
180
|
-
currentCells = c;
|
|
181
|
-
})();
|
|
182
|
-
|
|
183
|
-
currentCells.forEach((cell) => {
|
|
184
|
-
if (map) {
|
|
185
|
-
map.setFeatureState(
|
|
186
|
-
{
|
|
187
|
-
source: layerManager!.getSourceId(),
|
|
188
|
-
id: cell.id
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
selected: cell.id === currentSelectedId
|
|
192
|
-
}
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function handleClick(e: mapboxgl.MapLayerMouseEvent): void {
|
|
199
|
-
if (!e.features || e.features.length === 0) return;
|
|
200
|
-
const feature = e.features[0];
|
|
201
|
-
const cellId = feature.properties?.id;
|
|
202
|
-
const siteId = feature.properties?.siteId;
|
|
203
|
-
if (cellId) {
|
|
204
|
-
cellDataContext.selectedCellId.set(cellId);
|
|
205
|
-
if (siteId) {
|
|
206
|
-
cellDataContext.selectedSiteId.set(siteId);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function handleMouseEnter(e: mapboxgl.MapLayerMouseEvent): void {
|
|
212
|
-
if (!map) return;
|
|
213
|
-
map.getCanvas().style.cursor = 'pointer';
|
|
214
|
-
if (!e.features || e.features.length === 0) return;
|
|
215
|
-
const feature = e.features[0];
|
|
216
|
-
const cellId = feature.properties?.id;
|
|
217
|
-
if (cellId) {
|
|
218
|
-
cellDataContext.hoveredCellId.set(cellId);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function handleMouseLeave(): void {
|
|
223
|
-
if (!map) return;
|
|
224
|
-
map.getCanvas().style.cursor = '';
|
|
225
|
-
cellDataContext.hoveredCellId.set(null);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function cleanup(): void {
|
|
229
|
-
if (map && layerManager) {
|
|
230
|
-
if (clickable) {
|
|
231
|
-
map.off('click', layerManager.getLayerId(), handleClick);
|
|
232
|
-
map.off('mouseenter', layerManager.getLayerId(), handleMouseEnter);
|
|
233
|
-
map.off('mouseleave', layerManager.getLayerId(), handleMouseLeave);
|
|
234
|
-
}
|
|
235
|
-
// Remove outline layer as well
|
|
236
|
-
if (map.getLayer(`${layerManager.getLayerId()}-outline`)) {
|
|
237
|
-
map.removeLayer(`${layerManager.getLayerId()}-outline`);
|
|
238
|
-
}
|
|
239
|
-
layerManager.cleanup();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
</script>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
interface Props {
|
|
2
|
-
/** Namespace for layer IDs (default: 'cellular') */
|
|
3
|
-
namespace?: string;
|
|
4
|
-
/** Enable click to select cells (default: true) */
|
|
5
|
-
clickable?: boolean;
|
|
6
|
-
/** Default cell radius in meters (default: 500) */
|
|
7
|
-
defaultRadius?: number;
|
|
8
|
-
/** Cell fill color (default: '#3b82f6') */
|
|
9
|
-
fillColor?: string;
|
|
10
|
-
/** Cell fill opacity (default: 0.3) */
|
|
11
|
-
fillOpacity?: number;
|
|
12
|
-
/** Selected cell fill color (default: '#ef4444') */
|
|
13
|
-
selectedFillColor?: string;
|
|
14
|
-
/** Cell outline color (default: '#1e40af') */
|
|
15
|
-
outlineColor?: string;
|
|
16
|
-
/** Cell outline width (default: 2) */
|
|
17
|
-
outlineWidth?: number;
|
|
18
|
-
}
|
|
19
|
-
declare const CellsLayer: import("svelte").Component<Props, {}, "">;
|
|
20
|
-
type CellsLayer = ReturnType<typeof CellsLayer>;
|
|
21
|
-
export default CellsLayer;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* CoverageLayer - Optional aggregated coverage visualization
|
|
4
|
-
*
|
|
5
|
-
* This is a placeholder for future coverage/heatmap features
|
|
6
|
-
* Can be extended to show:
|
|
7
|
-
* - Signal strength heatmaps
|
|
8
|
-
* - Coverage overlays
|
|
9
|
-
* - Interference zones
|
|
10
|
-
*/
|
|
11
|
-
import { onMount, onDestroy } from 'svelte';
|
|
12
|
-
import { useMapbox } from '../hooks/useMapbox';
|
|
13
|
-
import { useCellData } from '../hooks/useCellData';
|
|
14
|
-
|
|
15
|
-
interface Props {
|
|
16
|
-
/** Namespace for layer IDs (default: 'cellular') */
|
|
17
|
-
namespace?: string;
|
|
18
|
-
/** Coverage visualization type */
|
|
19
|
-
type?: 'heatmap' | 'overlay' | 'none';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let { namespace = 'cellular', type = 'none' }: Props = $props();
|
|
23
|
-
|
|
24
|
-
const mapStore = useMapbox();
|
|
25
|
-
const cellDataContext = useCellData();
|
|
26
|
-
|
|
27
|
-
onMount(() => {
|
|
28
|
-
// Placeholder for future implementation
|
|
29
|
-
console.log('CoverageLayer mounted with type:', type);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
onDestroy(() => {
|
|
33
|
-
// Cleanup logic here
|
|
34
|
-
});
|
|
35
|
-
</script>
|
|
36
|
-
|
|
37
|
-
<!-- Placeholder component - no visual output yet -->
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
interface Props {
|
|
2
|
-
/** Namespace for layer IDs (default: 'cellular') */
|
|
3
|
-
namespace?: string;
|
|
4
|
-
/** Coverage visualization type */
|
|
5
|
-
type?: 'heatmap' | 'overlay' | 'none';
|
|
6
|
-
}
|
|
7
|
-
declare const CoverageLayer: import("svelte").Component<Props, {}, "">;
|
|
8
|
-
type CoverageLayer = ReturnType<typeof CoverageLayer>;
|
|
9
|
-
export default CoverageLayer;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for Mapbox layer components
|
|
3
|
-
*/
|
|
4
|
-
export interface LayerConfig {
|
|
5
|
-
namespace: string;
|
|
6
|
-
layerName: string;
|
|
7
|
-
sourceName: string;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Base class for managing Mapbox layers with consistent patterns
|
|
11
|
-
*/
|
|
12
|
-
export declare class LayerBase {
|
|
13
|
-
protected map: mapboxgl.Map;
|
|
14
|
-
protected config: LayerConfig;
|
|
15
|
-
protected layerId: string;
|
|
16
|
-
protected sourceId: string;
|
|
17
|
-
constructor(map: mapboxgl.Map, config: LayerConfig);
|
|
18
|
-
/**
|
|
19
|
-
* Adds a GeoJSON source to the map
|
|
20
|
-
*/
|
|
21
|
-
protected addGeoJSONSource(data: GeoJSON.FeatureCollection | GeoJSON.Feature): void;
|
|
22
|
-
/**
|
|
23
|
-
* Adds a layer to the map
|
|
24
|
-
*/
|
|
25
|
-
protected addLayer(layer: mapboxgl.AnyLayer, beforeId?: string): void;
|
|
26
|
-
/**
|
|
27
|
-
* Updates the GeoJSON source data
|
|
28
|
-
*/
|
|
29
|
-
protected updateSource(data: GeoJSON.FeatureCollection | GeoJSON.Feature): void;
|
|
30
|
-
/**
|
|
31
|
-
* Removes the layer and source from the map
|
|
32
|
-
*/
|
|
33
|
-
cleanup(): void;
|
|
34
|
-
/**
|
|
35
|
-
* Gets the layer ID
|
|
36
|
-
*/
|
|
37
|
-
getLayerId(): string;
|
|
38
|
-
/**
|
|
39
|
-
* Gets the source ID
|
|
40
|
-
*/
|
|
41
|
-
getSourceId(): string;
|
|
42
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for Mapbox layer components
|
|
3
|
-
*/
|
|
4
|
-
import { addSourceIfMissing, addLayerIfMissing, removeLayerAndSource, generateLayerId, generateSourceId, updateGeoJSONSource } from '../utils/mapboxHelpers';
|
|
5
|
-
/**
|
|
6
|
-
* Base class for managing Mapbox layers with consistent patterns
|
|
7
|
-
*/
|
|
8
|
-
export class LayerBase {
|
|
9
|
-
map;
|
|
10
|
-
config;
|
|
11
|
-
layerId;
|
|
12
|
-
sourceId;
|
|
13
|
-
constructor(map, config) {
|
|
14
|
-
this.map = map;
|
|
15
|
-
this.config = config;
|
|
16
|
-
this.layerId = generateLayerId(config.namespace, config.layerName);
|
|
17
|
-
this.sourceId = generateSourceId(config.namespace, config.sourceName);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Adds a GeoJSON source to the map
|
|
21
|
-
*/
|
|
22
|
-
addGeoJSONSource(data) {
|
|
23
|
-
addSourceIfMissing(this.map, this.sourceId, {
|
|
24
|
-
type: 'geojson',
|
|
25
|
-
data
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Adds a layer to the map
|
|
30
|
-
*/
|
|
31
|
-
addLayer(layer, beforeId) {
|
|
32
|
-
addLayerIfMissing(this.map, layer, beforeId);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Updates the GeoJSON source data
|
|
36
|
-
*/
|
|
37
|
-
updateSource(data) {
|
|
38
|
-
updateGeoJSONSource(this.map, this.sourceId, data);
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Removes the layer and source from the map
|
|
42
|
-
*/
|
|
43
|
-
cleanup() {
|
|
44
|
-
removeLayerAndSource(this.map, this.layerId, this.sourceId);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Gets the layer ID
|
|
48
|
-
*/
|
|
49
|
-
getLayerId() {
|
|
50
|
-
return this.layerId;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Gets the source ID
|
|
54
|
-
*/
|
|
55
|
-
getSourceId() {
|
|
56
|
-
return this.sourceId;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SitesLayer - Visualizes cellular sites as points on the map
|
|
4
|
-
*
|
|
5
|
-
* Consumes:
|
|
6
|
-
* - Map instance from MapboxProvider
|
|
7
|
-
* - Sites data from CellDataProvider
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Displays sites as circles
|
|
11
|
-
* - Supports selection and hover states
|
|
12
|
-
* - Click to select a site
|
|
13
|
-
*/
|
|
14
|
-
import { onMount, onDestroy } from 'svelte';
|
|
15
|
-
import mapboxgl from 'mapbox-gl';
|
|
16
|
-
import { useMapbox } from '../hooks/useMapbox';
|
|
17
|
-
import { useCellData } from '../hooks/useCellData';
|
|
18
|
-
import { sitesToGeoJSON } from '../utils/geojson';
|
|
19
|
-
import { LayerBase } from './LayerBase';
|
|
20
|
-
import type { Site } from '../types';
|
|
21
|
-
|
|
22
|
-
interface Props {
|
|
23
|
-
/** Namespace for layer IDs (default: 'cellular') */
|
|
24
|
-
namespace?: string;
|
|
25
|
-
/** Enable click to select sites (default: true) */
|
|
26
|
-
clickable?: boolean;
|
|
27
|
-
/** Show popup on click (default: true) */
|
|
28
|
-
showPopup?: boolean;
|
|
29
|
-
/** Site circle radius (default: 8) */
|
|
30
|
-
circleRadius?: number;
|
|
31
|
-
/** Site circle color (default: '#3b82f6') */
|
|
32
|
-
circleColor?: string;
|
|
33
|
-
/** Selected site color (default: '#ef4444') */
|
|
34
|
-
selectedColor?: string;
|
|
35
|
-
/** Hovered site color (default: '#8b5cf6') */
|
|
36
|
-
hoverColor?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let {
|
|
40
|
-
namespace = 'cellular',
|
|
41
|
-
clickable = true,
|
|
42
|
-
showPopup = true,
|
|
43
|
-
circleRadius = 8,
|
|
44
|
-
circleColor = '#3b82f6',
|
|
45
|
-
selectedColor = '#ef4444',
|
|
46
|
-
hoverColor = '#8b5cf6'
|
|
47
|
-
}: Props = $props();
|
|
48
|
-
|
|
49
|
-
const mapStore = useMapbox();
|
|
50
|
-
const cellDataContext = useCellData();
|
|
51
|
-
|
|
52
|
-
let map: mapboxgl.Map | null = null;
|
|
53
|
-
let layerManager: LayerBase | null = null;
|
|
54
|
-
let popup: mapboxgl.Popup | null = null;
|
|
55
|
-
let unsubscribers: (() => void)[] = [];
|
|
56
|
-
|
|
57
|
-
onMount(() => {
|
|
58
|
-
// Subscribe to map store
|
|
59
|
-
const mapUnsub = mapStore.subscribe((m) => {
|
|
60
|
-
if (!m) return;
|
|
61
|
-
map = m;
|
|
62
|
-
initializeLayer();
|
|
63
|
-
});
|
|
64
|
-
unsubscribers.push(mapUnsub);
|
|
65
|
-
|
|
66
|
-
// Subscribe to sites data
|
|
67
|
-
const sitesUnsub = cellDataContext.sites.subscribe((sites) => {
|
|
68
|
-
if (map && layerManager) {
|
|
69
|
-
updateLayerData(sites);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
unsubscribers.push(sitesUnsub);
|
|
73
|
-
|
|
74
|
-
// Subscribe to selection state
|
|
75
|
-
const selectionUnsub = cellDataContext.selectedSiteId.subscribe(() => {
|
|
76
|
-
if (map) {
|
|
77
|
-
updateLayerStyle();
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
unsubscribers.push(selectionUnsub);
|
|
81
|
-
|
|
82
|
-
// Subscribe to hover state
|
|
83
|
-
const hoverUnsub = cellDataContext.hoveredSiteId.subscribe(() => {
|
|
84
|
-
if (map) {
|
|
85
|
-
updateLayerStyle();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
unsubscribers.push(hoverUnsub);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
onDestroy(() => {
|
|
92
|
-
cleanup();
|
|
93
|
-
unsubscribers.forEach((unsub) => unsub());
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
function initializeLayer(): void {
|
|
97
|
-
if (!map) return;
|
|
98
|
-
|
|
99
|
-
layerManager = new LayerBase(map, {
|
|
100
|
-
namespace,
|
|
101
|
-
layerName: 'sites',
|
|
102
|
-
sourceName: 'sites'
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Add empty GeoJSON source
|
|
106
|
-
layerManager['addGeoJSONSource']({
|
|
107
|
-
type: 'FeatureCollection',
|
|
108
|
-
features: []
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Add circle layer
|
|
112
|
-
layerManager['addLayer']({
|
|
113
|
-
id: layerManager.getLayerId(),
|
|
114
|
-
type: 'circle',
|
|
115
|
-
source: layerManager.getSourceId(),
|
|
116
|
-
paint: {
|
|
117
|
-
'circle-radius': circleRadius,
|
|
118
|
-
'circle-color': [
|
|
119
|
-
'case',
|
|
120
|
-
['boolean', ['feature-state', 'selected'], false],
|
|
121
|
-
selectedColor,
|
|
122
|
-
['boolean', ['feature-state', 'hovered'], false],
|
|
123
|
-
hoverColor,
|
|
124
|
-
circleColor
|
|
125
|
-
],
|
|
126
|
-
'circle-stroke-width': 2,
|
|
127
|
-
'circle-stroke-color': '#ffffff'
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Add click handler
|
|
132
|
-
if (clickable) {
|
|
133
|
-
map.on('click', layerManager.getLayerId(), handleClick);
|
|
134
|
-
map.on('mouseenter', layerManager.getLayerId(), handleMouseEnter);
|
|
135
|
-
map.on('mouseleave', layerManager.getLayerId(), handleMouseLeave);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Initial data load
|
|
139
|
-
const sites = cellDataContext.sites;
|
|
140
|
-
let currentSites: Site[] = [];
|
|
141
|
-
sites.subscribe((s) => {
|
|
142
|
-
currentSites = s;
|
|
143
|
-
})();
|
|
144
|
-
updateLayerData(currentSites);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function updateLayerData(sites: Site[]): void {
|
|
148
|
-
if (!layerManager) return;
|
|
149
|
-
const geojson = sitesToGeoJSON(sites);
|
|
150
|
-
layerManager['updateSource'](geojson);
|
|
151
|
-
updateLayerStyle();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function updateLayerStyle(): void {
|
|
155
|
-
if (!map || !layerManager) return;
|
|
156
|
-
|
|
157
|
-
const selectedId = cellDataContext.selectedSiteId;
|
|
158
|
-
const hoveredId = cellDataContext.hoveredSiteId;
|
|
159
|
-
|
|
160
|
-
let currentSelectedId: string | null = null;
|
|
161
|
-
let currentHoveredId: string | null = null;
|
|
162
|
-
|
|
163
|
-
selectedId.subscribe((id) => {
|
|
164
|
-
currentSelectedId = id;
|
|
165
|
-
})();
|
|
166
|
-
hoveredId.subscribe((id) => {
|
|
167
|
-
currentHoveredId = id;
|
|
168
|
-
})();
|
|
169
|
-
|
|
170
|
-
// Update feature states
|
|
171
|
-
const sites = cellDataContext.sites;
|
|
172
|
-
let currentSites: Site[] = [];
|
|
173
|
-
sites.subscribe((s) => {
|
|
174
|
-
currentSites = s;
|
|
175
|
-
})();
|
|
176
|
-
|
|
177
|
-
currentSites.forEach((site) => {
|
|
178
|
-
if (map) {
|
|
179
|
-
map.setFeatureState(
|
|
180
|
-
{
|
|
181
|
-
source: layerManager!.getSourceId(),
|
|
182
|
-
id: site.id
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
selected: site.id === currentSelectedId,
|
|
186
|
-
hovered: site.id === currentHoveredId
|
|
187
|
-
}
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function handleClick(e: mapboxgl.MapLayerMouseEvent): void {
|
|
194
|
-
if (!e.features || e.features.length === 0) return;
|
|
195
|
-
const feature = e.features[0];
|
|
196
|
-
const siteId = feature.properties?.id;
|
|
197
|
-
if (siteId) {
|
|
198
|
-
cellDataContext.selectedSiteId.set(siteId);
|
|
199
|
-
cellDataContext.selectedCellId.set(null);
|
|
200
|
-
|
|
201
|
-
// Show popup if enabled
|
|
202
|
-
if (showPopup && map && e.lngLat) {
|
|
203
|
-
createSitePopup(feature.properties, e.lngLat);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function createSitePopup(siteProps: any, lngLat: mapboxgl.LngLat): void {
|
|
209
|
-
if (!map) return;
|
|
210
|
-
|
|
211
|
-
// Remove existing popup
|
|
212
|
-
if (popup) {
|
|
213
|
-
popup.remove();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Build popup HTML
|
|
217
|
-
const fbands = siteProps.fbands ? JSON.parse(siteProps.fbands) : [];
|
|
218
|
-
const fbandsHtml = fbands.length > 0
|
|
219
|
-
? `<div><strong>Bands:</strong> ${fbands.join(', ')} MHz</div>`
|
|
220
|
-
: '';
|
|
221
|
-
|
|
222
|
-
const html = `
|
|
223
|
-
<div style="font-family: system-ui, -apple-system, sans-serif; font-size: 13px;">
|
|
224
|
-
<h6 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600;">${siteProps.name}</h6>
|
|
225
|
-
<div style="color: #666; line-height: 1.6;">
|
|
226
|
-
<div><strong>ID:</strong> ${siteProps.id}</div>
|
|
227
|
-
<div><strong>Technology:</strong> <span class="badge bg-primary">${siteProps.technology}</span></div>
|
|
228
|
-
${fbandsHtml}
|
|
229
|
-
<div><strong>Provider:</strong> ${siteProps.provider}</div>
|
|
230
|
-
<div><strong>Group:</strong> ${siteProps.featureGroup}</div>
|
|
231
|
-
<div><strong>Cells:</strong> ${siteProps.cellNames ? JSON.parse(siteProps.cellNames).length : 0}</div>
|
|
232
|
-
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee; font-size: 11px;">
|
|
233
|
-
<strong>Coordinates:</strong><br/>
|
|
234
|
-
${siteProps.latitude.toFixed(6)}, ${siteProps.longitude.toFixed(6)}
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
`;
|
|
239
|
-
|
|
240
|
-
// Create and add popup
|
|
241
|
-
popup = new mapboxgl.Popup({
|
|
242
|
-
closeButton: true,
|
|
243
|
-
closeOnClick: false,
|
|
244
|
-
maxWidth: '300px'
|
|
245
|
-
})
|
|
246
|
-
.setLngLat(lngLat)
|
|
247
|
-
.setHTML(html)
|
|
248
|
-
.addTo(map);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function handleMouseEnter(e: mapboxgl.MapLayerMouseEvent): void {
|
|
252
|
-
if (!map) return;
|
|
253
|
-
map.getCanvas().style.cursor = 'pointer';
|
|
254
|
-
if (!e.features || e.features.length === 0) return;
|
|
255
|
-
const feature = e.features[0];
|
|
256
|
-
const siteId = feature.properties?.id;
|
|
257
|
-
if (siteId) {
|
|
258
|
-
cellDataContext.hoveredSiteId.set(siteId);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function handleMouseLeave(): void {
|
|
263
|
-
if (!map) return;
|
|
264
|
-
map.getCanvas().style.cursor = '';
|
|
265
|
-
cellDataContext.hoveredSiteId.set(null);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function cleanup(): void {
|
|
269
|
-
if (popup) {
|
|
270
|
-
popup.remove();
|
|
271
|
-
popup = null;
|
|
272
|
-
}
|
|
273
|
-
if (map && layerManager) {
|
|
274
|
-
if (clickable) {
|
|
275
|
-
map.off('click', layerManager.getLayerId(), handleClick);
|
|
276
|
-
map.off('mouseenter', layerManager.getLayerId(), handleMouseEnter);
|
|
277
|
-
map.off('mouseleave', layerManager.getLayerId(), handleMouseLeave);
|
|
278
|
-
}
|
|
279
|
-
layerManager.cleanup();
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
</script>
|