@smartnet360/svelte-components 0.0.69 → 0.0.71
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/map-v2/demo/DemoMap.svelte +6 -15
- package/dist/map-v2/features/cells/layers/CellLabelsLayer.svelte +111 -49
- package/dist/map-v2/features/cells/layers/CellsLayer.svelte +34 -5
- package/dist/map-v2/features/sites/stores/siteStore.js +8 -1
- package/dist/map-v2/index.d.ts +1 -1
- package/dist/map-v2/index.js +1 -1
- package/dist/map-v2/shared/controls/FeatureSettingsControl.svelte +206 -0
- package/dist/map-v2/shared/controls/FeatureSettingsControl.svelte.d.ts +28 -0
- package/dist/map-v2/shared/controls/MapControl.svelte +1 -1
- package/dist/map-v2/shared/controls/panels/CellSettingsPanel.svelte +342 -0
- package/dist/map-v2/shared/controls/panels/CellSettingsPanel.svelte.d.ts +15 -0
- package/dist/map-v2/shared/controls/panels/RepeaterSettingsPanel.svelte +19 -0
- package/dist/map-v2/shared/controls/panels/RepeaterSettingsPanel.svelte.d.ts +18 -0
- package/dist/map-v2/shared/controls/panels/SiteSettingsPanel.svelte +199 -0
- package/dist/map-v2/shared/controls/panels/SiteSettingsPanel.svelte.d.ts +11 -0
- package/dist/map-v2/shared/index.d.ts +1 -0
- package/dist/map-v2/shared/index.js +1 -0
- package/package.json +1 -1
|
@@ -17,12 +17,11 @@
|
|
|
17
17
|
import ViewportSync from '../core/components/ViewportSync.svelte';
|
|
18
18
|
import SitesLayer from '../features/sites/layers/SitesLayer.svelte';
|
|
19
19
|
import SiteFilterControl from '../features/sites/controls/SiteFilterControl.svelte';
|
|
20
|
-
import SiteSizeSlider from '../features/sites/controls/SiteSizeSlider.svelte';
|
|
21
20
|
import SiteSelectionControl from '../features/sites/controls/SiteSelectionControl.svelte';
|
|
22
21
|
import CellsLayer from '../features/cells/layers/CellsLayer.svelte';
|
|
23
22
|
import CellLabelsLayer from '../features/cells/layers/CellLabelsLayer.svelte';
|
|
24
23
|
import CellFilterControl from '../features/cells/controls/CellFilterControl.svelte';
|
|
25
|
-
import
|
|
24
|
+
import FeatureSettingsControl from '../shared/controls/FeatureSettingsControl.svelte';
|
|
26
25
|
import { createSiteStoreContext } from '../features/sites/stores/siteStoreContext.svelte';
|
|
27
26
|
import { createCellStoreContext } from '../features/cells/stores/cellStoreContext.svelte';
|
|
28
27
|
import { createViewportStore } from '../core/stores/viewportStore.svelte';
|
|
@@ -196,11 +195,12 @@
|
|
|
196
195
|
actionButtonLabel="Open Cluster KPIs"
|
|
197
196
|
/>
|
|
198
197
|
|
|
199
|
-
<!--
|
|
200
|
-
<
|
|
201
|
-
|
|
198
|
+
<!-- Unified feature settings control - Sites, Cells, and Repeaters -->
|
|
199
|
+
<FeatureSettingsControl
|
|
200
|
+
siteStore={siteStore}
|
|
201
|
+
cellStore={cellStore}
|
|
202
202
|
position="top-right"
|
|
203
|
-
title="
|
|
203
|
+
title="Feature Settings"
|
|
204
204
|
initiallyCollapsed={true}
|
|
205
205
|
icon={controlIcons.cellStyle}
|
|
206
206
|
iconOnlyWhenCollapsed={useIconHeaders}
|
|
@@ -208,15 +208,6 @@
|
|
|
208
208
|
labelFieldOptions2G={labelFieldOptions2G}
|
|
209
209
|
/>
|
|
210
210
|
|
|
211
|
-
<!-- Site size control - updates store visual properties -->
|
|
212
|
-
<SiteSizeSlider
|
|
213
|
-
store={siteStore}
|
|
214
|
-
position="top-right"
|
|
215
|
-
title="Site Settings"
|
|
216
|
-
icon={controlIcons.siteSize}
|
|
217
|
-
iconOnlyWhenCollapsed={useIconHeaders}
|
|
218
|
-
/>
|
|
219
|
-
|
|
220
211
|
|
|
221
212
|
</MapboxProvider>
|
|
222
213
|
</div>
|
|
@@ -34,6 +34,23 @@
|
|
|
34
34
|
|
|
35
35
|
let map = $state<mapboxgl.Map | null>(null);
|
|
36
36
|
let unsubscribe: (() => void) | null = null;
|
|
37
|
+
let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
|
+
let viewportVersion = $state(0); // Increment to force $effect re-run on viewport changes
|
|
39
|
+
|
|
40
|
+
// Viewport change handler (pan/zoom/move) with debouncing
|
|
41
|
+
function handleViewportChange() {
|
|
42
|
+
if (!map) return;
|
|
43
|
+
|
|
44
|
+
// Debounce viewport updates to avoid excessive re-renders
|
|
45
|
+
if (viewportUpdateTimer) {
|
|
46
|
+
clearTimeout(viewportUpdateTimer);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Wait 150ms after last pan/zoom before updating
|
|
50
|
+
viewportUpdateTimer = setTimeout(() => {
|
|
51
|
+
viewportVersion++;
|
|
52
|
+
}, 150);
|
|
53
|
+
}
|
|
37
54
|
|
|
38
55
|
/**
|
|
39
56
|
* Get label text for a cell based on tech and selected field
|
|
@@ -84,8 +101,18 @@
|
|
|
84
101
|
// Get current zoom level
|
|
85
102
|
const currentZoom = map.getZoom();
|
|
86
103
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
104
|
+
// Filter cells by viewport bounds (only render labels for visible cells)
|
|
105
|
+
const bounds = map.getBounds();
|
|
106
|
+
if (!bounds) {
|
|
107
|
+
return { type: 'FeatureCollection', features: [] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const visibleCells = store.filteredCells.filter(cell =>
|
|
111
|
+
bounds.contains([cell.longitude, cell.latitude])
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Group visible cells by site + azimuth
|
|
115
|
+
const cellGroups = groupCellsByAzimuth(visibleCells, store.azimuthTolerance);
|
|
89
116
|
|
|
90
117
|
cellGroups.forEach((cells, groupKey) => {
|
|
91
118
|
// Sort cells within group for consistent stacking
|
|
@@ -123,8 +150,9 @@
|
|
|
123
150
|
// labelOffset is treated as a percentage (e.g., 300 = 300% = beyond arc, 70 = 70% = inside arc)
|
|
124
151
|
const labelDistance = (cellRadius * store.labelOffset) / 100;
|
|
125
152
|
|
|
126
|
-
// Project label position along azimuth
|
|
127
|
-
|
|
153
|
+
// Project label position along azimuth from the CELL'S location (not site)
|
|
154
|
+
// This allows labels to follow cells that may be offset from the site center
|
|
155
|
+
const origin = turf.point([cell.longitude, cell.latitude]);
|
|
128
156
|
const labelPosition = turf.destination(
|
|
129
157
|
origin,
|
|
130
158
|
labelDistance / 1000, // Convert meters to kilometers
|
|
@@ -171,6 +199,66 @@
|
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Initialize or reinitialize the label layer
|
|
204
|
+
* Called on mount and when map style changes
|
|
205
|
+
*/
|
|
206
|
+
async function initializeLayer() {
|
|
207
|
+
if (!map) return;
|
|
208
|
+
|
|
209
|
+
// Wait for style to load
|
|
210
|
+
await waitForStyleLoad(map);
|
|
211
|
+
|
|
212
|
+
// Add source if missing
|
|
213
|
+
if (!map.getSource(sourceId)) {
|
|
214
|
+
map.addSource(sourceId, {
|
|
215
|
+
type: 'geojson',
|
|
216
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Add label layer if missing
|
|
221
|
+
if (!map.getLayer(layerId)) {
|
|
222
|
+
map.addLayer({
|
|
223
|
+
id: layerId,
|
|
224
|
+
type: 'symbol',
|
|
225
|
+
source: sourceId,
|
|
226
|
+
minzoom: store.minLabelZoom,
|
|
227
|
+
layout: {
|
|
228
|
+
'text-field': ['get', 'text'],
|
|
229
|
+
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
|
|
230
|
+
'text-size': store.labelSize,
|
|
231
|
+
'text-anchor': 'center',
|
|
232
|
+
'text-offset': [
|
|
233
|
+
0,
|
|
234
|
+
['/', ['get', 'stackOffset'], store.labelSize]
|
|
235
|
+
],
|
|
236
|
+
'text-allow-overlap': true,
|
|
237
|
+
'text-ignore-placement': false,
|
|
238
|
+
'text-max-width': 999,
|
|
239
|
+
'text-justify': 'center'
|
|
240
|
+
},
|
|
241
|
+
paint: {
|
|
242
|
+
'text-color': store.labelColor,
|
|
243
|
+
'text-halo-color': store.labelHaloColor,
|
|
244
|
+
'text-halo-width': store.labelHaloWidth
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add event listeners (remove first to avoid duplicates)
|
|
250
|
+
map.off('zoom', updateLabels);
|
|
251
|
+
map.off('moveend', handleViewportChange);
|
|
252
|
+
map.off('zoomend', handleViewportChange);
|
|
253
|
+
|
|
254
|
+
map.on('zoom', updateLabels);
|
|
255
|
+
map.on('moveend', handleViewportChange);
|
|
256
|
+
map.on('zoomend', handleViewportChange);
|
|
257
|
+
|
|
258
|
+
// Initial render
|
|
259
|
+
updateLabels();
|
|
260
|
+
}
|
|
261
|
+
|
|
174
262
|
onMount(async () => {
|
|
175
263
|
unsubscribe = mapStore.subscribe(async (m) => {
|
|
176
264
|
if (!m) {
|
|
@@ -180,56 +268,20 @@
|
|
|
180
268
|
|
|
181
269
|
map = m;
|
|
182
270
|
|
|
183
|
-
//
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
// Add source
|
|
187
|
-
if (!map.getSource(sourceId)) {
|
|
188
|
-
map.addSource(sourceId, {
|
|
189
|
-
type: 'geojson',
|
|
190
|
-
data: { type: 'FeatureCollection', features: [] }
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Add label layer
|
|
195
|
-
if (!map.getLayer(layerId)) {
|
|
196
|
-
map.addLayer({
|
|
197
|
-
id: layerId,
|
|
198
|
-
type: 'symbol',
|
|
199
|
-
source: sourceId,
|
|
200
|
-
minzoom: store.minLabelZoom,
|
|
201
|
-
layout: {
|
|
202
|
-
'text-field': ['get', 'text'],
|
|
203
|
-
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
|
|
204
|
-
'text-size': store.labelSize,
|
|
205
|
-
'text-anchor': 'center', // Center horizontally only
|
|
206
|
-
'text-offset': [
|
|
207
|
-
0,
|
|
208
|
-
['/', ['get', 'stackOffset'], store.labelSize] // Offset includes centering adjustment
|
|
209
|
-
],
|
|
210
|
-
'text-allow-overlap': true,
|
|
211
|
-
'text-ignore-placement': false,
|
|
212
|
-
'text-max-width': 999, // Essentially disable wrapping
|
|
213
|
-
'text-justify': 'center' // Center horizontally
|
|
214
|
-
},
|
|
215
|
-
paint: {
|
|
216
|
-
'text-color': store.labelColor,
|
|
217
|
-
'text-halo-color': store.labelHaloColor,
|
|
218
|
-
'text-halo-width': store.labelHaloWidth
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Initial render
|
|
224
|
-
updateLabels();
|
|
271
|
+
// Initial layer setup
|
|
272
|
+
await initializeLayer();
|
|
225
273
|
|
|
226
|
-
//
|
|
227
|
-
|
|
274
|
+
// Re-initialize layer when map style changes
|
|
275
|
+
// Mapbox removes all custom layers/sources when setStyle() is called
|
|
276
|
+
map.on('style.load', initializeLayer);
|
|
228
277
|
});
|
|
229
278
|
});
|
|
230
279
|
|
|
231
280
|
// Watch for changes in store properties and update labels
|
|
232
281
|
$effect(() => {
|
|
282
|
+
// Track viewportVersion to force re-run on pan (even without zoom change)
|
|
283
|
+
viewportVersion;
|
|
284
|
+
|
|
233
285
|
// Dependencies that should trigger label refresh
|
|
234
286
|
store.filteredCells;
|
|
235
287
|
store.showLabels;
|
|
@@ -255,9 +307,19 @@
|
|
|
255
307
|
onDestroy(() => {
|
|
256
308
|
unsubscribe?.();
|
|
257
309
|
|
|
310
|
+
// Clear any pending viewport update timer
|
|
311
|
+
if (viewportUpdateTimer) {
|
|
312
|
+
clearTimeout(viewportUpdateTimer);
|
|
313
|
+
}
|
|
314
|
+
|
|
258
315
|
if (map) {
|
|
259
|
-
// Remove
|
|
316
|
+
// Remove style.load listener
|
|
317
|
+
map.off('style.load', initializeLayer);
|
|
318
|
+
|
|
319
|
+
// Remove all event listeners
|
|
260
320
|
map.off('zoom', updateLabels);
|
|
321
|
+
map.off('moveend', handleViewportChange);
|
|
322
|
+
map.off('zoomend', handleViewportChange);
|
|
261
323
|
|
|
262
324
|
if (map.getLayer(layerId)) {
|
|
263
325
|
map.removeLayer(layerId);
|
|
@@ -58,6 +58,32 @@
|
|
|
58
58
|
}, 200);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Initialize or reinitialize the cell layers
|
|
63
|
+
* Called on mount and when map style changes
|
|
64
|
+
*/
|
|
65
|
+
function initializeLayer() {
|
|
66
|
+
if (!map) return;
|
|
67
|
+
|
|
68
|
+
console.log('CellsLayer: initializeLayer called');
|
|
69
|
+
|
|
70
|
+
// Set initial zoom
|
|
71
|
+
store.setCurrentZoom(map.getZoom());
|
|
72
|
+
|
|
73
|
+
// Add moveend listener if not already added
|
|
74
|
+
// Note: We need to be careful not to duplicate listeners
|
|
75
|
+
map.off('moveend', handleMoveEnd); // Remove if exists
|
|
76
|
+
map.on('moveend', handleMoveEnd); // Add it back
|
|
77
|
+
|
|
78
|
+
// Mark as mounted to trigger $effect
|
|
79
|
+
mounted = true;
|
|
80
|
+
|
|
81
|
+
// Force $effect to re-run by incrementing viewportVersion
|
|
82
|
+
// This is critical after style.load when layers need to be recreated
|
|
83
|
+
// but mounted/map haven't changed (they're already true/set)
|
|
84
|
+
viewportVersion++;
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
onMount(() => {
|
|
62
88
|
console.log('CellsLayer: onMount, waiting for map...');
|
|
63
89
|
|
|
@@ -66,13 +92,13 @@
|
|
|
66
92
|
if (mapInstance && !map) {
|
|
67
93
|
console.log('CellsLayer: Map available, initializing...');
|
|
68
94
|
map = mapInstance;
|
|
69
|
-
mounted = true;
|
|
70
95
|
|
|
71
|
-
//
|
|
72
|
-
|
|
96
|
+
// Initial layer setup
|
|
97
|
+
initializeLayer();
|
|
73
98
|
|
|
74
|
-
//
|
|
75
|
-
|
|
99
|
+
// Re-initialize layer when map style changes
|
|
100
|
+
// Mapbox removes all custom layers/sources when setStyle() is called
|
|
101
|
+
map.on('style.load', initializeLayer);
|
|
76
102
|
}
|
|
77
103
|
});
|
|
78
104
|
|
|
@@ -89,6 +115,9 @@
|
|
|
89
115
|
clearTimeout(viewportUpdateTimer);
|
|
90
116
|
}
|
|
91
117
|
|
|
118
|
+
// Remove style.load listener
|
|
119
|
+
map.off('style.load', initializeLayer);
|
|
120
|
+
|
|
92
121
|
map.off('moveend', handleMoveEnd);
|
|
93
122
|
|
|
94
123
|
// Clean up layers and source
|
|
@@ -15,7 +15,14 @@ export function createSiteStore(initialSites = []) {
|
|
|
15
15
|
showLabels: false,
|
|
16
16
|
showOnHover: true,
|
|
17
17
|
strokeWidth: 2,
|
|
18
|
-
strokeColor: '#ffffff'
|
|
18
|
+
strokeColor: '#ffffff',
|
|
19
|
+
labelSize: 12,
|
|
20
|
+
labelColor: '#000000',
|
|
21
|
+
labelOffset: 0,
|
|
22
|
+
labelProperty: 'name',
|
|
23
|
+
groupColorMap: new Map(),
|
|
24
|
+
selectionMode: false,
|
|
25
|
+
selectedSiteIds: new Set()
|
|
19
26
|
};
|
|
20
27
|
const { subscribe, update, set } = writable(defaultState);
|
|
21
28
|
return {
|
package/dist/map-v2/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Each feature (sites, cells) is completely independent with its own store, layers, and controls.
|
|
6
6
|
*/
|
|
7
7
|
export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyleControl, createMapStore, createViewportStore, type ViewportStore, type ViewportState, useMapbox, tryUseMapbox } from './core';
|
|
8
|
-
export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
|
|
8
|
+
export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
|
|
9
9
|
export { type Site, type SiteStoreValue, type SiteStoreContext, createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSelectionControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
|
|
10
10
|
export { type Cell, type CellStatus, type CellStatusStyle, type CellGroupingField, type CellGroupingLabels, type CellTreeConfig, type TechnologyBandKey, type CellStoreValue, type CellStoreContext, createCellStoreContext, CellsLayer, CellLabelsLayer, CellFilterControl, CellStyleControl, cellsToGeoJSON, buildCellTree, getFilteredCells, calculateRadius, getZoomFactor, createArcPolygon, DEFAULT_CELL_TREE_CONFIG, TECHNOLOGY_BAND_COLORS, DEFAULT_STATUS_STYLES, RADIUS_MULTIPLIER } from './features/cells';
|
|
11
11
|
export { DemoMap, demoSites, demoCells } from './demo';
|
package/dist/map-v2/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyle
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// SHARED UTILITIES
|
|
13
13
|
// ============================================================================
|
|
14
|
-
export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
|
|
14
|
+
export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
|
|
15
15
|
// ============================================================================
|
|
16
16
|
// SITE FEATURE
|
|
17
17
|
// ============================================================================
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FeatureSettingsControl - Unified settings control for Sites, Cells, and Repeaters
|
|
4
|
+
*
|
|
5
|
+
* Uses Bootstrap accordion to organize settings by feature type.
|
|
6
|
+
* Consolidates SiteSizeSlider and CellStyleControl into a single UI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import MapControl from './MapControl.svelte';
|
|
10
|
+
import SiteSettingsPanel from './panels/SiteSettingsPanel.svelte';
|
|
11
|
+
import CellSettingsPanel from './panels/CellSettingsPanel.svelte';
|
|
12
|
+
import RepeaterSettingsPanel from './panels/RepeaterSettingsPanel.svelte';
|
|
13
|
+
import type { SiteStoreContext } from '../../features/sites/stores/siteStoreContext.svelte';
|
|
14
|
+
import type { CellStoreContext } from '../../features/cells/stores/cellStoreContext.svelte';
|
|
15
|
+
import type { Cell, CellGroupingLabels } from '../../features/cells/types';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Site store context */
|
|
19
|
+
siteStore: SiteStoreContext;
|
|
20
|
+
/** Cell store context */
|
|
21
|
+
cellStore: CellStoreContext;
|
|
22
|
+
/** Available label field options for 4G/5G cells */
|
|
23
|
+
labelFieldOptions4G5G: Array<keyof Cell>;
|
|
24
|
+
/** Available label field options for 2G cells */
|
|
25
|
+
labelFieldOptions2G: Array<keyof Cell>;
|
|
26
|
+
/** Optional label map for human-readable field names */
|
|
27
|
+
fieldLabels?: CellGroupingLabels;
|
|
28
|
+
/** Control position */
|
|
29
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
30
|
+
/** Control title */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** Optional header icon */
|
|
33
|
+
icon?: string;
|
|
34
|
+
/** Show icon when collapsed (default: true) */
|
|
35
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
36
|
+
/** Initially collapsed? */
|
|
37
|
+
initiallyCollapsed?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
siteStore,
|
|
42
|
+
cellStore,
|
|
43
|
+
labelFieldOptions4G5G,
|
|
44
|
+
labelFieldOptions2G,
|
|
45
|
+
fieldLabels,
|
|
46
|
+
position = 'top-right',
|
|
47
|
+
title = 'Feature Settings',
|
|
48
|
+
icon = 'sliders',
|
|
49
|
+
iconOnlyWhenCollapsed = true,
|
|
50
|
+
initiallyCollapsed = true
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
// Generate unique IDs for accordion items
|
|
54
|
+
const accordionId = `settings-accordion-${Math.random().toString(36).substring(7)}`;
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
|
|
58
|
+
<div class="accordion" id={accordionId}>
|
|
59
|
+
<!-- Site Settings Accordion Item -->
|
|
60
|
+
<div class="accordion-item">
|
|
61
|
+
<h2 class="accordion-header" id="heading-sites">
|
|
62
|
+
<button
|
|
63
|
+
class="accordion-button collapsed"
|
|
64
|
+
type="button"
|
|
65
|
+
data-bs-toggle="collapse"
|
|
66
|
+
data-bs-target="#collapse-sites"
|
|
67
|
+
aria-expanded="false"
|
|
68
|
+
aria-controls="collapse-sites"
|
|
69
|
+
>
|
|
70
|
+
Site Settings
|
|
71
|
+
</button>
|
|
72
|
+
</h2>
|
|
73
|
+
<div
|
|
74
|
+
id="collapse-sites"
|
|
75
|
+
class="accordion-collapse collapse"
|
|
76
|
+
aria-labelledby="heading-sites"
|
|
77
|
+
>
|
|
78
|
+
<div class="accordion-body">
|
|
79
|
+
<SiteSettingsPanel store={siteStore} />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Cell Settings Accordion Item -->
|
|
85
|
+
<div class="accordion-item">
|
|
86
|
+
<h2 class="accordion-header" id="heading-cells">
|
|
87
|
+
<button
|
|
88
|
+
class="accordion-button collapsed"
|
|
89
|
+
type="button"
|
|
90
|
+
data-bs-toggle="collapse"
|
|
91
|
+
data-bs-target="#collapse-cells"
|
|
92
|
+
aria-expanded="false"
|
|
93
|
+
aria-controls="collapse-cells"
|
|
94
|
+
>
|
|
95
|
+
Cell Settings
|
|
96
|
+
</button>
|
|
97
|
+
</h2>
|
|
98
|
+
<div
|
|
99
|
+
id="collapse-cells"
|
|
100
|
+
class="accordion-collapse collapse"
|
|
101
|
+
aria-labelledby="heading-cells"
|
|
102
|
+
>
|
|
103
|
+
<div class="accordion-body">
|
|
104
|
+
<CellSettingsPanel
|
|
105
|
+
store={cellStore}
|
|
106
|
+
{labelFieldOptions4G5G}
|
|
107
|
+
{labelFieldOptions2G}
|
|
108
|
+
{fieldLabels}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Repeater Settings Accordion Item (Placeholder) -->
|
|
115
|
+
<div class="accordion-item">
|
|
116
|
+
<h2 class="accordion-header" id="heading-repeaters">
|
|
117
|
+
<button
|
|
118
|
+
class="accordion-button collapsed"
|
|
119
|
+
type="button"
|
|
120
|
+
data-bs-toggle="collapse"
|
|
121
|
+
data-bs-target="#collapse-repeaters"
|
|
122
|
+
aria-expanded="false"
|
|
123
|
+
aria-controls="collapse-repeaters"
|
|
124
|
+
>
|
|
125
|
+
Repeater Settings
|
|
126
|
+
</button>
|
|
127
|
+
</h2>
|
|
128
|
+
<div
|
|
129
|
+
id="collapse-repeaters"
|
|
130
|
+
class="accordion-collapse collapse"
|
|
131
|
+
aria-labelledby="heading-repeaters"
|
|
132
|
+
>
|
|
133
|
+
<div class="accordion-body">
|
|
134
|
+
<RepeaterSettingsPanel />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</MapControl>
|
|
140
|
+
|
|
141
|
+
<style>
|
|
142
|
+
.accordion {
|
|
143
|
+
--bs-accordion-border-width: 0;
|
|
144
|
+
/* Fixed width to prevent resizing when sections expand/collapse */
|
|
145
|
+
width: 320px;
|
|
146
|
+
min-width: 320px;
|
|
147
|
+
max-width: 320px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.accordion-item {
|
|
151
|
+
background-color: transparent;
|
|
152
|
+
border: none;
|
|
153
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.accordion-item:last-child {
|
|
157
|
+
border-bottom: none;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.accordion-button {
|
|
161
|
+
font-size: 0.875rem;
|
|
162
|
+
font-weight: 600;
|
|
163
|
+
padding: 0.75rem 1rem;
|
|
164
|
+
background-color: transparent;
|
|
165
|
+
color: var(--bs-body-color);
|
|
166
|
+
/* Ensure chevron is visible and properly positioned */
|
|
167
|
+
position: relative;
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
width: 100%;
|
|
171
|
+
text-align: left;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Preserve Bootstrap's chevron */
|
|
175
|
+
.accordion-button::after {
|
|
176
|
+
flex-shrink: 0;
|
|
177
|
+
width: 1.25rem;
|
|
178
|
+
height: 1.25rem;
|
|
179
|
+
margin-left: auto;
|
|
180
|
+
content: "";
|
|
181
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
|
182
|
+
background-repeat: no-repeat;
|
|
183
|
+
background-size: 1.25rem;
|
|
184
|
+
transition: transform 0.2s ease-in-out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Rotate chevron when expanded */
|
|
188
|
+
.accordion-button:not(.collapsed)::after {
|
|
189
|
+
transform: rotate(-180deg);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.accordion-button:not(.collapsed) {
|
|
193
|
+
background-color: rgba(13, 110, 253, 0.05);
|
|
194
|
+
color: #0d6efd;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.accordion-button:focus {
|
|
198
|
+
box-shadow: none;
|
|
199
|
+
border-color: rgba(0, 0, 0, 0.125);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.accordion-body {
|
|
203
|
+
padding: 1rem;
|
|
204
|
+
padding-top: 0.5rem;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SiteStoreContext } from '../../features/sites/stores/siteStoreContext.svelte';
|
|
2
|
+
import type { CellStoreContext } from '../../features/cells/stores/cellStoreContext.svelte';
|
|
3
|
+
import type { Cell, CellGroupingLabels } from '../../features/cells/types';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Site store context */
|
|
6
|
+
siteStore: SiteStoreContext;
|
|
7
|
+
/** Cell store context */
|
|
8
|
+
cellStore: CellStoreContext;
|
|
9
|
+
/** Available label field options for 4G/5G cells */
|
|
10
|
+
labelFieldOptions4G5G: Array<keyof Cell>;
|
|
11
|
+
/** Available label field options for 2G cells */
|
|
12
|
+
labelFieldOptions2G: Array<keyof Cell>;
|
|
13
|
+
/** Optional label map for human-readable field names */
|
|
14
|
+
fieldLabels?: CellGroupingLabels;
|
|
15
|
+
/** Control position */
|
|
16
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
17
|
+
/** Control title */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Optional header icon */
|
|
20
|
+
icon?: string;
|
|
21
|
+
/** Show icon when collapsed (default: true) */
|
|
22
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
23
|
+
/** Initially collapsed? */
|
|
24
|
+
initiallyCollapsed?: boolean;
|
|
25
|
+
}
|
|
26
|
+
declare const FeatureSettingsControl: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type FeatureSettingsControl = ReturnType<typeof FeatureSettingsControl>;
|
|
28
|
+
export default FeatureSettingsControl;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CellSettingsPanel - Cell visualization and label settings
|
|
4
|
+
* Reusable panel component for cell display configuration
|
|
5
|
+
*/
|
|
6
|
+
import type { CellStoreContext } from '../../../features/cells/stores/cellStoreContext.svelte';
|
|
7
|
+
import type { Cell, CellGroupingLabels } from '../../../features/cells/types';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
store: CellStoreContext;
|
|
11
|
+
labelFieldOptions4G5G: Array<keyof Cell>;
|
|
12
|
+
labelFieldOptions2G: Array<keyof Cell>;
|
|
13
|
+
fieldLabels?: CellGroupingLabels;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { store, labelFieldOptions4G5G, labelFieldOptions2G, fieldLabels }: Props = $props();
|
|
17
|
+
|
|
18
|
+
// Get label for a field from the fieldLabels map or fallback to field name
|
|
19
|
+
function getFieldLabel(field: keyof Cell | 'none'): string {
|
|
20
|
+
if (field === 'none') return 'None';
|
|
21
|
+
return fieldLabels?.[field] || String(field);
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div class="settings-panel">
|
|
26
|
+
|
|
27
|
+
<!-- Base radius slider -->
|
|
28
|
+
<div class="control-row">
|
|
29
|
+
<label for="cell-radius-slider" class="control-label">
|
|
30
|
+
Base Radius: <strong>{store.baseRadius}m</strong>
|
|
31
|
+
</label>
|
|
32
|
+
<input
|
|
33
|
+
id="cell-radius-slider"
|
|
34
|
+
type="range"
|
|
35
|
+
class="form-range"
|
|
36
|
+
min="100"
|
|
37
|
+
max="2000"
|
|
38
|
+
step="50"
|
|
39
|
+
bind:value={store.baseRadius}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Line width slider -->
|
|
44
|
+
<div class="control-row">
|
|
45
|
+
<label for="cell-line-width-slider" class="control-label">
|
|
46
|
+
Line Width: <strong>{store.lineWidth}px</strong>
|
|
47
|
+
</label>
|
|
48
|
+
<input
|
|
49
|
+
id="cell-line-width-slider"
|
|
50
|
+
type="range"
|
|
51
|
+
class="form-range"
|
|
52
|
+
min="1"
|
|
53
|
+
max="5"
|
|
54
|
+
step="0.5"
|
|
55
|
+
bind:value={store.lineWidth}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Fill opacity slider -->
|
|
60
|
+
<div class="control-row">
|
|
61
|
+
<label for="cell-opacity-slider" class="control-label">
|
|
62
|
+
Fill Opacity: <strong>{Math.round(store.fillOpacity * 100)}%</strong>
|
|
63
|
+
</label>
|
|
64
|
+
<input
|
|
65
|
+
id="cell-opacity-slider"
|
|
66
|
+
type="range"
|
|
67
|
+
class="form-range"
|
|
68
|
+
min="0"
|
|
69
|
+
max="1"
|
|
70
|
+
step="0.1"
|
|
71
|
+
bind:value={store.fillOpacity}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Cell Labels Section -->
|
|
76
|
+
<hr class="section-divider" />
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
<!-- Show labels toggle -->
|
|
80
|
+
<div class="control-row">
|
|
81
|
+
<label for="cell-labels-toggle" class="control-label">
|
|
82
|
+
Show Cell Labels
|
|
83
|
+
</label>
|
|
84
|
+
<div class="form-check form-switch">
|
|
85
|
+
<input
|
|
86
|
+
id="cell-labels-toggle"
|
|
87
|
+
type="checkbox"
|
|
88
|
+
class="form-check-input"
|
|
89
|
+
role="switch"
|
|
90
|
+
checked={store.showLabels}
|
|
91
|
+
onchange={(e) => store.setShowLabels(e.currentTarget.checked)}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{#if store.showLabels}
|
|
97
|
+
<!-- 4G/5G Primary Label Field -->
|
|
98
|
+
<div class="control-row">
|
|
99
|
+
<label for="label-primary-4g5g" class="control-label">
|
|
100
|
+
4G/5G Primary
|
|
101
|
+
</label>
|
|
102
|
+
<select
|
|
103
|
+
id="label-primary-4g5g"
|
|
104
|
+
class="form-select form-select-sm"
|
|
105
|
+
value={store.primaryLabelField4G5G}
|
|
106
|
+
onchange={(e) => store.setPrimaryLabelField4G5G(e.currentTarget.value as keyof Cell)}
|
|
107
|
+
>
|
|
108
|
+
{#each labelFieldOptions4G5G as field}
|
|
109
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</select>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- 4G/5G Secondary Label Field -->
|
|
115
|
+
<div class="control-row">
|
|
116
|
+
<label for="label-secondary-4g5g" class="control-label">
|
|
117
|
+
4G/5G Secondary
|
|
118
|
+
</label>
|
|
119
|
+
<select
|
|
120
|
+
id="label-secondary-4g5g"
|
|
121
|
+
class="form-select form-select-sm"
|
|
122
|
+
value={store.secondaryLabelField4G5G}
|
|
123
|
+
onchange={(e) => store.setSecondaryLabelField4G5G(e.currentTarget.value as keyof Cell | 'none')}
|
|
124
|
+
>
|
|
125
|
+
<option value="none">None</option>
|
|
126
|
+
{#each labelFieldOptions4G5G as field}
|
|
127
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
128
|
+
{/each}
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- 2G Primary Label Field -->
|
|
133
|
+
<div class="control-row">
|
|
134
|
+
<label for="label-primary-2g" class="control-label">
|
|
135
|
+
2G Primary
|
|
136
|
+
</label>
|
|
137
|
+
<select
|
|
138
|
+
id="label-primary-2g"
|
|
139
|
+
class="form-select form-select-sm"
|
|
140
|
+
value={store.primaryLabelField2G}
|
|
141
|
+
onchange={(e) => store.setPrimaryLabelField2G(e.currentTarget.value as keyof Cell)}
|
|
142
|
+
>
|
|
143
|
+
{#each labelFieldOptions2G as field}
|
|
144
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
145
|
+
{/each}
|
|
146
|
+
</select>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- 2G Secondary Label Field -->
|
|
150
|
+
<div class="control-row">
|
|
151
|
+
<label for="label-secondary-2g" class="control-label">
|
|
152
|
+
2G Secondary
|
|
153
|
+
</label>
|
|
154
|
+
<select
|
|
155
|
+
id="label-secondary-2g"
|
|
156
|
+
class="form-select form-select-sm"
|
|
157
|
+
value={store.secondaryLabelField2G}
|
|
158
|
+
onchange={(e) => store.setSecondaryLabelField2G(e.currentTarget.value as keyof Cell | 'none')}
|
|
159
|
+
>
|
|
160
|
+
<option value="none">None</option>
|
|
161
|
+
{#each labelFieldOptions2G as field}
|
|
162
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
163
|
+
{/each}
|
|
164
|
+
</select>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Label Size -->
|
|
168
|
+
<div class="control-row">
|
|
169
|
+
<label for="label-size-slider" class="control-label">
|
|
170
|
+
Label Size: <strong>{store.labelSize}px</strong>
|
|
171
|
+
</label>
|
|
172
|
+
<input
|
|
173
|
+
id="label-size-slider"
|
|
174
|
+
type="range"
|
|
175
|
+
class="form-range"
|
|
176
|
+
min="8"
|
|
177
|
+
max="20"
|
|
178
|
+
step="1"
|
|
179
|
+
value={store.labelSize}
|
|
180
|
+
oninput={(e) => store.setLabelSize(Number(e.currentTarget.value))}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Label Color -->
|
|
185
|
+
<div class="control-row">
|
|
186
|
+
<label for="label-color-picker" class="control-label">
|
|
187
|
+
Label Color
|
|
188
|
+
</label>
|
|
189
|
+
<input
|
|
190
|
+
id="label-color-picker"
|
|
191
|
+
type="color"
|
|
192
|
+
class="form-control form-control-color"
|
|
193
|
+
value={store.labelColor}
|
|
194
|
+
oninput={(e) => store.setLabelColor(e.currentTarget.value)}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Label Offset -->
|
|
199
|
+
<div class="control-row">
|
|
200
|
+
<label for="label-offset-slider" class="control-label">
|
|
201
|
+
Label Offset: <strong>{store.labelOffset}%</strong>
|
|
202
|
+
</label>
|
|
203
|
+
<input
|
|
204
|
+
id="label-offset-slider"
|
|
205
|
+
type="range"
|
|
206
|
+
class="form-range"
|
|
207
|
+
min="100"
|
|
208
|
+
max="1000"
|
|
209
|
+
step="50"
|
|
210
|
+
value={store.labelOffset}
|
|
211
|
+
oninput={(e) => store.setLabelOffset(Number(e.currentTarget.value))}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- Label Halo Color -->
|
|
216
|
+
<div class="control-row">
|
|
217
|
+
<label for="label-halo-color-picker" class="control-label">
|
|
218
|
+
Halo Color
|
|
219
|
+
</label>
|
|
220
|
+
<input
|
|
221
|
+
id="label-halo-color-picker"
|
|
222
|
+
type="color"
|
|
223
|
+
class="form-control form-control-color"
|
|
224
|
+
value={store.labelHaloColor}
|
|
225
|
+
oninput={(e) => store.setLabelHaloColor(e.currentTarget.value)}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<!-- Label Halo Width -->
|
|
230
|
+
<div class="control-row">
|
|
231
|
+
<label for="label-halo-width-slider" class="control-label">
|
|
232
|
+
Halo Width: <strong>{store.labelHaloWidth}px</strong>
|
|
233
|
+
</label>
|
|
234
|
+
<input
|
|
235
|
+
id="label-halo-width-slider"
|
|
236
|
+
type="range"
|
|
237
|
+
class="form-range"
|
|
238
|
+
min="0"
|
|
239
|
+
max="3"
|
|
240
|
+
step="0.5"
|
|
241
|
+
value={store.labelHaloWidth}
|
|
242
|
+
oninput={(e) => store.setLabelHaloWidth(Number(e.currentTarget.value))}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- Min Label Zoom -->
|
|
247
|
+
<div class="control-row">
|
|
248
|
+
<label for="min-label-zoom-slider" class="control-label">
|
|
249
|
+
Min Zoom: <strong>{store.minLabelZoom}</strong>
|
|
250
|
+
</label>
|
|
251
|
+
<input
|
|
252
|
+
id="min-label-zoom-slider"
|
|
253
|
+
type="range"
|
|
254
|
+
class="form-range"
|
|
255
|
+
min="10"
|
|
256
|
+
max="18"
|
|
257
|
+
step="1"
|
|
258
|
+
value={store.minLabelZoom}
|
|
259
|
+
oninput={(e) => store.setMinLabelZoom(Number(e.currentTarget.value))}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<!-- Azimuth Tolerance -->
|
|
264
|
+
<!-- <div class="control-row">
|
|
265
|
+
<label for="azimuth-tolerance-slider" class="control-label">
|
|
266
|
+
Azimuth Tolerance: <strong>±{store.azimuthTolerance}°</strong>
|
|
267
|
+
</label>
|
|
268
|
+
<input
|
|
269
|
+
id="azimuth-tolerance-slider"
|
|
270
|
+
type="range"
|
|
271
|
+
class="form-range"
|
|
272
|
+
min="1"
|
|
273
|
+
max="45"
|
|
274
|
+
step="1"
|
|
275
|
+
value={store.azimuthTolerance}
|
|
276
|
+
oninput={(e) => store.setAzimuthTolerance(Number(e.currentTarget.value))}
|
|
277
|
+
/>
|
|
278
|
+
</div> -->
|
|
279
|
+
{/if}
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<style>
|
|
283
|
+
.settings-panel {
|
|
284
|
+
width: 100%;
|
|
285
|
+
padding: 0.75rem;
|
|
286
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
287
|
+
border-radius: 0.375rem;
|
|
288
|
+
background-color: rgba(255, 255, 255, 0.02);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.section-divider {
|
|
292
|
+
margin: 1rem 0;
|
|
293
|
+
opacity: 0.3;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.control-row {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
justify-content: space-between;
|
|
300
|
+
gap: 0.75rem;
|
|
301
|
+
margin-bottom: 0.875rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.control-row:last-child {
|
|
305
|
+
margin-bottom: 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.control-label {
|
|
309
|
+
font-size: 0.8125rem;
|
|
310
|
+
font-weight: 500;
|
|
311
|
+
margin: 0;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
flex-shrink: 0;
|
|
314
|
+
min-width: 110px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.form-range,
|
|
318
|
+
.form-select {
|
|
319
|
+
width: 140px;
|
|
320
|
+
min-width: 140px;
|
|
321
|
+
max-width: 140px;
|
|
322
|
+
flex: 0 0 140px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.form-control-color {
|
|
326
|
+
width: 45px;
|
|
327
|
+
height: 28px;
|
|
328
|
+
padding: 2px;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
flex-shrink: 0;
|
|
331
|
+
margin-right: auto;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.form-check {
|
|
335
|
+
margin: 0;
|
|
336
|
+
margin-right: auto;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.form-switch .form-check-input {
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
}
|
|
342
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CellSettingsPanel - Cell visualization and label settings
|
|
3
|
+
* Reusable panel component for cell display configuration
|
|
4
|
+
*/
|
|
5
|
+
import type { CellStoreContext } from '../../../features/cells/stores/cellStoreContext.svelte';
|
|
6
|
+
import type { Cell, CellGroupingLabels } from '../../../features/cells/types';
|
|
7
|
+
interface Props {
|
|
8
|
+
store: CellStoreContext;
|
|
9
|
+
labelFieldOptions4G5G: Array<keyof Cell>;
|
|
10
|
+
labelFieldOptions2G: Array<keyof Cell>;
|
|
11
|
+
fieldLabels?: CellGroupingLabels;
|
|
12
|
+
}
|
|
13
|
+
declare const CellSettingsPanel: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type CellSettingsPanel = ReturnType<typeof CellSettingsPanel>;
|
|
15
|
+
export default CellSettingsPanel;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* RepeaterSettingsPanel - Repeater visualization settings
|
|
4
|
+
* Placeholder component for future repeater functionality
|
|
5
|
+
*/
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div class="settings-panel">
|
|
9
|
+
<p class="text-muted text-center mb-0" style="font-size: 0.875rem;">
|
|
10
|
+
Coming soon...
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.settings-panel {
|
|
16
|
+
width: 100%;
|
|
17
|
+
padding: 1rem 0;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const RepeaterSettingsPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type RepeaterSettingsPanel = InstanceType<typeof RepeaterSettingsPanel>;
|
|
18
|
+
export default RepeaterSettingsPanel;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SiteSettingsPanel - Site visualization settings
|
|
4
|
+
* Reusable panel component for site display configuration
|
|
5
|
+
*/
|
|
6
|
+
import type { SiteStoreContext } from '../../../features/sites/stores/siteStoreContext.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
store: SiteStoreContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { store }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="settings-panel">
|
|
16
|
+
<!-- Site Size slider -->
|
|
17
|
+
<div class="control-row">
|
|
18
|
+
<label for="site-size-slider" class="control-label">
|
|
19
|
+
Radius: <strong>{store.size}px</strong>
|
|
20
|
+
</label>
|
|
21
|
+
<input
|
|
22
|
+
id="site-size-slider"
|
|
23
|
+
type="range"
|
|
24
|
+
class="form-range"
|
|
25
|
+
min="2"
|
|
26
|
+
max="20"
|
|
27
|
+
step="1"
|
|
28
|
+
bind:value={store.size}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Site Opacity slider -->
|
|
33
|
+
<div class="control-row">
|
|
34
|
+
<label for="site-opacity-slider" class="control-label">
|
|
35
|
+
Opacity: <strong>{Math.round(store.opacity * 100)}%</strong>
|
|
36
|
+
</label>
|
|
37
|
+
<input
|
|
38
|
+
id="site-opacity-slider"
|
|
39
|
+
type="range"
|
|
40
|
+
class="form-range"
|
|
41
|
+
min="0"
|
|
42
|
+
max="1"
|
|
43
|
+
step="0.1"
|
|
44
|
+
bind:value={store.opacity}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Site Circle Color -->
|
|
49
|
+
<!-- <div class="control-row">
|
|
50
|
+
<label for="site-color-picker" class="control-label">
|
|
51
|
+
Base Color
|
|
52
|
+
</label>
|
|
53
|
+
<input
|
|
54
|
+
id="site-color-picker"
|
|
55
|
+
type="color"
|
|
56
|
+
class="form-control form-control-color"
|
|
57
|
+
bind:value={store.color}
|
|
58
|
+
/>
|
|
59
|
+
</div> -->
|
|
60
|
+
<!-- Cell Labels Section -->
|
|
61
|
+
<hr class="section-divider" />
|
|
62
|
+
<!-- Show site labels checkbox -->
|
|
63
|
+
<div class="control-row">
|
|
64
|
+
<label for="site-show-labels" class="control-label">
|
|
65
|
+
Show Site Labels
|
|
66
|
+
</label>
|
|
67
|
+
<div class="form-check form-switch">
|
|
68
|
+
<input
|
|
69
|
+
id="site-show-labels"
|
|
70
|
+
type="checkbox"
|
|
71
|
+
class="form-check-input"
|
|
72
|
+
role="switch"
|
|
73
|
+
bind:checked={store.showLabels}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{#if store.showLabels}
|
|
79
|
+
<!-- Site Label size slider -->
|
|
80
|
+
<div class="control-row">
|
|
81
|
+
<label for="site-label-size-slider" class="control-label">
|
|
82
|
+
Label Size: <strong>{store.labelSize}px</strong>
|
|
83
|
+
</label>
|
|
84
|
+
<input
|
|
85
|
+
id="site-label-size-slider"
|
|
86
|
+
type="range"
|
|
87
|
+
class="form-range"
|
|
88
|
+
min="8"
|
|
89
|
+
max="20"
|
|
90
|
+
step="1"
|
|
91
|
+
bind:value={store.labelSize}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Site Label color picker -->
|
|
96
|
+
<div class="control-row">
|
|
97
|
+
<label for="site-label-color-picker" class="control-label">
|
|
98
|
+
Label Color
|
|
99
|
+
</label>
|
|
100
|
+
<input
|
|
101
|
+
id="site-label-color-picker"
|
|
102
|
+
type="color"
|
|
103
|
+
class="form-control form-control-color"
|
|
104
|
+
bind:value={store.labelColor}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- Site Label offset slider -->
|
|
109
|
+
<div class="control-row">
|
|
110
|
+
<label for="site-label-offset-slider" class="control-label">
|
|
111
|
+
Label Offset: <strong>{store.labelOffset.toFixed(1)}</strong>
|
|
112
|
+
</label>
|
|
113
|
+
<input
|
|
114
|
+
id="site-label-offset-slider"
|
|
115
|
+
type="range"
|
|
116
|
+
class="form-range"
|
|
117
|
+
min="-3"
|
|
118
|
+
max="3"
|
|
119
|
+
step="0.1"
|
|
120
|
+
bind:value={store.labelOffset}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Site Label property dropdown -->
|
|
125
|
+
<div class="control-row">
|
|
126
|
+
<label for="site-label-property-select" class="control-label">
|
|
127
|
+
Label Field
|
|
128
|
+
</label>
|
|
129
|
+
<select
|
|
130
|
+
id="site-label-property-select"
|
|
131
|
+
class="form-select form-select-sm"
|
|
132
|
+
bind:value={store.labelProperty}
|
|
133
|
+
>
|
|
134
|
+
<option value="name">Name</option>
|
|
135
|
+
<option value="id">ID</option>
|
|
136
|
+
<option value="provider">Provider</option>
|
|
137
|
+
<option value="technology">Technology</option>
|
|
138
|
+
<option value="featureGroup">Feature Group</option>
|
|
139
|
+
</select>
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<style>
|
|
145
|
+
.settings-panel {
|
|
146
|
+
width: 100%;
|
|
147
|
+
padding: 0.75rem;
|
|
148
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
149
|
+
border-radius: 0.375rem;
|
|
150
|
+
background-color: rgba(255, 255, 255, 0.02);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.control-row {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: space-between;
|
|
157
|
+
gap: 0.75rem;
|
|
158
|
+
margin-bottom: 0.875rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.control-row:last-child {
|
|
162
|
+
margin-bottom: 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.control-label {
|
|
166
|
+
font-size: 0.8125rem;
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
margin: 0;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
flex-shrink: 0;
|
|
171
|
+
min-width: 110px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.form-range,
|
|
175
|
+
.form-select {
|
|
176
|
+
width: 140px;
|
|
177
|
+
min-width: 140px;
|
|
178
|
+
max-width: 140px;
|
|
179
|
+
flex: 0 0 140px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.form-control-color {
|
|
183
|
+
width: 45px;
|
|
184
|
+
height: 28px;
|
|
185
|
+
padding: 2px;
|
|
186
|
+
border-radius: 4px;
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
margin-right: auto;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.form-check {
|
|
192
|
+
margin: 0;
|
|
193
|
+
margin-right: auto;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.form-switch .form-check-input {
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SiteSettingsPanel - Site visualization settings
|
|
3
|
+
* Reusable panel component for site display configuration
|
|
4
|
+
*/
|
|
5
|
+
import type { SiteStoreContext } from '../../../features/sites/stores/siteStoreContext.svelte';
|
|
6
|
+
interface Props {
|
|
7
|
+
store: SiteStoreContext;
|
|
8
|
+
}
|
|
9
|
+
declare const SiteSettingsPanel: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type SiteSettingsPanel = ReturnType<typeof SiteSettingsPanel>;
|
|
11
|
+
export default SiteSettingsPanel;
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
* Exports shared controls and utilities used across features
|
|
5
5
|
*/
|
|
6
6
|
export { default as MapControl } from './controls/MapControl.svelte';
|
|
7
|
+
export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
|
|
7
8
|
export { addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './utils/mapboxHelpers';
|
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
// Controls
|
|
7
7
|
export { default as MapControl } from './controls/MapControl.svelte';
|
|
8
|
+
export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
|
|
8
9
|
// Mapbox Helpers
|
|
9
10
|
export { addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './utils/mapboxHelpers';
|