@smartnet360/svelte-components 0.0.67 → 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 +2 -2
- package/dist/map-v2/index.js +2 -2
- 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';
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CellLabelsLayer - Renders tech-aware cell labels positioned along azimuth
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Groups cells by site + azimuth (with configurable tolerance)
|
|
7
|
+
* - Projects labels along bearing from site center
|
|
8
|
+
* - Stacks multiple labels vertically when cells share direction
|
|
9
|
+
* - Tech-specific field selection (2G vs 4G/5G)
|
|
10
|
+
* - Configurable styling (size, color, halo, zoom)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { onMount, onDestroy } from 'svelte';
|
|
14
|
+
import { useMapbox } from '../../../core/hooks/useMapbox';
|
|
15
|
+
import { waitForStyleLoad, generateLayerId, generateSourceId } from '../../../shared/utils/mapboxHelpers';
|
|
16
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
17
|
+
import type { Cell } from '../types';
|
|
18
|
+
import * as turf from '@turf/turf';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
/** Cell store context */
|
|
22
|
+
store: CellStoreContext;
|
|
23
|
+
/** Unique namespace for layer/source IDs */
|
|
24
|
+
namespace: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { store, namespace }: Props = $props();
|
|
28
|
+
|
|
29
|
+
const mapStore = useMapbox();
|
|
30
|
+
const sourceId = generateSourceId(namespace, 'cell-labels');
|
|
31
|
+
const layerId = generateLayerId(namespace, 'cell-labels');
|
|
32
|
+
|
|
33
|
+
let map = $state<mapboxgl.Map | null>(null);
|
|
34
|
+
let unsubscribe: (() => void) | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get label text for a cell based on tech and selected field
|
|
38
|
+
*/
|
|
39
|
+
function getCellLabelText(cell: Cell, field: keyof Cell | 'none'): string {
|
|
40
|
+
if (field === 'none') return '';
|
|
41
|
+
|
|
42
|
+
const value = cell[field];
|
|
43
|
+
return value == null ? '' : String(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Round azimuth to nearest tolerance value
|
|
48
|
+
*/
|
|
49
|
+
function roundAzimuth(azimuth: number, tolerance: number): number {
|
|
50
|
+
return Math.round(azimuth / tolerance) * tolerance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Group cells by site + rounded azimuth
|
|
55
|
+
*/
|
|
56
|
+
function groupCellsByAzimuth(cells: Cell[], tolerance: number): Map<string, Cell[]> {
|
|
57
|
+
const groups = new Map<string, Cell[]>();
|
|
58
|
+
|
|
59
|
+
cells.forEach(cell => {
|
|
60
|
+
const roundedAzimuth = roundAzimuth(cell.azimuth, tolerance);
|
|
61
|
+
const groupKey = `${cell.siteId}:${roundedAzimuth}`;
|
|
62
|
+
|
|
63
|
+
if (!groups.has(groupKey)) {
|
|
64
|
+
groups.set(groupKey, []);
|
|
65
|
+
}
|
|
66
|
+
groups.get(groupKey)!.push(cell);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return groups;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build GeoJSON features for cell labels
|
|
74
|
+
*/
|
|
75
|
+
function buildLabelFeatures(): GeoJSON.FeatureCollection {
|
|
76
|
+
const features: GeoJSON.Feature[] = [];
|
|
77
|
+
|
|
78
|
+
if (!store.showLabels || store.filteredCells.length === 0) {
|
|
79
|
+
return { type: 'FeatureCollection', features: [] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Group cells by site + azimuth
|
|
83
|
+
const cellGroups = groupCellsByAzimuth(store.filteredCells, store.azimuthTolerance);
|
|
84
|
+
|
|
85
|
+
cellGroups.forEach((cells, groupKey) => {
|
|
86
|
+
// Sort cells within group for consistent stacking
|
|
87
|
+
cells.sort((a, b) => a.id.localeCompare(b.id));
|
|
88
|
+
|
|
89
|
+
cells.forEach((cell, index) => {
|
|
90
|
+
// Determine which fields to use based on tech
|
|
91
|
+
const is2G = cell.tech === '2G';
|
|
92
|
+
const primaryField = is2G ? store.primaryLabelField2G : store.primaryLabelField4G5G;
|
|
93
|
+
const secondaryField = is2G ? store.secondaryLabelField2G : store.secondaryLabelField4G5G;
|
|
94
|
+
|
|
95
|
+
// Get label text
|
|
96
|
+
const primaryText = getCellLabelText(cell, primaryField);
|
|
97
|
+
const secondaryText = getCellLabelText(cell, secondaryField);
|
|
98
|
+
|
|
99
|
+
// Build combined label (on single line with separator)
|
|
100
|
+
let labelText = primaryText;
|
|
101
|
+
if (secondaryText) {
|
|
102
|
+
labelText += ` | ${secondaryText}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!labelText.trim()) return; // Skip if no text
|
|
106
|
+
|
|
107
|
+
// Project label position along azimuth
|
|
108
|
+
const origin = turf.point([cell.siteLongitude, cell.siteLatitude]);
|
|
109
|
+
const labelPosition = turf.destination(
|
|
110
|
+
origin,
|
|
111
|
+
store.labelOffset / 1000, // Convert meters to kilometers
|
|
112
|
+
cell.azimuth,
|
|
113
|
+
{ units: 'kilometers' }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Calculate vertical offset for stacking (pixels)
|
|
117
|
+
const stackOffset = index * (store.labelSize + 4);
|
|
118
|
+
|
|
119
|
+
features.push({
|
|
120
|
+
type: 'Feature',
|
|
121
|
+
geometry: labelPosition.geometry,
|
|
122
|
+
properties: {
|
|
123
|
+
text: labelText,
|
|
124
|
+
stackOffset,
|
|
125
|
+
cellId: cell.id
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return { type: 'FeatureCollection', features };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Update label layer on map
|
|
136
|
+
*/
|
|
137
|
+
function updateLabels() {
|
|
138
|
+
if (!map || !map.getSource(sourceId)) return;
|
|
139
|
+
|
|
140
|
+
const geojson = buildLabelFeatures();
|
|
141
|
+
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
142
|
+
source.setData(geojson);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onMount(async () => {
|
|
146
|
+
unsubscribe = mapStore.subscribe(async (m) => {
|
|
147
|
+
if (!m) {
|
|
148
|
+
map = null;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
map = m;
|
|
153
|
+
|
|
154
|
+
// Wait for style to load
|
|
155
|
+
await waitForStyleLoad(map);
|
|
156
|
+
|
|
157
|
+
// Add source
|
|
158
|
+
if (!map.getSource(sourceId)) {
|
|
159
|
+
map.addSource(sourceId, {
|
|
160
|
+
type: 'geojson',
|
|
161
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add label layer
|
|
166
|
+
if (!map.getLayer(layerId)) {
|
|
167
|
+
map.addLayer({
|
|
168
|
+
id: layerId,
|
|
169
|
+
type: 'symbol',
|
|
170
|
+
source: sourceId,
|
|
171
|
+
minzoom: store.minLabelZoom,
|
|
172
|
+
layout: {
|
|
173
|
+
'text-field': ['get', 'text'],
|
|
174
|
+
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
|
|
175
|
+
'text-size': store.labelSize,
|
|
176
|
+
'text-anchor': 'center',
|
|
177
|
+
'text-offset': [
|
|
178
|
+
0,
|
|
179
|
+
['/', ['get', 'stackOffset'], store.labelSize] // Dynamic vertical offset
|
|
180
|
+
],
|
|
181
|
+
'text-allow-overlap': true,
|
|
182
|
+
'text-ignore-placement': false,
|
|
183
|
+
'text-max-width': 999, // Essentially disable wrapping
|
|
184
|
+
'text-justify': 'center' // Center multi-line text
|
|
185
|
+
},
|
|
186
|
+
paint: {
|
|
187
|
+
'text-color': store.labelColor,
|
|
188
|
+
'text-halo-color': store.labelHaloColor,
|
|
189
|
+
'text-halo-width': store.labelHaloWidth
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Initial render
|
|
195
|
+
updateLabels();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Watch for changes in store properties and update labels
|
|
200
|
+
$effect(() => {
|
|
201
|
+
// Dependencies that should trigger label refresh
|
|
202
|
+
store.filteredCells;
|
|
203
|
+
store.showLabels;
|
|
204
|
+
store.primaryLabelField4G5G;
|
|
205
|
+
store.secondaryLabelField4G5G;
|
|
206
|
+
store.primaryLabelField2G;
|
|
207
|
+
store.secondaryLabelField2G;
|
|
208
|
+
store.labelOffset;
|
|
209
|
+
store.azimuthTolerance;
|
|
210
|
+
|
|
211
|
+
updateLabels();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Watch for style changes and update layer paint/layout properties
|
|
215
|
+
$effect(() => {
|
|
216
|
+
if (!map || !map.getLayer(layerId)) return;
|
|
217
|
+
|
|
218
|
+
map.setLayoutProperty(layerId, 'text-size', store.labelSize);
|
|
219
|
+
map.setPaintProperty(layerId, 'text-color', store.labelColor);
|
|
220
|
+
map.setPaintProperty(layerId, 'text-halo-color', store.labelHaloColor);
|
|
221
|
+
map.setPaintProperty(layerId, 'text-halo-width', store.labelHaloWidth);
|
|
222
|
+
map.setLayerZoomRange(layerId, store.minLabelZoom, 24);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
onDestroy(() => {
|
|
226
|
+
unsubscribe?.();
|
|
227
|
+
|
|
228
|
+
if (map) {
|
|
229
|
+
if (map.getLayer(layerId)) {
|
|
230
|
+
map.removeLayer(layerId);
|
|
231
|
+
}
|
|
232
|
+
if (map.getSource(sourceId)) {
|
|
233
|
+
map.removeSource(sourceId);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Cell store context */
|
|
4
|
+
store: CellStoreContext;
|
|
5
|
+
/** Unique namespace for layer/source IDs */
|
|
6
|
+
namespace: string;
|
|
7
|
+
}
|
|
8
|
+
declare const CellLabelsLayer: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type CellLabelsLayer = ReturnType<typeof CellLabelsLayer>;
|
|
10
|
+
export default CellLabelsLayer;
|
|
@@ -13,6 +13,18 @@ export interface CellStoreValue {
|
|
|
13
13
|
lineWidth: number;
|
|
14
14
|
fillOpacity: number;
|
|
15
15
|
baseRadius: number;
|
|
16
|
+
showLabels: boolean;
|
|
17
|
+
primaryLabelField4G5G: keyof Cell;
|
|
18
|
+
secondaryLabelField4G5G: keyof Cell | 'none';
|
|
19
|
+
primaryLabelField2G: keyof Cell;
|
|
20
|
+
secondaryLabelField2G: keyof Cell | 'none';
|
|
21
|
+
labelSize: number;
|
|
22
|
+
labelColor: string;
|
|
23
|
+
labelOffset: number;
|
|
24
|
+
labelHaloColor: string;
|
|
25
|
+
labelHaloWidth: number;
|
|
26
|
+
minLabelZoom: number;
|
|
27
|
+
azimuthTolerance: number;
|
|
16
28
|
statusStyles: Map<CellStatus, CellStatusStyle>;
|
|
17
29
|
groupingConfig: CellTreeConfig;
|
|
18
30
|
groupColorMap: Map<string, string>;
|
|
@@ -32,6 +44,18 @@ export interface CellStoreContext {
|
|
|
32
44
|
readonly groupingConfig: CellTreeConfig;
|
|
33
45
|
readonly groupColorMap: Map<string, string>;
|
|
34
46
|
readonly cellGroupMap: Map<string, string>;
|
|
47
|
+
readonly showLabels: boolean;
|
|
48
|
+
readonly primaryLabelField4G5G: keyof Cell;
|
|
49
|
+
readonly secondaryLabelField4G5G: keyof Cell | 'none';
|
|
50
|
+
readonly primaryLabelField2G: keyof Cell;
|
|
51
|
+
readonly secondaryLabelField2G: keyof Cell | 'none';
|
|
52
|
+
readonly labelSize: number;
|
|
53
|
+
readonly labelColor: string;
|
|
54
|
+
readonly labelOffset: number;
|
|
55
|
+
readonly labelHaloColor: string;
|
|
56
|
+
readonly labelHaloWidth: number;
|
|
57
|
+
readonly minLabelZoom: number;
|
|
58
|
+
readonly azimuthTolerance: number;
|
|
35
59
|
setFilteredCells(cells: Cell[]): void;
|
|
36
60
|
setIncludePlannedCells(value: boolean): void;
|
|
37
61
|
setShowCells(value: boolean): void;
|
|
@@ -46,6 +70,18 @@ export interface CellStoreContext {
|
|
|
46
70
|
getGroupColor(groupKey: string): string | undefined;
|
|
47
71
|
setGroupColor(groupKey: string, color: string): void;
|
|
48
72
|
clearGroupColor(groupKey: string): void;
|
|
73
|
+
setShowLabels(value: boolean): void;
|
|
74
|
+
setPrimaryLabelField4G5G(field: keyof Cell): void;
|
|
75
|
+
setSecondaryLabelField4G5G(field: keyof Cell | 'none'): void;
|
|
76
|
+
setPrimaryLabelField2G(field: keyof Cell): void;
|
|
77
|
+
setSecondaryLabelField2G(field: keyof Cell | 'none'): void;
|
|
78
|
+
setLabelSize(value: number): void;
|
|
79
|
+
setLabelColor(value: string): void;
|
|
80
|
+
setLabelOffset(value: number): void;
|
|
81
|
+
setLabelHaloColor(value: string): void;
|
|
82
|
+
setLabelHaloWidth(value: number): void;
|
|
83
|
+
setMinLabelZoom(value: number): void;
|
|
84
|
+
setAzimuthTolerance(value: number): void;
|
|
49
85
|
}
|
|
50
86
|
/**
|
|
51
87
|
* Create a cell store context with reactive state
|
|
@@ -57,7 +57,20 @@ export function createCellStoreContext(cells) {
|
|
|
57
57
|
groupingConfig: persistedSettings.groupingConfig ?? DEFAULT_CELL_TREE_CONFIG,
|
|
58
58
|
groupColorMap: initialColorMap,
|
|
59
59
|
cellGroupMap: new Map(), // Will be populated when tree is built
|
|
60
|
-
currentZoom: 12 // Default zoom
|
|
60
|
+
currentZoom: 12, // Default zoom
|
|
61
|
+
// Label settings with defaults
|
|
62
|
+
showLabels: persistedSettings.showLabels ?? false,
|
|
63
|
+
primaryLabelField4G5G: (persistedSettings.primaryLabelField4G5G ?? 'fband'),
|
|
64
|
+
secondaryLabelField4G5G: (persistedSettings.secondaryLabelField4G5G ?? 'none'),
|
|
65
|
+
primaryLabelField2G: (persistedSettings.primaryLabelField2G ?? 'bcch'),
|
|
66
|
+
secondaryLabelField2G: (persistedSettings.secondaryLabelField2G ?? 'none'),
|
|
67
|
+
labelSize: persistedSettings.labelSize ?? 12,
|
|
68
|
+
labelColor: persistedSettings.labelColor ?? '#000000',
|
|
69
|
+
labelOffset: persistedSettings.labelOffset ?? 300,
|
|
70
|
+
labelHaloColor: persistedSettings.labelHaloColor ?? '#ffffff',
|
|
71
|
+
labelHaloWidth: persistedSettings.labelHaloWidth ?? 1,
|
|
72
|
+
minLabelZoom: persistedSettings.minLabelZoom ?? 14,
|
|
73
|
+
azimuthTolerance: persistedSettings.azimuthTolerance ?? 5
|
|
61
74
|
});
|
|
62
75
|
// Derived: Filter cells by status based on includePlannedCells flag
|
|
63
76
|
// IMPORTANT: This is a pure $derived - it only READS from state, never writes
|
|
@@ -79,7 +92,20 @@ export function createCellStoreContext(cells) {
|
|
|
79
92
|
fillOpacity: state.fillOpacity,
|
|
80
93
|
baseRadius: state.baseRadius,
|
|
81
94
|
groupingConfig: state.groupingConfig,
|
|
82
|
-
groupColors: groupColorsObj
|
|
95
|
+
groupColors: groupColorsObj,
|
|
96
|
+
// Label settings
|
|
97
|
+
showLabels: state.showLabels,
|
|
98
|
+
primaryLabelField4G5G: state.primaryLabelField4G5G,
|
|
99
|
+
secondaryLabelField4G5G: state.secondaryLabelField4G5G,
|
|
100
|
+
primaryLabelField2G: state.primaryLabelField2G,
|
|
101
|
+
secondaryLabelField2G: state.secondaryLabelField2G,
|
|
102
|
+
labelSize: state.labelSize,
|
|
103
|
+
labelColor: state.labelColor,
|
|
104
|
+
labelOffset: state.labelOffset,
|
|
105
|
+
labelHaloColor: state.labelHaloColor,
|
|
106
|
+
labelHaloWidth: state.labelHaloWidth,
|
|
107
|
+
minLabelZoom: state.minLabelZoom,
|
|
108
|
+
azimuthTolerance: state.azimuthTolerance
|
|
83
109
|
};
|
|
84
110
|
saveSettings(settings);
|
|
85
111
|
});
|
|
@@ -107,6 +133,19 @@ export function createCellStoreContext(cells) {
|
|
|
107
133
|
get groupingConfig() { return state.groupingConfig; },
|
|
108
134
|
get groupColorMap() { return state.groupColorMap; },
|
|
109
135
|
get cellGroupMap() { return state.cellGroupMap; },
|
|
136
|
+
// Label getters
|
|
137
|
+
get showLabels() { return state.showLabels; },
|
|
138
|
+
get primaryLabelField4G5G() { return state.primaryLabelField4G5G; },
|
|
139
|
+
get secondaryLabelField4G5G() { return state.secondaryLabelField4G5G; },
|
|
140
|
+
get primaryLabelField2G() { return state.primaryLabelField2G; },
|
|
141
|
+
get secondaryLabelField2G() { return state.secondaryLabelField2G; },
|
|
142
|
+
get labelSize() { return state.labelSize; },
|
|
143
|
+
get labelColor() { return state.labelColor; },
|
|
144
|
+
get labelOffset() { return state.labelOffset; },
|
|
145
|
+
get labelHaloColor() { return state.labelHaloColor; },
|
|
146
|
+
get labelHaloWidth() { return state.labelHaloWidth; },
|
|
147
|
+
get minLabelZoom() { return state.minLabelZoom; },
|
|
148
|
+
get azimuthTolerance() { return state.azimuthTolerance; },
|
|
110
149
|
// Methods
|
|
111
150
|
setFilteredCells(cells) {
|
|
112
151
|
state.filteredCells = cells;
|
|
@@ -152,6 +191,43 @@ export function createCellStoreContext(cells) {
|
|
|
152
191
|
clearGroupColor(groupKey) {
|
|
153
192
|
state.groupColorMap.delete(groupKey);
|
|
154
193
|
state.groupColorMap = new Map(state.groupColorMap);
|
|
194
|
+
},
|
|
195
|
+
// Label setters
|
|
196
|
+
setShowLabels(value) {
|
|
197
|
+
state.showLabels = value;
|
|
198
|
+
},
|
|
199
|
+
setPrimaryLabelField4G5G(field) {
|
|
200
|
+
state.primaryLabelField4G5G = field;
|
|
201
|
+
},
|
|
202
|
+
setSecondaryLabelField4G5G(field) {
|
|
203
|
+
state.secondaryLabelField4G5G = field;
|
|
204
|
+
},
|
|
205
|
+
setPrimaryLabelField2G(field) {
|
|
206
|
+
state.primaryLabelField2G = field;
|
|
207
|
+
},
|
|
208
|
+
setSecondaryLabelField2G(field) {
|
|
209
|
+
state.secondaryLabelField2G = field;
|
|
210
|
+
},
|
|
211
|
+
setLabelSize(value) {
|
|
212
|
+
state.labelSize = value;
|
|
213
|
+
},
|
|
214
|
+
setLabelColor(value) {
|
|
215
|
+
state.labelColor = value;
|
|
216
|
+
},
|
|
217
|
+
setLabelOffset(value) {
|
|
218
|
+
state.labelOffset = value;
|
|
219
|
+
},
|
|
220
|
+
setLabelHaloColor(value) {
|
|
221
|
+
state.labelHaloColor = value;
|
|
222
|
+
},
|
|
223
|
+
setLabelHaloWidth(value) {
|
|
224
|
+
state.labelHaloWidth = value;
|
|
225
|
+
},
|
|
226
|
+
setMinLabelZoom(value) {
|
|
227
|
+
state.minLabelZoom = value;
|
|
228
|
+
},
|
|
229
|
+
setAzimuthTolerance(value) {
|
|
230
|
+
state.azimuthTolerance = value;
|
|
155
231
|
}
|
|
156
232
|
};
|
|
157
233
|
}
|
|
@@ -82,13 +82,19 @@ export interface ParsedTechBand {
|
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Available fields for grouping cells in the filter tree
|
|
85
|
+
* Now accepts any property key from Cell interface
|
|
85
86
|
*/
|
|
86
|
-
export type CellGroupingField =
|
|
87
|
+
export type CellGroupingField = keyof Cell | 'none';
|
|
88
|
+
/**
|
|
89
|
+
* Optional label map for human-readable field names
|
|
90
|
+
* Maps field keys to display labels
|
|
91
|
+
*/
|
|
92
|
+
export type CellGroupingLabels = Partial<Record<CellGroupingField, string>>;
|
|
87
93
|
/**
|
|
88
94
|
* Configuration for dynamic tree grouping
|
|
89
95
|
*/
|
|
90
96
|
export interface CellTreeConfig {
|
|
91
|
-
/** Primary grouping field (required) */
|
|
97
|
+
/** Primary grouping field (required, cannot be 'none') */
|
|
92
98
|
level1: Exclude<CellGroupingField, 'none'>;
|
|
93
99
|
/** Secondary grouping field (optional, can be 'none') */
|
|
94
100
|
level2: CellGroupingField;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Cell Tree Builder
|
|
3
3
|
*
|
|
4
4
|
* Build hierarchical tree structure for cell filtering
|
|
5
|
-
* Structure:
|
|
5
|
+
* Structure: Configurable hierarchy with any Cell properties
|
|
6
6
|
*/
|
|
7
|
-
import type { Cell, CellTreeConfig } from '../types';
|
|
7
|
+
import type { Cell, CellTreeConfig, CellGroupingLabels } from '../types';
|
|
8
8
|
import type { TreeNode } from '../../../../core/TreeView/tree.model';
|
|
9
9
|
/**
|
|
10
10
|
* Build hierarchical tree from flat cell array with dynamic grouping
|
|
@@ -12,9 +12,10 @@ import type { TreeNode } from '../../../../core/TreeView/tree.model';
|
|
|
12
12
|
* @param cells - Array of cells to build tree from
|
|
13
13
|
* @param config - Grouping configuration (level1, level2)
|
|
14
14
|
* @param colorMap - Optional map of leaf group IDs to custom colors
|
|
15
|
+
* @param labelMap - Optional map of field keys to human-readable labels
|
|
15
16
|
* @returns Object with tree and cell-to-group lookup map
|
|
16
17
|
*/
|
|
17
|
-
export declare function buildCellTree(cells: Cell[], config: CellTreeConfig, colorMap?: Map<string, string
|
|
18
|
+
export declare function buildCellTree(cells: Cell[], config: CellTreeConfig, colorMap?: Map<string, string>, labelMap?: CellGroupingLabels): {
|
|
18
19
|
tree: TreeNode;
|
|
19
20
|
cellGroupMap: Map<string, string>;
|
|
20
21
|
};
|
|
@@ -2,55 +2,54 @@
|
|
|
2
2
|
* Cell Tree Builder
|
|
3
3
|
*
|
|
4
4
|
* Build hierarchical tree structure for cell filtering
|
|
5
|
-
* Structure:
|
|
5
|
+
* Structure: Configurable hierarchy with any Cell properties
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Smart sort comparator that handles numeric suffixes
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* "700", "1800" → sorted as 700, 1800 (numeric)
|
|
12
|
+
* "LTE700", "LTE1800" → sorted as 700, 1800 (numeric suffix)
|
|
13
|
+
* "On_Air", "Planned" → sorted alphabetically
|
|
14
|
+
*
|
|
15
|
+
* @param a - First value to compare
|
|
16
|
+
* @param b - Second value to compare
|
|
17
|
+
* @returns Sort order (-1, 0, 1)
|
|
18
|
+
*/
|
|
19
|
+
function smartSort(a, b) {
|
|
20
|
+
// Extract trailing 3-4 digits
|
|
21
|
+
const numRegex = /(\d{3,4})$/;
|
|
22
|
+
const matchA = a.match(numRegex);
|
|
23
|
+
const matchB = b.match(numRegex);
|
|
24
|
+
// If both have numeric suffixes, compare numerically
|
|
25
|
+
if (matchA && matchB) {
|
|
26
|
+
const numA = parseInt(matchA[1], 10);
|
|
27
|
+
const numB = parseInt(matchB[1], 10);
|
|
28
|
+
return numA - numB;
|
|
29
|
+
}
|
|
30
|
+
// Otherwise alphabetical
|
|
31
|
+
return a.localeCompare(b);
|
|
32
|
+
}
|
|
7
33
|
/**
|
|
8
34
|
* Get the value of a grouping field from a cell
|
|
9
35
|
*/
|
|
10
36
|
function getGroupingValue(cell, field) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return cell.siteId;
|
|
20
|
-
case 'customSubgroup':
|
|
21
|
-
return cell.customSubgroup || 'Ungrouped';
|
|
22
|
-
case 'type':
|
|
23
|
-
return cell.type;
|
|
24
|
-
case 'planner':
|
|
25
|
-
return cell.planner || 'Unassigned';
|
|
26
|
-
case 'none':
|
|
27
|
-
return '';
|
|
28
|
-
default:
|
|
29
|
-
return 'Unknown';
|
|
30
|
-
}
|
|
37
|
+
if (field === 'none')
|
|
38
|
+
return '';
|
|
39
|
+
const value = cell[field];
|
|
40
|
+
// Handle null/undefined
|
|
41
|
+
if (value == null)
|
|
42
|
+
return 'Unassigned';
|
|
43
|
+
// Convert to string
|
|
44
|
+
return String(value);
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* Get a human-readable label for a grouping field value
|
|
34
48
|
*/
|
|
35
|
-
function getGroupLabel(field, value, count) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
case 'band':
|
|
40
|
-
return `${value} MHz (${count})`;
|
|
41
|
-
case 'status':
|
|
42
|
-
return `${value} (${count})`;
|
|
43
|
-
case 'siteId':
|
|
44
|
-
return `${value} (${count})`;
|
|
45
|
-
case 'customSubgroup':
|
|
46
|
-
return `${value} (${count})`;
|
|
47
|
-
case 'type':
|
|
48
|
-
return `${value} (${count})`;
|
|
49
|
-
case 'planner':
|
|
50
|
-
return `${value} (${count})`;
|
|
51
|
-
default:
|
|
52
|
-
return `${value} (${count})`;
|
|
53
|
-
}
|
|
49
|
+
function getGroupLabel(field, value, count, labelMap) {
|
|
50
|
+
// Use custom label if provided
|
|
51
|
+
const fieldLabel = labelMap?.[field] || String(field);
|
|
52
|
+
return `${value} (${count})`;
|
|
54
53
|
}
|
|
55
54
|
/**
|
|
56
55
|
* Generate a unique node ID based on grouping path
|
|
@@ -67,21 +66,22 @@ function generateNodeId(field, value, parentId) {
|
|
|
67
66
|
* @param cells - Array of cells to build tree from
|
|
68
67
|
* @param config - Grouping configuration (level1, level2)
|
|
69
68
|
* @param colorMap - Optional map of leaf group IDs to custom colors
|
|
69
|
+
* @param labelMap - Optional map of field keys to human-readable labels
|
|
70
70
|
* @returns Object with tree and cell-to-group lookup map
|
|
71
71
|
*/
|
|
72
|
-
export function buildCellTree(cells, config, colorMap) {
|
|
72
|
+
export function buildCellTree(cells, config, colorMap, labelMap) {
|
|
73
73
|
const { level1, level2 } = config;
|
|
74
74
|
// If level2 is 'none', create flat 2-level tree
|
|
75
75
|
if (level2 === 'none') {
|
|
76
|
-
return buildFlatTree(cells, level1, colorMap);
|
|
76
|
+
return buildFlatTree(cells, level1, colorMap, labelMap);
|
|
77
77
|
}
|
|
78
78
|
// Otherwise, create nested 3-level tree
|
|
79
|
-
return buildNestedTree(cells, level1, level2, colorMap);
|
|
79
|
+
return buildNestedTree(cells, level1, level2, colorMap, labelMap);
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Build flat 2-level tree (Root → Level1 groups)
|
|
83
83
|
*/
|
|
84
|
-
function buildFlatTree(cells, level1, colorMap) {
|
|
84
|
+
function buildFlatTree(cells, level1, colorMap, labelMap) {
|
|
85
85
|
// Group cells by level1 field
|
|
86
86
|
const groups = new Map();
|
|
87
87
|
const cellGroupMap = new Map();
|
|
@@ -92,8 +92,8 @@ function buildFlatTree(cells, level1, colorMap) {
|
|
|
92
92
|
}
|
|
93
93
|
groups.get(value).push(cell);
|
|
94
94
|
});
|
|
95
|
-
// Sort groups
|
|
96
|
-
const sortedGroups = Array.from(groups.entries()).sort(([a], [b]) => a
|
|
95
|
+
// Sort groups with smart numeric/alphabetical sorting
|
|
96
|
+
const sortedGroups = Array.from(groups.entries()).sort(([a], [b]) => smartSort(a, b));
|
|
97
97
|
// Build tree nodes and populate cellGroupMap
|
|
98
98
|
const children = sortedGroups.map(([value, groupCells]) => {
|
|
99
99
|
const nodeId = generateNodeId(level1, value);
|
|
@@ -105,7 +105,7 @@ function buildFlatTree(cells, level1, colorMap) {
|
|
|
105
105
|
});
|
|
106
106
|
return {
|
|
107
107
|
id: nodeId,
|
|
108
|
-
label: getGroupLabel(level1, value, groupCells.length),
|
|
108
|
+
label: getGroupLabel(level1, value, groupCells.length, labelMap),
|
|
109
109
|
defaultChecked: true,
|
|
110
110
|
children: [],
|
|
111
111
|
metadata: {
|
|
@@ -131,7 +131,7 @@ function buildFlatTree(cells, level1, colorMap) {
|
|
|
131
131
|
/**
|
|
132
132
|
* Build nested 3-level tree (Root → Level1 → Level2 groups)
|
|
133
133
|
*/
|
|
134
|
-
function buildNestedTree(cells, level1, level2, colorMap) {
|
|
134
|
+
function buildNestedTree(cells, level1, level2, colorMap, labelMap) {
|
|
135
135
|
// Group cells by level1, then by level2
|
|
136
136
|
const level1Groups = new Map();
|
|
137
137
|
const cellGroupMap = new Map();
|
|
@@ -147,13 +147,13 @@ function buildNestedTree(cells, level1, level2, colorMap) {
|
|
|
147
147
|
}
|
|
148
148
|
level2Groups.get(value2).push(cell);
|
|
149
149
|
});
|
|
150
|
-
// Sort level1 groups
|
|
151
|
-
const sortedLevel1 = Array.from(level1Groups.entries()).sort(([a], [b]) => a
|
|
150
|
+
// Sort level1 groups with smart numeric/alphabetical sorting
|
|
151
|
+
const sortedLevel1 = Array.from(level1Groups.entries()).sort(([a], [b]) => smartSort(a, b));
|
|
152
152
|
// Build tree nodes and populate cellGroupMap
|
|
153
153
|
const children = sortedLevel1.map(([value1, level2Groups]) => {
|
|
154
154
|
const parentId = generateNodeId(level1, value1);
|
|
155
|
-
// Sort level2 groups
|
|
156
|
-
const sortedLevel2 = Array.from(level2Groups.entries()).sort(([a], [b]) => a
|
|
155
|
+
// Sort level2 groups with smart numeric/alphabetical sorting
|
|
156
|
+
const sortedLevel2 = Array.from(level2Groups.entries()).sort(([a], [b]) => smartSort(a, b));
|
|
157
157
|
const level2Children = sortedLevel2.map(([value2, groupCells]) => {
|
|
158
158
|
const nodeId = generateNodeId(level2, value2, parentId);
|
|
159
159
|
const color = colorMap?.get(nodeId);
|
|
@@ -164,7 +164,7 @@ function buildNestedTree(cells, level1, level2, colorMap) {
|
|
|
164
164
|
});
|
|
165
165
|
return {
|
|
166
166
|
id: nodeId,
|
|
167
|
-
label: getGroupLabel(level2, value2, groupCells.length),
|
|
167
|
+
label: getGroupLabel(level2, value2, groupCells.length, labelMap),
|
|
168
168
|
defaultChecked: true,
|
|
169
169
|
children: [],
|
|
170
170
|
metadata: {
|
|
@@ -182,7 +182,7 @@ function buildNestedTree(cells, level1, level2, colorMap) {
|
|
|
182
182
|
const totalCells = Array.from(level2Groups.values()).flat().length;
|
|
183
183
|
return {
|
|
184
184
|
id: parentId,
|
|
185
|
-
label: getGroupLabel(level1, value1, totalCells),
|
|
185
|
+
label: getGroupLabel(level1, value1, totalCells, labelMap),
|
|
186
186
|
defaultChecked: true,
|
|
187
187
|
children: level2Children,
|
|
188
188
|
metadata: {
|
package/dist/map-v2/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* A decoupled, feature-based architecture for Mapbox cellular network visualization.
|
|
5
5
|
* Each feature (sites, cells) is completely independent with its own store, layers, and controls.
|
|
6
6
|
*/
|
|
7
|
-
export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStyleControl, createMapStore, createViewportStore, type ViewportStore, type ViewportState, useMapbox, tryUseMapbox } from './core';
|
|
7
|
+
export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyleControl, createMapStore, createViewportStore, type ViewportStore, type ViewportState, useMapbox, tryUseMapbox } from './core';
|
|
8
8
|
export { MapControl, 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
|
-
export { type Cell, type CellStatus, type CellStatusStyle, type CellGroupingField, type CellTreeConfig, type TechnologyBandKey, type CellStoreValue, type CellStoreContext, createCellStoreContext, CellsLayer, CellFilterControl, CellStyleControl, cellsToGeoJSON, buildCellTree, getFilteredCells, calculateRadius, getZoomFactor, createArcPolygon, DEFAULT_CELL_TREE_CONFIG, TECHNOLOGY_BAND_COLORS, DEFAULT_STATUS_STYLES, RADIUS_MULTIPLIER } from './features/cells';
|
|
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
// CORE INFRASTRUCTURE
|
|
9
9
|
// ============================================================================
|
|
10
|
-
export { MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStyleControl, createMapStore, createViewportStore, useMapbox, tryUseMapbox } from './core';
|
|
10
|
+
export { MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyleControl, createMapStore, createViewportStore, useMapbox, tryUseMapbox } from './core';
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// SHARED UTILITIES
|
|
13
13
|
// ============================================================================
|
|
@@ -19,7 +19,7 @@ export { createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl,
|
|
|
19
19
|
// ============================================================================
|
|
20
20
|
// CELL FEATURE
|
|
21
21
|
// ============================================================================
|
|
22
|
-
export { createCellStoreContext, CellsLayer, CellFilterControl, CellStyleControl, cellsToGeoJSON, buildCellTree, getFilteredCells, calculateRadius, getZoomFactor, createArcPolygon, DEFAULT_CELL_TREE_CONFIG, TECHNOLOGY_BAND_COLORS, DEFAULT_STATUS_STYLES, RADIUS_MULTIPLIER } from './features/cells';
|
|
22
|
+
export { 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';
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// DEMO
|
|
25
25
|
// ============================================================================
|