@smartnet360/svelte-components 0.0.66 → 0.0.68
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/core/components/MapStoreBridge.svelte +26 -0
- package/dist/map-v2/core/components/MapStoreBridge.svelte.d.ts +8 -0
- package/dist/map-v2/core/index.d.ts +1 -0
- package/dist/map-v2/core/index.js +1 -0
- package/dist/map-v2/demo/DemoMap.svelte +74 -3
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +20 -31
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte.d.ts +7 -0
- package/dist/map-v2/features/cells/controls/CellStyleControl.svelte +246 -0
- package/dist/map-v2/features/cells/controls/CellStyleControl.svelte.d.ts +7 -0
- package/dist/map-v2/features/cells/index.d.ts +2 -1
- package/dist/map-v2/features/cells/index.js +1 -0
- package/dist/map-v2/features/cells/layers/CellLabelsLayer.svelte +237 -0
- package/dist/map-v2/features/cells/layers/CellLabelsLayer.svelte.d.ts +10 -0
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.d.ts +36 -0
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.js +78 -2
- package/dist/map-v2/features/cells/types.d.ts +8 -2
- package/dist/map-v2/features/cells/types.js +1 -1
- package/dist/map-v2/features/cells/utils/cellTree.d.ts +4 -3
- package/dist/map-v2/features/cells/utils/cellTree.js +54 -54
- package/dist/map-v2/index.d.ts +3 -3
- package/dist/map-v2/index.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import type { Map as MapboxMap } from 'mapbox-gl';
|
|
4
|
+
import { useMapbox } from '../hooks/useMapbox';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
onMapChange?: (map: MapboxMap | null) => void;
|
|
8
|
+
children?: import('svelte').Snippet;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { onMapChange, children }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const providerStore = useMapbox();
|
|
14
|
+
let ready = $state(false);
|
|
15
|
+
|
|
16
|
+
const unsubscribe = providerStore.subscribe((map) => {
|
|
17
|
+
onMapChange?.(map);
|
|
18
|
+
ready = map !== null;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
onDestroy(unsubscribe);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if ready && children}
|
|
25
|
+
{@render children()}
|
|
26
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Map as MapboxMap } from 'mapbox-gl';
|
|
2
|
+
interface Props {
|
|
3
|
+
onMapChange?: (map: MapboxMap | null) => void;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
declare const MapStoreBridge: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type MapStoreBridge = ReturnType<typeof MapStoreBridge>;
|
|
8
|
+
export default MapStoreBridge;
|
|
@@ -7,6 +7,7 @@ export type { MapStore } from './types';
|
|
|
7
7
|
export { MAP_CONTEXT_KEY } from './types';
|
|
8
8
|
export { default as MapboxProvider } from './providers/MapboxProvider.svelte';
|
|
9
9
|
export { default as ViewportSync } from './components/ViewportSync.svelte';
|
|
10
|
+
export { default as MapStoreBridge } from './components/MapStoreBridge.svelte';
|
|
10
11
|
export { default as MapStyleControl } from './controls/MapStyleControl.svelte';
|
|
11
12
|
export { createMapStore } from './stores/mapStore';
|
|
12
13
|
export { createViewportStore, type ViewportStore, type ViewportState } from './stores/viewportStore.svelte';
|
|
@@ -8,6 +8,7 @@ export { MAP_CONTEXT_KEY } from './types';
|
|
|
8
8
|
export { default as MapboxProvider } from './providers/MapboxProvider.svelte';
|
|
9
9
|
// Components
|
|
10
10
|
export { default as ViewportSync } from './components/ViewportSync.svelte';
|
|
11
|
+
export { default as MapStoreBridge } from './components/MapStoreBridge.svelte';
|
|
11
12
|
// Controls
|
|
12
13
|
export { default as MapStyleControl } from './controls/MapStyleControl.svelte';
|
|
13
14
|
// Stores
|
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
import SiteSizeSlider from '../features/sites/controls/SiteSizeSlider.svelte';
|
|
21
21
|
import SiteSelectionControl from '../features/sites/controls/SiteSelectionControl.svelte';
|
|
22
22
|
import CellsLayer from '../features/cells/layers/CellsLayer.svelte';
|
|
23
|
+
import CellLabelsLayer from '../features/cells/layers/CellLabelsLayer.svelte';
|
|
23
24
|
import CellFilterControl from '../features/cells/controls/CellFilterControl.svelte';
|
|
24
25
|
import CellStyleControl from '../features/cells/controls/CellStyleControl.svelte';
|
|
25
26
|
import { createSiteStoreContext } from '../features/sites/stores/siteStoreContext.svelte';
|
|
26
27
|
import { createCellStoreContext } from '../features/cells/stores/cellStoreContext.svelte';
|
|
27
28
|
import { createViewportStore } from '../core/stores/viewportStore.svelte';
|
|
29
|
+
import type { CellGroupingField, CellGroupingLabels } from '../features/cells/types';
|
|
28
30
|
import { demoSites } from './demo-data';
|
|
29
31
|
import { demoCells } from './demo-cells';
|
|
32
|
+
import { writable } from 'svelte/store';
|
|
30
33
|
|
|
31
34
|
interface Props {
|
|
32
35
|
/** Mapbox access token */
|
|
@@ -58,8 +61,8 @@
|
|
|
58
61
|
// Bootstrap icon names for each control header
|
|
59
62
|
const controlIcons = {
|
|
60
63
|
mapStyle: 'layers',
|
|
61
|
-
cellFilter: '
|
|
62
|
-
siteFilter: '
|
|
64
|
+
cellFilter: 'radioactive',
|
|
65
|
+
siteFilter: 'broadcast-pin',
|
|
63
66
|
siteSelection: 'check2-square',
|
|
64
67
|
cellStyle: 'palette',
|
|
65
68
|
siteSize: 'sliders'
|
|
@@ -73,7 +76,67 @@
|
|
|
73
76
|
// Or use SvelteKit navigate, or open modal, etc.
|
|
74
77
|
alert(`Selected ${siteIds.length} sites:\n${siteIds.join(', ')}`);
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
|
|
80
|
+
// Cell filter grouping configuration
|
|
81
|
+
const cellLevel1Options: Array<Exclude<CellGroupingField, 'none'>> = [
|
|
82
|
+
'tech',
|
|
83
|
+
'frq',
|
|
84
|
+
'status',
|
|
85
|
+
'siteId',
|
|
86
|
+
'customSubgroup',
|
|
87
|
+
'type',
|
|
88
|
+
'planner'
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const cellLevel2Options: CellGroupingField[] = [
|
|
92
|
+
'none',
|
|
93
|
+
'tech',
|
|
94
|
+
'frq',
|
|
95
|
+
'status',
|
|
96
|
+
'siteId',
|
|
97
|
+
'customSubgroup',
|
|
98
|
+
'type',
|
|
99
|
+
'planner'
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const cellFieldLabels: CellGroupingLabels = {
|
|
103
|
+
tech: 'Technology',
|
|
104
|
+
frq: 'Frequency Band',
|
|
105
|
+
status: 'Status',
|
|
106
|
+
siteId: 'Site ID',
|
|
107
|
+
customSubgroup: 'Custom Subgroup',
|
|
108
|
+
type: 'Type',
|
|
109
|
+
planner: 'Planner',
|
|
110
|
+
none: 'None (Flat Tree)'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Cell label field options (for CellStyleControl)
|
|
114
|
+
// 4G/5G cells have different available fields than 2G
|
|
115
|
+
const labelFieldOptions4G5G: Array<keyof typeof demoCells[0]> = [
|
|
116
|
+
'cellID',
|
|
117
|
+
'cellName',
|
|
118
|
+
'azimuth',
|
|
119
|
+
'fband',
|
|
120
|
+
'frq',
|
|
121
|
+
'dlEarfn',
|
|
122
|
+
'tech',
|
|
123
|
+
'status',
|
|
124
|
+
'pci1'
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const labelFieldOptions2G: Array<keyof typeof demoCells[0]> = [
|
|
128
|
+
'cellID',
|
|
129
|
+
'cellName',
|
|
130
|
+
'azimuth',
|
|
131
|
+
'bcch',
|
|
132
|
+
'ctrlid',
|
|
133
|
+
'fband',
|
|
134
|
+
'frq',
|
|
135
|
+
'tech',
|
|
136
|
+
'status'
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
let mapStore = writable()
|
|
77
140
|
</script>
|
|
78
141
|
|
|
79
142
|
<!-- // controls={['navigation', 'scale']} -->
|
|
@@ -88,6 +151,9 @@
|
|
|
88
151
|
<!-- Cell layer - renders arc sectors from store -->
|
|
89
152
|
<CellsLayer store={cellStore} namespace="demo-cells" />
|
|
90
153
|
|
|
154
|
+
<!-- Cell labels layer - renders tech-aware labels positioned along azimuth -->
|
|
155
|
+
<CellLabelsLayer store={cellStore} namespace="demo-cells" />
|
|
156
|
+
|
|
91
157
|
<!-- Map style control - switch between map styles -->
|
|
92
158
|
<MapStyleControl
|
|
93
159
|
position="top-right"
|
|
@@ -100,6 +166,9 @@
|
|
|
100
166
|
<!-- Cell filter control - dynamic hierarchical filtering -->
|
|
101
167
|
<CellFilterControl
|
|
102
168
|
store={cellStore}
|
|
169
|
+
level1Options={cellLevel1Options}
|
|
170
|
+
level2Options={cellLevel2Options}
|
|
171
|
+
fieldLabels={cellFieldLabels}
|
|
103
172
|
position="top-left"
|
|
104
173
|
title="Cell Filter"
|
|
105
174
|
initiallyCollapsed={true}
|
|
@@ -135,6 +204,8 @@
|
|
|
135
204
|
initiallyCollapsed={true}
|
|
136
205
|
icon={controlIcons.cellStyle}
|
|
137
206
|
iconOnlyWhenCollapsed={useIconHeaders}
|
|
207
|
+
labelFieldOptions4G5G={labelFieldOptions4G5G}
|
|
208
|
+
labelFieldOptions2G={labelFieldOptions2G}
|
|
138
209
|
/>
|
|
139
210
|
|
|
140
211
|
<!-- Site size control - updates store visual properties -->
|
|
@@ -17,12 +17,18 @@
|
|
|
17
17
|
import { createTreeStore } from '../../../../core/TreeView/tree.store';
|
|
18
18
|
import { buildCellTree, getFilteredCells } from '../utils/cellTree';
|
|
19
19
|
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
20
|
-
import type { CellGroupingField } from '../types';
|
|
20
|
+
import type { CellGroupingField, CellGroupingLabels } from '../types';
|
|
21
21
|
import type { TreeStoreValue } from '../../../../core/TreeView/tree.model';
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
/** Cell store context */
|
|
25
25
|
store: CellStoreContext;
|
|
26
|
+
/** Available options for level 1 grouping (mandatory) */
|
|
27
|
+
level1Options: Array<Exclude<CellGroupingField, 'none'>>;
|
|
28
|
+
/** Available options for level 2 grouping (mandatory, can include 'none') */
|
|
29
|
+
level2Options: CellGroupingField[];
|
|
30
|
+
/** Optional label map for human-readable field names */
|
|
31
|
+
fieldLabels?: CellGroupingLabels;
|
|
26
32
|
/** Control position */
|
|
27
33
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
28
34
|
/** Control title */
|
|
@@ -37,6 +43,9 @@
|
|
|
37
43
|
|
|
38
44
|
let {
|
|
39
45
|
store,
|
|
46
|
+
level1Options,
|
|
47
|
+
level2Options,
|
|
48
|
+
fieldLabels,
|
|
40
49
|
position = 'top-left',
|
|
41
50
|
title = 'Cell Filter',
|
|
42
51
|
icon = 'diagram-3',
|
|
@@ -44,20 +53,6 @@
|
|
|
44
53
|
initiallyCollapsed = true
|
|
45
54
|
}: Props = $props();
|
|
46
55
|
|
|
47
|
-
// Grouping options (excluding 'none' from level1)
|
|
48
|
-
const GROUPING_OPTIONS: Exclude<CellGroupingField, 'none'>[] = [
|
|
49
|
-
'tech',
|
|
50
|
-
'band',
|
|
51
|
-
'status',
|
|
52
|
-
'siteId',
|
|
53
|
-
'customSubgroup',
|
|
54
|
-
'type',
|
|
55
|
-
'planner'
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
// Level2 options include 'none' for flat tree
|
|
59
|
-
const LEVEL2_OPTIONS: CellGroupingField[] = ['none', ...GROUPING_OPTIONS];
|
|
60
|
-
|
|
61
56
|
let treeStore = $state<Writable<TreeStoreValue> | null>(null);
|
|
62
57
|
let level1 = $state<Exclude<CellGroupingField, 'none'>>(store.groupingConfig.level1);
|
|
63
58
|
let level2 = $state<CellGroupingField>(store.groupingConfig.level2);
|
|
@@ -91,7 +86,8 @@
|
|
|
91
86
|
const { tree: treeNodes, cellGroupMap } = buildCellTree(
|
|
92
87
|
store.cellsFilteredByStatus, // Use filtered cells, not all cells
|
|
93
88
|
{ level1, level2 },
|
|
94
|
-
store.groupColorMap
|
|
89
|
+
store.groupColorMap,
|
|
90
|
+
fieldLabels
|
|
95
91
|
);
|
|
96
92
|
|
|
97
93
|
// Update the cell-to-group lookup map in the store
|
|
@@ -161,17 +157,10 @@
|
|
|
161
157
|
store.setGroupColor(groupKey, color);
|
|
162
158
|
}
|
|
163
159
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
status: 'Status',
|
|
169
|
-
siteId: 'Site ID',
|
|
170
|
-
customSubgroup: 'Custom Subgroup',
|
|
171
|
-
type: 'Type',
|
|
172
|
-
planner: 'Planner',
|
|
173
|
-
none: 'None (2-Level Tree)'
|
|
174
|
-
};
|
|
160
|
+
// Get label for a field from the fieldLabels map or fallback to field name
|
|
161
|
+
function getFieldLabel(field: CellGroupingField): string {
|
|
162
|
+
return fieldLabels?.[field] || String(field);
|
|
163
|
+
}
|
|
175
164
|
</script>
|
|
176
165
|
|
|
177
166
|
<MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
|
|
@@ -207,8 +196,8 @@
|
|
|
207
196
|
value={level1}
|
|
208
197
|
onchange={handleLevel1Change}
|
|
209
198
|
>
|
|
210
|
-
{#each
|
|
211
|
-
<option value={option}>{
|
|
199
|
+
{#each level1Options as option}
|
|
200
|
+
<option value={option}>{getFieldLabel(option)}</option>
|
|
212
201
|
{/each}
|
|
213
202
|
</select>
|
|
214
203
|
</div>
|
|
@@ -222,8 +211,8 @@
|
|
|
222
211
|
value={level2}
|
|
223
212
|
onchange={handleLevel2Change}
|
|
224
213
|
>
|
|
225
|
-
{#each
|
|
226
|
-
<option value={option}>{
|
|
214
|
+
{#each level2Options as option}
|
|
215
|
+
<option value={option}>{getFieldLabel(option)}</option>
|
|
227
216
|
{/each}
|
|
228
217
|
</select>
|
|
229
218
|
</div>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
2
|
+
import type { CellGroupingField, CellGroupingLabels } from '../types';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Cell store context */
|
|
4
5
|
store: CellStoreContext;
|
|
6
|
+
/** Available options for level 1 grouping (mandatory) */
|
|
7
|
+
level1Options: Array<Exclude<CellGroupingField, 'none'>>;
|
|
8
|
+
/** Available options for level 2 grouping (mandatory, can include 'none') */
|
|
9
|
+
level2Options: CellGroupingField[];
|
|
10
|
+
/** Optional label map for human-readable field names */
|
|
11
|
+
fieldLabels?: CellGroupingLabels;
|
|
5
12
|
/** Control position */
|
|
6
13
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
14
|
/** Control title */
|
|
@@ -11,10 +11,17 @@
|
|
|
11
11
|
|
|
12
12
|
import MapControl from '../../../shared/controls/MapControl.svelte';
|
|
13
13
|
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
14
|
+
import type { Cell, CellGroupingLabels } from '../types';
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
16
17
|
/** Cell store context */
|
|
17
18
|
store: CellStoreContext;
|
|
19
|
+
/** Available label field options for 4G/5G cells */
|
|
20
|
+
labelFieldOptions4G5G: Array<keyof Cell>;
|
|
21
|
+
/** Available label field options for 2G cells */
|
|
22
|
+
labelFieldOptions2G: Array<keyof Cell>;
|
|
23
|
+
/** Optional label map for human-readable field names */
|
|
24
|
+
fieldLabels?: CellGroupingLabels;
|
|
18
25
|
/** Control position */
|
|
19
26
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
20
27
|
/** Control title */
|
|
@@ -29,12 +36,21 @@
|
|
|
29
36
|
|
|
30
37
|
let {
|
|
31
38
|
store,
|
|
39
|
+
labelFieldOptions4G5G,
|
|
40
|
+
labelFieldOptions2G,
|
|
41
|
+
fieldLabels,
|
|
32
42
|
position = 'bottom-left',
|
|
33
43
|
title = 'Cell Display',
|
|
34
44
|
icon = 'palette',
|
|
35
45
|
iconOnlyWhenCollapsed = true,
|
|
36
46
|
initiallyCollapsed = true
|
|
37
47
|
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
// Get label for a field from the fieldLabels map or fallback to field name
|
|
50
|
+
function getFieldLabel(field: keyof Cell | 'none'): string {
|
|
51
|
+
if (field === 'none') return 'None';
|
|
52
|
+
return fieldLabels?.[field] || String(field);
|
|
53
|
+
}
|
|
38
54
|
</script>
|
|
39
55
|
|
|
40
56
|
<MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
|
|
@@ -102,6 +118,212 @@
|
|
|
102
118
|
bind:value={store.fillOpacity}
|
|
103
119
|
/>
|
|
104
120
|
</div>
|
|
121
|
+
|
|
122
|
+
<!-- Cell Labels Section -->
|
|
123
|
+
<hr class="section-divider" />
|
|
124
|
+
<h6 class="section-title">Cell Labels</h6>
|
|
125
|
+
|
|
126
|
+
<!-- Show labels toggle -->
|
|
127
|
+
<div class="control-row">
|
|
128
|
+
<label for="cell-labels-toggle" class="control-label">
|
|
129
|
+
Show Labels
|
|
130
|
+
</label>
|
|
131
|
+
<div class="form-check form-switch">
|
|
132
|
+
<input
|
|
133
|
+
id="cell-labels-toggle"
|
|
134
|
+
type="checkbox"
|
|
135
|
+
class="form-check-input"
|
|
136
|
+
role="switch"
|
|
137
|
+
checked={store.showLabels}
|
|
138
|
+
onchange={(e) => store.setShowLabels(e.currentTarget.checked)}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{#if store.showLabels}
|
|
144
|
+
<!-- 4G/5G Primary Label Field -->
|
|
145
|
+
<div class="control-row">
|
|
146
|
+
<label for="label-primary-4g5g" class="control-label">
|
|
147
|
+
4G/5G Primary
|
|
148
|
+
</label>
|
|
149
|
+
<select
|
|
150
|
+
id="label-primary-4g5g"
|
|
151
|
+
class="form-select form-select-sm"
|
|
152
|
+
value={store.primaryLabelField4G5G}
|
|
153
|
+
onchange={(e) => store.setPrimaryLabelField4G5G(e.currentTarget.value as keyof Cell)}
|
|
154
|
+
>
|
|
155
|
+
{#each labelFieldOptions4G5G as field}
|
|
156
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
157
|
+
{/each}
|
|
158
|
+
</select>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- 4G/5G Secondary Label Field -->
|
|
162
|
+
<div class="control-row">
|
|
163
|
+
<label for="label-secondary-4g5g" class="control-label">
|
|
164
|
+
4G/5G Secondary
|
|
165
|
+
</label>
|
|
166
|
+
<select
|
|
167
|
+
id="label-secondary-4g5g"
|
|
168
|
+
class="form-select form-select-sm"
|
|
169
|
+
value={store.secondaryLabelField4G5G}
|
|
170
|
+
onchange={(e) => store.setSecondaryLabelField4G5G(e.currentTarget.value as keyof Cell | 'none')}
|
|
171
|
+
>
|
|
172
|
+
<option value="none">None</option>
|
|
173
|
+
{#each labelFieldOptions4G5G as field}
|
|
174
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
175
|
+
{/each}
|
|
176
|
+
</select>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- 2G Primary Label Field -->
|
|
180
|
+
<div class="control-row">
|
|
181
|
+
<label for="label-primary-2g" class="control-label">
|
|
182
|
+
2G Primary
|
|
183
|
+
</label>
|
|
184
|
+
<select
|
|
185
|
+
id="label-primary-2g"
|
|
186
|
+
class="form-select form-select-sm"
|
|
187
|
+
value={store.primaryLabelField2G}
|
|
188
|
+
onchange={(e) => store.setPrimaryLabelField2G(e.currentTarget.value as keyof Cell)}
|
|
189
|
+
>
|
|
190
|
+
{#each labelFieldOptions2G as field}
|
|
191
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
192
|
+
{/each}
|
|
193
|
+
</select>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- 2G Secondary Label Field -->
|
|
197
|
+
<div class="control-row">
|
|
198
|
+
<label for="label-secondary-2g" class="control-label">
|
|
199
|
+
2G Secondary
|
|
200
|
+
</label>
|
|
201
|
+
<select
|
|
202
|
+
id="label-secondary-2g"
|
|
203
|
+
class="form-select form-select-sm"
|
|
204
|
+
value={store.secondaryLabelField2G}
|
|
205
|
+
onchange={(e) => store.setSecondaryLabelField2G(e.currentTarget.value as keyof Cell | 'none')}
|
|
206
|
+
>
|
|
207
|
+
<option value="none">None</option>
|
|
208
|
+
{#each labelFieldOptions2G as field}
|
|
209
|
+
<option value={field}>{getFieldLabel(field)}</option>
|
|
210
|
+
{/each}
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- Label Size -->
|
|
215
|
+
<div class="control-row">
|
|
216
|
+
<label for="label-size-slider" class="control-label">
|
|
217
|
+
Label Size: <strong>{store.labelSize}px</strong>
|
|
218
|
+
</label>
|
|
219
|
+
<input
|
|
220
|
+
id="label-size-slider"
|
|
221
|
+
type="range"
|
|
222
|
+
class="form-range"
|
|
223
|
+
min="8"
|
|
224
|
+
max="20"
|
|
225
|
+
step="1"
|
|
226
|
+
value={store.labelSize}
|
|
227
|
+
oninput={(e) => store.setLabelSize(Number(e.currentTarget.value))}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- Label Color -->
|
|
232
|
+
<div class="control-row">
|
|
233
|
+
<label for="label-color-picker" class="control-label">
|
|
234
|
+
Label Color
|
|
235
|
+
</label>
|
|
236
|
+
<input
|
|
237
|
+
id="label-color-picker"
|
|
238
|
+
type="color"
|
|
239
|
+
class="form-control form-control-color"
|
|
240
|
+
value={store.labelColor}
|
|
241
|
+
oninput={(e) => store.setLabelColor(e.currentTarget.value)}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Label Offset -->
|
|
246
|
+
<div class="control-row">
|
|
247
|
+
<label for="label-offset-slider" class="control-label">
|
|
248
|
+
Label Offset: <strong>{store.labelOffset}m</strong>
|
|
249
|
+
</label>
|
|
250
|
+
<input
|
|
251
|
+
id="label-offset-slider"
|
|
252
|
+
type="range"
|
|
253
|
+
class="form-range"
|
|
254
|
+
min="100"
|
|
255
|
+
max="1000"
|
|
256
|
+
step="50"
|
|
257
|
+
value={store.labelOffset}
|
|
258
|
+
oninput={(e) => store.setLabelOffset(Number(e.currentTarget.value))}
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- Label Halo Color -->
|
|
263
|
+
<div class="control-row">
|
|
264
|
+
<label for="label-halo-color-picker" class="control-label">
|
|
265
|
+
Halo Color
|
|
266
|
+
</label>
|
|
267
|
+
<input
|
|
268
|
+
id="label-halo-color-picker"
|
|
269
|
+
type="color"
|
|
270
|
+
class="form-control form-control-color"
|
|
271
|
+
value={store.labelHaloColor}
|
|
272
|
+
oninput={(e) => store.setLabelHaloColor(e.currentTarget.value)}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<!-- Label Halo Width -->
|
|
277
|
+
<div class="control-row">
|
|
278
|
+
<label for="label-halo-width-slider" class="control-label">
|
|
279
|
+
Halo Width: <strong>{store.labelHaloWidth}px</strong>
|
|
280
|
+
</label>
|
|
281
|
+
<input
|
|
282
|
+
id="label-halo-width-slider"
|
|
283
|
+
type="range"
|
|
284
|
+
class="form-range"
|
|
285
|
+
min="0"
|
|
286
|
+
max="3"
|
|
287
|
+
step="0.5"
|
|
288
|
+
value={store.labelHaloWidth}
|
|
289
|
+
oninput={(e) => store.setLabelHaloWidth(Number(e.currentTarget.value))}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<!-- Min Label Zoom -->
|
|
294
|
+
<div class="control-row">
|
|
295
|
+
<label for="min-label-zoom-slider" class="control-label">
|
|
296
|
+
Min Zoom: <strong>{store.minLabelZoom}</strong>
|
|
297
|
+
</label>
|
|
298
|
+
<input
|
|
299
|
+
id="min-label-zoom-slider"
|
|
300
|
+
type="range"
|
|
301
|
+
class="form-range"
|
|
302
|
+
min="10"
|
|
303
|
+
max="18"
|
|
304
|
+
step="1"
|
|
305
|
+
value={store.minLabelZoom}
|
|
306
|
+
oninput={(e) => store.setMinLabelZoom(Number(e.currentTarget.value))}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<!-- Azimuth Tolerance -->
|
|
311
|
+
<div class="control-row">
|
|
312
|
+
<label for="azimuth-tolerance-slider" class="control-label">
|
|
313
|
+
Azimuth Tolerance: <strong>±{store.azimuthTolerance}°</strong>
|
|
314
|
+
</label>
|
|
315
|
+
<input
|
|
316
|
+
id="azimuth-tolerance-slider"
|
|
317
|
+
type="range"
|
|
318
|
+
class="form-range"
|
|
319
|
+
min="1"
|
|
320
|
+
max="15"
|
|
321
|
+
step="1"
|
|
322
|
+
value={store.azimuthTolerance}
|
|
323
|
+
oninput={(e) => store.setAzimuthTolerance(Number(e.currentTarget.value))}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
{/if}
|
|
105
327
|
</div>
|
|
106
328
|
</MapControl>
|
|
107
329
|
|
|
@@ -110,6 +332,18 @@
|
|
|
110
332
|
min-width: 250px;
|
|
111
333
|
padding: 0.5rem 0;
|
|
112
334
|
}
|
|
335
|
+
|
|
336
|
+
.section-divider {
|
|
337
|
+
margin: 1rem 0;
|
|
338
|
+
opacity: 0.3;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.section-title {
|
|
342
|
+
font-size: 0.875rem;
|
|
343
|
+
font-weight: 600;
|
|
344
|
+
margin-bottom: 0.75rem;
|
|
345
|
+
color: var(--bs-body-color);
|
|
346
|
+
}
|
|
113
347
|
|
|
114
348
|
.control-row {
|
|
115
349
|
display: flex;
|
|
@@ -134,6 +368,18 @@
|
|
|
134
368
|
flex: 1;
|
|
135
369
|
min-width: 120px;
|
|
136
370
|
}
|
|
371
|
+
|
|
372
|
+
.form-select {
|
|
373
|
+
flex: 1;
|
|
374
|
+
min-width: 120px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.form-control-color {
|
|
378
|
+
width: 50px;
|
|
379
|
+
height: 30px;
|
|
380
|
+
padding: 2px;
|
|
381
|
+
border-radius: 4px;
|
|
382
|
+
}
|
|
137
383
|
|
|
138
384
|
.form-check {
|
|
139
385
|
margin: 0;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
2
|
+
import type { Cell, CellGroupingLabels } from '../types';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Cell store context */
|
|
4
5
|
store: CellStoreContext;
|
|
6
|
+
/** Available label field options for 4G/5G cells */
|
|
7
|
+
labelFieldOptions4G5G: Array<keyof Cell>;
|
|
8
|
+
/** Available label field options for 2G cells */
|
|
9
|
+
labelFieldOptions2G: Array<keyof Cell>;
|
|
10
|
+
/** Optional label map for human-readable field names */
|
|
11
|
+
fieldLabels?: CellGroupingLabels;
|
|
5
12
|
/** Control position */
|
|
6
13
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
14
|
/** Control title */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Exports all cell-related types, components, utilities, and constants
|
|
5
5
|
*/
|
|
6
|
-
export type { Cell, CellStatus, CellStatusStyle, TechnologyBandKey, ParsedTechBand, CellGroupingField, CellTreeConfig } from './types';
|
|
6
|
+
export type { Cell, CellStatus, CellStatusStyle, TechnologyBandKey, ParsedTechBand, CellGroupingField, CellGroupingLabels, CellTreeConfig } from './types';
|
|
7
7
|
export { DEFAULT_CELL_TREE_CONFIG } from './types';
|
|
8
8
|
export { TECHNOLOGY_BAND_COLORS } from './constants/colors';
|
|
9
9
|
export { RADIUS_MULTIPLIER } from './constants/radiusMultipliers';
|
|
@@ -11,6 +11,7 @@ export { Z_INDEX_BY_BAND, CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from './constan
|
|
|
11
11
|
export { DEFAULT_STATUS_STYLES } from './constants/statusStyles';
|
|
12
12
|
export { createCellStoreContext, type CellStoreContext, type CellStoreValue } from './stores/cellStoreContext.svelte';
|
|
13
13
|
export { default as CellsLayer } from './layers/CellsLayer.svelte';
|
|
14
|
+
export { default as CellLabelsLayer } from './layers/CellLabelsLayer.svelte';
|
|
14
15
|
export { default as CellFilterControl } from './controls/CellFilterControl.svelte';
|
|
15
16
|
export { default as CellStyleControl } from './controls/CellStyleControl.svelte';
|
|
16
17
|
export { parseTechBand } from './utils/techBandParser';
|
|
@@ -13,6 +13,7 @@ export { DEFAULT_STATUS_STYLES } from './constants/statusStyles';
|
|
|
13
13
|
export { createCellStoreContext } from './stores/cellStoreContext.svelte';
|
|
14
14
|
// Layers
|
|
15
15
|
export { default as CellsLayer } from './layers/CellsLayer.svelte';
|
|
16
|
+
export { default as CellLabelsLayer } from './layers/CellLabelsLayer.svelte';
|
|
16
17
|
// Controls
|
|
17
18
|
export { default as CellFilterControl } from './controls/CellFilterControl.svelte';
|
|
18
19
|
export { default as CellStyleControl } from './controls/CellStyleControl.svelte';
|