@smartnet360/svelte-components 0.0.96 → 0.0.98
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/apps/site-check/SiteCheck.svelte +53 -14
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +1 -0
- package/dist/apps/site-check/helper.d.ts +1 -1
- package/dist/apps/site-check/helper.js +5 -5
- package/dist/apps/site-check/transforms.d.ts +1 -1
- package/dist/apps/site-check/transforms.js +3 -3
- package/dist/core/TreeView/tree.model.d.ts +2 -0
- package/dist/core/TreeView/tree.store.js +15 -0
- package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte +40 -26
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +36 -42
- package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +2 -1
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +8 -4
- package/package.json +1 -1
|
@@ -17,18 +17,19 @@
|
|
|
17
17
|
baseMetrics: string[];
|
|
18
18
|
mode: Mode;
|
|
19
19
|
markers?: ChartMarker[];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
cellStyling?: CellStylingConfig; // Optional cell styling config (defaults to defaultCellStyling)
|
|
21
|
+
initialGrouping?: TreeGroupingConfig; // Optional initial tree grouping (defaults to Site → Azimuth → Cell)
|
|
22
|
+
showGroupingSelector?: boolean; // Show/hide the grouping dropdown (default: true)
|
|
23
|
+
useSectorLineStyles?: boolean; // Enable sector-based line style differentiation (default: false)
|
|
24
|
+
onSearch?: (searchTerm: string) => void; // Optional: Search callback (if provided, shows search box)
|
|
25
|
+
searchPlaceholder?: string; // Optional: Search box placeholder text (default: "Search...")
|
|
26
|
+
plotlyLayout?: Record<string, any>; // Optional Plotly layout configuration
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
let { rawData, multiCellLayout, singleLteLayout,
|
|
29
30
|
singleNrLayout, baseMetrics, mode = "scrollspy", markers = [],
|
|
30
31
|
cellStyling = defaultCellStyling, initialGrouping = defaultTreeGrouping,
|
|
31
|
-
showGroupingSelector = true, onSearch, searchPlaceholder = "Search...", plotlyLayout }: Props = $props();
|
|
32
|
+
showGroupingSelector = true, useSectorLineStyles = false, onSearch, searchPlaceholder = "Search...", plotlyLayout }: Props = $props();
|
|
32
33
|
|
|
33
34
|
// Search state
|
|
34
35
|
let searchTerm = $state('');
|
|
@@ -61,6 +62,9 @@
|
|
|
61
62
|
// Color dimension state (defaults to 'band' for semantic RF characteristics)
|
|
62
63
|
let colorDimension = $state<ColorDimension>('band');
|
|
63
64
|
|
|
65
|
+
// Single root select mode - only one Level 0 node at a time (radio behavior)
|
|
66
|
+
let singleRootSelect = $state(false);
|
|
67
|
+
|
|
64
68
|
// Available field options for grouping levels
|
|
65
69
|
const fieldOptions: { value: TreeGroupField; label: string }[] = [
|
|
66
70
|
{ value: 'site', label: 'Site' },
|
|
@@ -92,10 +96,10 @@
|
|
|
92
96
|
return fieldOptions.filter(opt => opt.value !== treeGrouping.level0);
|
|
93
97
|
}); let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
94
98
|
|
|
95
|
-
// Rebuild tree whenever treeGrouping changes
|
|
99
|
+
// Rebuild tree whenever treeGrouping or singleRootSelect changes
|
|
96
100
|
$effect(() => {
|
|
97
101
|
|
|
98
|
-
log('
|
|
102
|
+
log('🔄 Rebuilding tree with grouping', { treeGrouping, singleRootSelect });
|
|
99
103
|
|
|
100
104
|
// Clear any existing localStorage data to prevent stale state
|
|
101
105
|
const storageKey = 'site-check:treeState';
|
|
@@ -119,11 +123,13 @@
|
|
|
119
123
|
nodes: treeNodes,
|
|
120
124
|
namespace: 'site-check',
|
|
121
125
|
persistState: false, // Don't persist when grouping changes dynamically
|
|
122
|
-
defaultExpandAll: false
|
|
126
|
+
defaultExpandAll: false,
|
|
127
|
+
singleRootSelect // Pass single root select mode
|
|
123
128
|
});
|
|
124
129
|
log('✅ Tree Store Created', {
|
|
125
130
|
namespace: 'site-check',
|
|
126
|
-
grouping: treeGrouping
|
|
131
|
+
grouping: treeGrouping,
|
|
132
|
+
singleRootSelect
|
|
127
133
|
});
|
|
128
134
|
});
|
|
129
135
|
|
|
@@ -157,9 +163,9 @@
|
|
|
157
163
|
|
|
158
164
|
// Expand layout based on selected cells and chosen base layout
|
|
159
165
|
let chartLayout = $derived.by(() => {
|
|
160
|
-
// Pass cellStyling, treeGrouping, and
|
|
161
|
-
// and generate appropriate labels based on grouping and
|
|
162
|
-
const expanded = expandLayoutForCells(selectedBaseLayout, filteredData, treeGrouping, colorDimension, cellStyling);
|
|
166
|
+
// Pass cellStyling, treeGrouping, colorDimension, and useSectorLineStyles - helper will decide per-section whether to use styling,
|
|
167
|
+
// and generate appropriate labels based on grouping, colors based on colorDimension, and line styles based on useSectorLineStyles
|
|
168
|
+
const expanded = expandLayoutForCells(selectedBaseLayout, filteredData, treeGrouping, colorDimension, useSectorLineStyles, cellStyling);
|
|
163
169
|
log('📐 Chart Layout:', {
|
|
164
170
|
layoutName: selectedBaseLayout.layoutName,
|
|
165
171
|
layoutDefaultColors: selectedBaseLayout.useDefaultChartColors ?? false,
|
|
@@ -359,6 +365,39 @@
|
|
|
359
365
|
</div>
|
|
360
366
|
</div>
|
|
361
367
|
|
|
368
|
+
<!-- Single Root Select Toggle -->
|
|
369
|
+
<div class="form-check mt-2">
|
|
370
|
+
<input
|
|
371
|
+
class="form-check-input"
|
|
372
|
+
type="checkbox"
|
|
373
|
+
id="singleRootSelectCheck"
|
|
374
|
+
checked={singleRootSelect}
|
|
375
|
+
onchange={(e) => {
|
|
376
|
+
singleRootSelect = e.currentTarget.checked;
|
|
377
|
+
log('🔘 Single root select mode:', singleRootSelect);
|
|
378
|
+
|
|
379
|
+
// When enabling single root mode, uncheck all roots except the first one
|
|
380
|
+
if (singleRootSelect && treeStore) {
|
|
381
|
+
const store = $treeStore;
|
|
382
|
+
if (store) {
|
|
383
|
+
const checkedRoots = store.state.rootPaths.filter(path =>
|
|
384
|
+
store.state.checkedPaths.has(path)
|
|
385
|
+
);
|
|
386
|
+
if (checkedRoots.length > 1) {
|
|
387
|
+
log('🔘 Multiple roots selected, keeping only first one:', checkedRoots[0]);
|
|
388
|
+
// Uncheck all except the first
|
|
389
|
+
for (let i = 1; i < checkedRoots.length; i++) {
|
|
390
|
+
store.toggle(checkedRoots[i]);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
397
|
+
<label class="form-check-label small" for="singleRootSelectCheck">
|
|
398
|
+
Single selection on level 0
|
|
399
|
+
</label>
|
|
400
|
+
</div>
|
|
362
401
|
</div>
|
|
363
402
|
{/if}
|
|
364
403
|
{/if} <!-- Tree View -->
|
|
@@ -12,6 +12,7 @@ interface Props {
|
|
|
12
12
|
cellStyling?: CellStylingConfig;
|
|
13
13
|
initialGrouping?: TreeGroupingConfig;
|
|
14
14
|
showGroupingSelector?: boolean;
|
|
15
|
+
useSectorLineStyles?: boolean;
|
|
15
16
|
onSearch?: (searchTerm: string) => void;
|
|
16
17
|
searchPlaceholder?: string;
|
|
17
18
|
plotlyLayout?: Record<string, any>;
|
|
@@ -13,7 +13,7 @@ import { type StackGroupMode } from './transforms.js';
|
|
|
13
13
|
* @param stackGroupMode - Optional stackgroup strategy for stacked charts (default: 'none' = single stack)
|
|
14
14
|
* @returns Expanded layout with cell-specific KPIs
|
|
15
15
|
*/
|
|
16
|
-
export declare function expandLayoutForCells(baseLayout: Layout, data: CellTrafficRecord[], grouping: TreeGroupingConfig, colorDimension: ColorDimension, stylingConfig?: CellStylingConfig, stackGroupMode?: StackGroupMode): Layout;
|
|
16
|
+
export declare function expandLayoutForCells(baseLayout: Layout, data: CellTrafficRecord[], grouping: TreeGroupingConfig, colorDimension: ColorDimension, useSectorLineStyles: boolean, stylingConfig?: CellStylingConfig, stackGroupMode?: StackGroupMode): Layout;
|
|
17
17
|
/**
|
|
18
18
|
* Extract base metric names from a layout configuration
|
|
19
19
|
* Returns unique metric rawNames that need to be pivoted
|
|
@@ -11,7 +11,7 @@ import { createStyledKPI, sortCellsByBandFrequency, assignStackGroups } from './
|
|
|
11
11
|
* @param stackGroupMode - Optional stackgroup strategy for stacked charts (default: 'none' = single stack)
|
|
12
12
|
* @returns Expanded layout with cell-specific KPIs
|
|
13
13
|
*/
|
|
14
|
-
export function expandLayoutForCells(baseLayout, data, grouping, colorDimension, stylingConfig, stackGroupMode = 'none') {
|
|
14
|
+
export function expandLayoutForCells(baseLayout, data, grouping, colorDimension, useSectorLineStyles, stylingConfig, stackGroupMode = 'none') {
|
|
15
15
|
// Get unique cells and their metadata, sorted by band frequency
|
|
16
16
|
const cellMap = new Map();
|
|
17
17
|
data.forEach((record) => {
|
|
@@ -39,8 +39,8 @@ export function expandLayoutForCells(baseLayout, data, grouping, colorDimension,
|
|
|
39
39
|
...section,
|
|
40
40
|
charts: section.charts.map((chart) => ({
|
|
41
41
|
...chart,
|
|
42
|
-
yLeft: expandKPIs(chart.yLeft, cells, grouping, colorDimension, effectiveStyling, stackGroupMode),
|
|
43
|
-
yRight: expandKPIs(chart.yRight, cells, grouping, colorDimension, effectiveStyling, stackGroupMode)
|
|
42
|
+
yLeft: expandKPIs(chart.yLeft, cells, grouping, colorDimension, useSectorLineStyles, effectiveStyling, stackGroupMode),
|
|
43
|
+
yRight: expandKPIs(chart.yRight, cells, grouping, colorDimension, useSectorLineStyles, effectiveStyling, stackGroupMode)
|
|
44
44
|
}))
|
|
45
45
|
};
|
|
46
46
|
})
|
|
@@ -60,13 +60,13 @@ export function expandLayoutForCells(baseLayout, data, grouping, colorDimension,
|
|
|
60
60
|
* @param stackGroupMode - Stackgroup strategy for this set of KPIs
|
|
61
61
|
* @returns Expanded array of KPIs (styled or default, with stackgroups assigned)
|
|
62
62
|
*/
|
|
63
|
-
function expandKPIs(baseKPIs, cells, grouping, colorDimension, stylingConfig, stackGroupMode = 'none') {
|
|
63
|
+
function expandKPIs(baseKPIs, cells, grouping, colorDimension, useSectorLineStyles, stylingConfig, stackGroupMode = 'none') {
|
|
64
64
|
let expandedKPIs = [];
|
|
65
65
|
baseKPIs.forEach((baseKPI) => {
|
|
66
66
|
cells.forEach(([cellName, record]) => {
|
|
67
67
|
if (stylingConfig) {
|
|
68
68
|
// Apply custom styling (band colors, sector line styles)
|
|
69
|
-
const styledKPI = createStyledKPI(baseKPI.rawName, record, baseKPI.unit, grouping, colorDimension, stylingConfig);
|
|
69
|
+
const styledKPI = createStyledKPI(baseKPI.rawName, record, baseKPI.unit, grouping, colorDimension, useSectorLineStyles, stylingConfig);
|
|
70
70
|
expandedKPIs.push({
|
|
71
71
|
...styledKPI,
|
|
72
72
|
stackGroup: undefined // Initialize for treeshake-safe property assignment
|
|
@@ -84,4 +84,4 @@ export declare function transformChartData(data: CellTrafficRecord[], baseMetric
|
|
|
84
84
|
* @param stylingConfig - Optional styling configuration
|
|
85
85
|
* @returns KPI with cell-specific styling applied
|
|
86
86
|
*/
|
|
87
|
-
export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, grouping: TreeGroupingConfig, colorDimension: ColorDimension, stylingConfig?: CellStylingConfig): KPI;
|
|
87
|
+
export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, grouping: TreeGroupingConfig, colorDimension: ColorDimension, useSectorLineStyles: boolean, stylingConfig?: CellStylingConfig): KPI;
|
|
@@ -483,7 +483,7 @@ function generateSiteColor(siteName) {
|
|
|
483
483
|
* @param stylingConfig - Optional styling configuration
|
|
484
484
|
* @returns KPI with cell-specific styling applied
|
|
485
485
|
*/
|
|
486
|
-
export function createStyledKPI(metricName, cellRecord, unit, grouping, colorDimension, stylingConfig) {
|
|
486
|
+
export function createStyledKPI(metricName, cellRecord, unit, grouping, colorDimension, useSectorLineStyles, stylingConfig) {
|
|
487
487
|
const { band, sector, azimuth, cellName, siteName } = cellRecord;
|
|
488
488
|
// Determine color based on colorDimension
|
|
489
489
|
let color;
|
|
@@ -495,8 +495,8 @@ export function createStyledKPI(metricName, cellRecord, unit, grouping, colorDim
|
|
|
495
495
|
// Generate consistent color for site
|
|
496
496
|
color = generateSiteColor(siteName);
|
|
497
497
|
}
|
|
498
|
-
// Get line style from sector
|
|
499
|
-
const lineStyle = stylingConfig?.sectorLineStyles?.[sector.toString()];
|
|
498
|
+
// Get line style from sector only if explicitly enabled
|
|
499
|
+
const lineStyle = useSectorLineStyles ? stylingConfig?.sectorLineStyles?.[sector.toString()] : undefined;
|
|
500
500
|
// Generate label based on tree grouping configuration
|
|
501
501
|
const displayName = generateAdaptiveLabel(cellRecord, grouping);
|
|
502
502
|
// Build KPI with cell-specific styling
|
|
@@ -74,6 +74,8 @@ export interface TreeConfig<T = any> {
|
|
|
74
74
|
persistState?: boolean;
|
|
75
75
|
/** Show indeterminate checkbox states */
|
|
76
76
|
showIndeterminate?: boolean;
|
|
77
|
+
/** Single root selection mode - only one root node can be checked at a time (radio behavior) */
|
|
78
|
+
singleRootSelect?: boolean;
|
|
77
79
|
}
|
|
78
80
|
/**
|
|
79
81
|
* Store value exposed to consumers
|
|
@@ -87,6 +87,21 @@ export function createTreeStore(config) {
|
|
|
87
87
|
const newChecked = !state.checkedPaths.has(path);
|
|
88
88
|
const newCheckedPaths = new Set(state.checkedPaths);
|
|
89
89
|
log('📌 Toggle action', { path, newChecked });
|
|
90
|
+
// STEP 0: If singleRootSelect mode and this is a root node being checked, uncheck all other roots
|
|
91
|
+
if (config.singleRootSelect && newChecked && nodeState.level === 0) {
|
|
92
|
+
log('🔘 Single root select mode: unchecking other roots', { path });
|
|
93
|
+
// Uncheck all root nodes and their descendants
|
|
94
|
+
state.rootPaths.forEach(rootPath => {
|
|
95
|
+
if (rootPath !== path) {
|
|
96
|
+
newCheckedPaths.delete(rootPath);
|
|
97
|
+
// Also uncheck all descendants of this root
|
|
98
|
+
const rootDescendants = getDescendantPaths(rootPath, state.nodes, separator);
|
|
99
|
+
rootDescendants.forEach(descendantPath => {
|
|
100
|
+
newCheckedPaths.delete(descendantPath);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
90
105
|
// STEP 1: Update this node
|
|
91
106
|
if (newChecked) {
|
|
92
107
|
newCheckedPaths.add(path);
|
|
@@ -84,59 +84,73 @@
|
|
|
84
84
|
|
|
85
85
|
<!-- Auto Size -->
|
|
86
86
|
<div class="row align-items-center g-2 mb-3">
|
|
87
|
-
<div class="col-4 text-secondary fw-semibold small text-uppercase">
|
|
88
|
-
<div class="col-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
<div class="col-4 text-secondary fw-semibold small text-uppercase">Density Caps</div>
|
|
88
|
+
<div class="col-8">
|
|
89
|
+
<div class="d-flex gap-3">
|
|
90
|
+
<div class="form-check form-switch m-0">
|
|
91
|
+
<input
|
|
92
|
+
id="cell-mincap-toggle"
|
|
93
|
+
type="checkbox"
|
|
94
|
+
class="form-check-input"
|
|
95
|
+
role="switch"
|
|
96
|
+
bind:checked={displayStore.useMinCap}
|
|
97
|
+
/>
|
|
98
|
+
<label class="form-check-label small text-secondary" for="cell-mincap-toggle">
|
|
99
|
+
Min
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="form-check form-switch m-0">
|
|
103
|
+
<input
|
|
104
|
+
id="cell-maxcap-toggle"
|
|
105
|
+
type="checkbox"
|
|
106
|
+
class="form-check-input"
|
|
107
|
+
role="switch"
|
|
108
|
+
bind:checked={displayStore.useMaxCap}
|
|
109
|
+
/>
|
|
110
|
+
<label class="form-check-label small text-secondary" for="cell-maxcap-toggle">
|
|
111
|
+
Max
|
|
112
|
+
</label>
|
|
113
|
+
</div>
|
|
98
114
|
</div>
|
|
99
115
|
</div>
|
|
100
116
|
</div>
|
|
101
117
|
|
|
102
|
-
{#if displayStore.
|
|
103
|
-
<!--
|
|
118
|
+
{#if displayStore.useMinCap || displayStore.useMaxCap}
|
|
119
|
+
<!-- Cap Mode -->
|
|
104
120
|
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
105
|
-
<div class="col-4 text-secondary small">Mode</div>
|
|
121
|
+
<div class="col-4 text-secondary small">Cap Mode</div>
|
|
106
122
|
<div class="col-8">
|
|
107
123
|
<select
|
|
108
124
|
class="form-select form-select-sm"
|
|
109
125
|
bind:value={displayStore.autoSizeMode}
|
|
110
126
|
>
|
|
111
|
-
<option value="logarithmic">Logarithmic
|
|
112
|
-
<option value="percentage">Proportional
|
|
113
|
-
<option value="tiered">Tiered
|
|
114
|
-
<option value="hybrid">Hybrid
|
|
127
|
+
<option value="logarithmic">Logarithmic</option>
|
|
128
|
+
<option value="percentage">Proportional</option>
|
|
129
|
+
<option value="tiered">Tiered</option>
|
|
130
|
+
<option value="hybrid">Hybrid</option>
|
|
115
131
|
</select>
|
|
116
132
|
</div>
|
|
117
133
|
</div>
|
|
118
134
|
|
|
119
|
-
<!--
|
|
135
|
+
<!-- Cap Base -->
|
|
120
136
|
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
121
|
-
<div class="col-4 text-secondary small">Base
|
|
137
|
+
<div class="col-4 text-secondary small">Cap Base</div>
|
|
122
138
|
<div class="col-3 text-end">
|
|
123
139
|
<span class="badge bg-white text-muted border">{displayStore.autoSizeBase.toFixed(1)}x</span>
|
|
124
140
|
</div>
|
|
125
141
|
<div class="col-5">
|
|
126
142
|
<input
|
|
127
|
-
id="cell-
|
|
143
|
+
id="cell-cap-base-slider"
|
|
128
144
|
type="range"
|
|
129
145
|
class="form-range w-100"
|
|
130
|
-
min="0.
|
|
131
|
-
max="
|
|
146
|
+
min="0.5"
|
|
147
|
+
max="3.0"
|
|
132
148
|
step="0.1"
|
|
133
149
|
bind:value={displayStore.autoSizeBase}
|
|
134
150
|
/>
|
|
135
151
|
</div>
|
|
136
152
|
</div>
|
|
137
|
-
{/if}
|
|
138
|
-
|
|
139
|
-
<div class="border-top my-3"></div>
|
|
153
|
+
{/if} <div class="border-top my-3"></div>
|
|
140
154
|
|
|
141
155
|
<!-- Show Labels -->
|
|
142
156
|
<div class="row align-items-center g-2 mb-3">
|
|
@@ -100,14 +100,10 @@
|
|
|
100
100
|
// Initial setup
|
|
101
101
|
addLayers();
|
|
102
102
|
|
|
103
|
-
// Events for updating -
|
|
103
|
+
// Events for updating - always listen to zoom/move
|
|
104
104
|
map.on('style.load', addLayers);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!displayStore.useAutoSize) {
|
|
108
|
-
map.on('moveend', updateLayer);
|
|
109
|
-
map.on('zoomend', updateLayer);
|
|
110
|
-
}
|
|
105
|
+
map.on('moveend', updateLayer);
|
|
106
|
+
map.on('zoomend', updateLayer);
|
|
111
107
|
|
|
112
108
|
// Cleanup
|
|
113
109
|
return () => {
|
|
@@ -131,9 +127,10 @@
|
|
|
131
127
|
const _l1 = displayStore.level1;
|
|
132
128
|
const _l2 = displayStore.level2;
|
|
133
129
|
const _layerGrouping = displayStore.layerGrouping;
|
|
134
|
-
const _useAutoSize = displayStore.useAutoSize;
|
|
135
130
|
const _autoSizeMode = displayStore.autoSizeMode;
|
|
136
131
|
const _autoSizeBase = displayStore.autoSizeBase;
|
|
132
|
+
const _useMinCap = displayStore.useMinCap;
|
|
133
|
+
const _useMaxCap = displayStore.useMaxCap;
|
|
137
134
|
|
|
138
135
|
updateLayer();
|
|
139
136
|
});
|
|
@@ -149,82 +146,79 @@
|
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
function renderCells(map: mapboxgl.Map) {
|
|
149
|
+
const bounds = map.getBounds();
|
|
150
|
+
if (!bounds) return;
|
|
151
|
+
|
|
152
152
|
const zoom = map.getZoom();
|
|
153
153
|
const centerLat = map.getCenter().lat;
|
|
154
154
|
|
|
155
155
|
console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
|
|
156
156
|
|
|
157
|
-
//
|
|
157
|
+
// Calculate base radius from pixel size
|
|
158
158
|
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
159
|
-
|
|
160
|
-
if (!displayStore.useAutoSize) {
|
|
161
|
-
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
162
|
-
}
|
|
159
|
+
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
163
160
|
|
|
164
|
-
//
|
|
165
|
-
// In real app, this comes from a store
|
|
161
|
+
// Group cells
|
|
166
162
|
const groups = groupCells(dataStore.filteredCells, displayStore.level1, displayStore.level2);
|
|
167
163
|
console.log(`[CellsLayer] Groups: ${groups.size}`);
|
|
168
164
|
|
|
169
165
|
const features: GeoJSON.Feature[] = [];
|
|
170
166
|
let groupIndex = 0;
|
|
171
167
|
|
|
172
|
-
//
|
|
168
|
+
// Iterate groups and generate features
|
|
173
169
|
for (const [groupId, cells] of groups) {
|
|
174
|
-
// Get style from registry
|
|
175
170
|
const defaultColor = getColorForGroup(groupIndex++);
|
|
176
171
|
const style = registry.getStyle(groupId, defaultColor);
|
|
177
172
|
|
|
178
173
|
if (!style.visible) continue;
|
|
179
174
|
|
|
180
175
|
for (const cell of cells) {
|
|
181
|
-
//
|
|
182
|
-
if (!
|
|
183
|
-
|
|
184
|
-
if (!bounds || !bounds.contains([cell.longitude, cell.latitude])) {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
176
|
+
// Viewport filter
|
|
177
|
+
if (!bounds.contains([cell.longitude, cell.latitude])) {
|
|
178
|
+
continue;
|
|
187
179
|
}
|
|
188
180
|
|
|
189
|
-
//
|
|
181
|
+
// Z-Index Lookup
|
|
190
182
|
const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
|
|
191
183
|
const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
|
|
192
184
|
|
|
193
|
-
//
|
|
185
|
+
// Calculate radius with z-index scaling
|
|
194
186
|
const MAX_Z = 35;
|
|
195
|
-
|
|
187
|
+
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
188
|
+
let radiusMeters = baseRadiusMeters * scaleFactor;
|
|
196
189
|
|
|
197
|
-
if
|
|
198
|
-
|
|
190
|
+
// Apply density-based caps if enabled
|
|
191
|
+
if (displayStore.useMinCap || displayStore.useMaxCap) {
|
|
199
192
|
const siteDistance = dataStore.siteDistanceStore.getDistance(cell.siteId, 500);
|
|
200
193
|
const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
|
|
201
|
-
|
|
202
|
-
// Apply base size multiplier
|
|
203
194
|
const baseAdjusted = autoRadius * displayStore.autoSizeBase;
|
|
195
|
+
const scaledAuto = baseAdjusted * scaleFactor;
|
|
196
|
+
|
|
197
|
+
// Apply caps: min = 60% of auto-size, max = 140% of auto-size
|
|
198
|
+
const minCap = scaledAuto * 0.6;
|
|
199
|
+
const maxCap = scaledAuto * 1.4;
|
|
204
200
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
radiusMeters
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
212
|
-
radiusMeters = baseRadiusMeters * scaleFactor;
|
|
201
|
+
if (displayStore.useMinCap && radiusMeters < minCap) {
|
|
202
|
+
radiusMeters = minCap;
|
|
203
|
+
}
|
|
204
|
+
if (displayStore.useMaxCap && radiusMeters > maxCap) {
|
|
205
|
+
radiusMeters = maxCap;
|
|
206
|
+
}
|
|
213
207
|
}
|
|
214
208
|
|
|
215
|
-
//
|
|
209
|
+
// Apply beamwidth boost
|
|
216
210
|
const beamwidthBoost = displayStore.currentBeamwidthBoost[zIndexKey] || 0;
|
|
217
211
|
const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
|
|
218
212
|
|
|
219
|
-
//
|
|
213
|
+
// Generate Arc
|
|
220
214
|
const feature = generateCellArc(cell, radiusMeters, zIndex, style.color, adjustedBeamwidth);
|
|
221
215
|
features.push(feature);
|
|
222
216
|
}
|
|
223
217
|
}
|
|
224
218
|
|
|
225
|
-
console.log(`[CellsLayer] Generated ${features.length} features
|
|
219
|
+
console.log(`[CellsLayer] Generated ${features.length} features in view`);
|
|
226
220
|
|
|
227
|
-
//
|
|
221
|
+
// Update Source
|
|
228
222
|
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
229
223
|
if (source) {
|
|
230
224
|
source.setData({
|
|
@@ -7,9 +7,10 @@ export declare class CellDisplayStore {
|
|
|
7
7
|
lineWidth: number;
|
|
8
8
|
showLabels: boolean;
|
|
9
9
|
layerGrouping: LayerGroupingPreset;
|
|
10
|
-
useAutoSize: boolean;
|
|
11
10
|
autoSizeMode: AutoSizeMode;
|
|
12
11
|
autoSizeBase: number;
|
|
12
|
+
useMinCap: boolean;
|
|
13
|
+
useMaxCap: boolean;
|
|
13
14
|
level1: CellGroupingField;
|
|
14
15
|
level2: CellGroupingField;
|
|
15
16
|
currentZIndex: Record<string, number>;
|
|
@@ -8,10 +8,12 @@ export class CellDisplayStore {
|
|
|
8
8
|
lineWidth = $state(1);
|
|
9
9
|
showLabels = $state(false);
|
|
10
10
|
layerGrouping = $state('frequency');
|
|
11
|
-
// Auto-size settings
|
|
12
|
-
useAutoSize = $state(false);
|
|
11
|
+
// Auto-size settings (used by density caps)
|
|
13
12
|
autoSizeMode = $state('logarithmic');
|
|
14
13
|
autoSizeBase = $state(1.0);
|
|
14
|
+
// Density-based caps
|
|
15
|
+
useMinCap = $state(false);
|
|
16
|
+
useMaxCap = $state(false);
|
|
15
17
|
// Grouping
|
|
16
18
|
level1 = $state('tech');
|
|
17
19
|
level2 = $state('fband');
|
|
@@ -39,9 +41,10 @@ export class CellDisplayStore {
|
|
|
39
41
|
this.lineWidth = parsed.lineWidth ?? 1;
|
|
40
42
|
this.showLabels = parsed.showLabels ?? false;
|
|
41
43
|
this.layerGrouping = parsed.layerGrouping ?? 'frequency';
|
|
42
|
-
this.useAutoSize = parsed.useAutoSize ?? false;
|
|
43
44
|
this.autoSizeMode = parsed.autoSizeMode ?? 'logarithmic';
|
|
44
45
|
this.autoSizeBase = parsed.autoSizeBase ?? 1.0;
|
|
46
|
+
this.useMinCap = parsed.useMinCap ?? false;
|
|
47
|
+
this.useMaxCap = parsed.useMaxCap ?? false;
|
|
45
48
|
this.level1 = parsed.level1 ?? 'tech';
|
|
46
49
|
this.level2 = parsed.level2 ?? 'fband';
|
|
47
50
|
this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
|
|
@@ -64,9 +67,10 @@ export class CellDisplayStore {
|
|
|
64
67
|
lineWidth: this.lineWidth,
|
|
65
68
|
showLabels: this.showLabels,
|
|
66
69
|
layerGrouping: this.layerGrouping,
|
|
67
|
-
useAutoSize: this.useAutoSize,
|
|
68
70
|
autoSizeMode: this.autoSizeMode,
|
|
69
71
|
autoSizeBase: this.autoSizeBase,
|
|
72
|
+
useMinCap: this.useMinCap,
|
|
73
|
+
useMaxCap: this.useMaxCap,
|
|
70
74
|
level1: this.level1,
|
|
71
75
|
level2: this.level2,
|
|
72
76
|
labelPixelDistance: this.labelPixelDistance,
|