@smartnet360/svelte-components 0.0.97 → 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 +37 -93
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +32 -59
- package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +0 -1
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +2 -5
- 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,47 +84,63 @@
|
|
|
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
146
|
min="0.5"
|
|
@@ -134,78 +150,6 @@
|
|
|
134
150
|
/>
|
|
135
151
|
</div>
|
|
136
152
|
</div>
|
|
137
|
-
{:else}
|
|
138
|
-
<!-- Density-Based Caps (Manual Mode Only) -->
|
|
139
|
-
<div class="ps-3 border-start border-2 mb-3">
|
|
140
|
-
<!-- Min Cap -->
|
|
141
|
-
<div class="row align-items-center g-2 mb-2">
|
|
142
|
-
<div class="col-7 text-secondary small">Min Size Cap (Density)</div>
|
|
143
|
-
<div class="col-5">
|
|
144
|
-
<div class="form-check form-switch m-0 d-flex align-items-center justify-content-end">
|
|
145
|
-
<input
|
|
146
|
-
id="cell-mincap-toggle"
|
|
147
|
-
type="checkbox"
|
|
148
|
-
class="form-check-input"
|
|
149
|
-
role="switch"
|
|
150
|
-
bind:checked={displayStore.useMinCap}
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
<!-- Max Cap -->
|
|
157
|
-
<div class="row align-items-center g-2 mb-2">
|
|
158
|
-
<div class="col-7 text-secondary small">Max Size Cap (Density)</div>
|
|
159
|
-
<div class="col-5">
|
|
160
|
-
<div class="form-check form-switch m-0 d-flex align-items-center justify-content-end">
|
|
161
|
-
<input
|
|
162
|
-
id="cell-maxcap-toggle"
|
|
163
|
-
type="checkbox"
|
|
164
|
-
class="form-check-input"
|
|
165
|
-
role="switch"
|
|
166
|
-
bind:checked={displayStore.useMaxCap}
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
{#if displayStore.useMinCap || displayStore.useMaxCap}
|
|
173
|
-
<!-- Cap Mode (shares auto-size settings) -->
|
|
174
|
-
<div class="row align-items-center g-2 mb-2 mt-2">
|
|
175
|
-
<div class="col-4 text-secondary small">Cap Mode</div>
|
|
176
|
-
<div class="col-8">
|
|
177
|
-
<select
|
|
178
|
-
class="form-select form-select-sm"
|
|
179
|
-
bind:value={displayStore.autoSizeMode}
|
|
180
|
-
>
|
|
181
|
-
<option value="logarithmic">Logarithmic</option>
|
|
182
|
-
<option value="percentage">Proportional</option>
|
|
183
|
-
<option value="tiered">Tiered</option>
|
|
184
|
-
<option value="hybrid">Hybrid</option>
|
|
185
|
-
</select>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<!-- Cap Base -->
|
|
190
|
-
<div class="row align-items-center g-2 mb-2">
|
|
191
|
-
<div class="col-4 text-secondary small">Cap Base</div>
|
|
192
|
-
<div class="col-3 text-end">
|
|
193
|
-
<span class="badge bg-white text-muted border">{displayStore.autoSizeBase.toFixed(1)}x</span>
|
|
194
|
-
</div>
|
|
195
|
-
<div class="col-5">
|
|
196
|
-
<input
|
|
197
|
-
id="cell-cap-base-slider"
|
|
198
|
-
type="range"
|
|
199
|
-
class="form-range w-100"
|
|
200
|
-
min="0.5"
|
|
201
|
-
max="3.0"
|
|
202
|
-
step="0.1"
|
|
203
|
-
bind:value={displayStore.autoSizeBase}
|
|
204
|
-
/>
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
{/if}
|
|
208
|
-
</div>
|
|
209
153
|
{/if} <div class="border-top my-3"></div>
|
|
210
154
|
|
|
211
155
|
<!-- Show Labels -->
|
|
@@ -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,7 +127,6 @@
|
|
|
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;
|
|
137
132
|
const _useMinCap = displayStore.useMinCap;
|
|
@@ -151,101 +146,79 @@
|
|
|
151
146
|
}
|
|
152
147
|
|
|
153
148
|
function renderCells(map: mapboxgl.Map) {
|
|
149
|
+
const bounds = map.getBounds();
|
|
150
|
+
if (!bounds) return;
|
|
151
|
+
|
|
154
152
|
const zoom = map.getZoom();
|
|
155
153
|
const centerLat = map.getCenter().lat;
|
|
156
154
|
|
|
157
155
|
console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
|
|
158
156
|
|
|
159
|
-
//
|
|
157
|
+
// Calculate base radius from pixel size
|
|
160
158
|
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
161
|
-
|
|
162
|
-
if (!displayStore.useAutoSize) {
|
|
163
|
-
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
164
|
-
}
|
|
159
|
+
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
165
160
|
|
|
166
|
-
//
|
|
167
|
-
// In real app, this comes from a store
|
|
161
|
+
// Group cells
|
|
168
162
|
const groups = groupCells(dataStore.filteredCells, displayStore.level1, displayStore.level2);
|
|
169
163
|
console.log(`[CellsLayer] Groups: ${groups.size}`);
|
|
170
164
|
|
|
171
165
|
const features: GeoJSON.Feature[] = [];
|
|
172
166
|
let groupIndex = 0;
|
|
173
167
|
|
|
174
|
-
//
|
|
168
|
+
// Iterate groups and generate features
|
|
175
169
|
for (const [groupId, cells] of groups) {
|
|
176
|
-
// Get style from registry
|
|
177
170
|
const defaultColor = getColorForGroup(groupIndex++);
|
|
178
171
|
const style = registry.getStyle(groupId, defaultColor);
|
|
179
172
|
|
|
180
173
|
if (!style.visible) continue;
|
|
181
174
|
|
|
182
175
|
for (const cell of cells) {
|
|
183
|
-
//
|
|
184
|
-
if (!
|
|
185
|
-
|
|
186
|
-
if (!bounds || !bounds.contains([cell.longitude, cell.latitude])) {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
176
|
+
// Viewport filter
|
|
177
|
+
if (!bounds.contains([cell.longitude, cell.latitude])) {
|
|
178
|
+
continue;
|
|
189
179
|
}
|
|
190
180
|
|
|
191
|
-
//
|
|
181
|
+
// Z-Index Lookup
|
|
192
182
|
const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
|
|
193
183
|
const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
|
|
194
184
|
|
|
195
|
-
//
|
|
185
|
+
// Calculate radius with z-index scaling
|
|
196
186
|
const MAX_Z = 35;
|
|
197
|
-
|
|
187
|
+
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
188
|
+
let radiusMeters = baseRadiusMeters * scaleFactor;
|
|
198
189
|
|
|
199
|
-
if
|
|
200
|
-
|
|
190
|
+
// Apply density-based caps if enabled
|
|
191
|
+
if (displayStore.useMinCap || displayStore.useMaxCap) {
|
|
201
192
|
const siteDistance = dataStore.siteDistanceStore.getDistance(cell.siteId, 500);
|
|
202
193
|
const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
|
|
203
|
-
|
|
204
|
-
// Apply base size multiplier
|
|
205
194
|
const baseAdjusted = autoRadius * displayStore.autoSizeBase;
|
|
195
|
+
const scaledAuto = baseAdjusted * scaleFactor;
|
|
206
196
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
radiusMeters = baseAdjusted * scaleFactor;
|
|
211
|
-
} else {
|
|
212
|
-
// Manual mode: base from pixel size, then scale by z-index
|
|
213
|
-
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
214
|
-
radiusMeters = baseRadiusMeters * scaleFactor;
|
|
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;
|
|
215
200
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const scaledAuto = baseAdjusted * scaleFactor;
|
|
222
|
-
|
|
223
|
-
// Apply caps: min = 60% of auto-size, max = 140% of auto-size
|
|
224
|
-
const minCap = scaledAuto * 0.6;
|
|
225
|
-
const maxCap = scaledAuto * 1.4;
|
|
226
|
-
|
|
227
|
-
if (displayStore.useMinCap && radiusMeters < minCap) {
|
|
228
|
-
radiusMeters = minCap;
|
|
229
|
-
}
|
|
230
|
-
if (displayStore.useMaxCap && radiusMeters > maxCap) {
|
|
231
|
-
radiusMeters = maxCap;
|
|
232
|
-
}
|
|
201
|
+
if (displayStore.useMinCap && radiusMeters < minCap) {
|
|
202
|
+
radiusMeters = minCap;
|
|
203
|
+
}
|
|
204
|
+
if (displayStore.useMaxCap && radiusMeters > maxCap) {
|
|
205
|
+
radiusMeters = maxCap;
|
|
233
206
|
}
|
|
234
207
|
}
|
|
235
208
|
|
|
236
|
-
//
|
|
209
|
+
// Apply beamwidth boost
|
|
237
210
|
const beamwidthBoost = displayStore.currentBeamwidthBoost[zIndexKey] || 0;
|
|
238
211
|
const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
|
|
239
212
|
|
|
240
|
-
//
|
|
213
|
+
// Generate Arc
|
|
241
214
|
const feature = generateCellArc(cell, radiusMeters, zIndex, style.color, adjustedBeamwidth);
|
|
242
215
|
features.push(feature);
|
|
243
216
|
}
|
|
244
217
|
}
|
|
245
218
|
|
|
246
|
-
console.log(`[CellsLayer] Generated ${features.length} features
|
|
219
|
+
console.log(`[CellsLayer] Generated ${features.length} features in view`);
|
|
247
220
|
|
|
248
|
-
//
|
|
221
|
+
// Update Source
|
|
249
222
|
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
250
223
|
if (source) {
|
|
251
224
|
source.setData({
|
|
@@ -8,11 +8,10 @@ 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);
|
|
15
|
-
// Density-based caps
|
|
14
|
+
// Density-based caps
|
|
16
15
|
useMinCap = $state(false);
|
|
17
16
|
useMaxCap = $state(false);
|
|
18
17
|
// Grouping
|
|
@@ -42,7 +41,6 @@ export class CellDisplayStore {
|
|
|
42
41
|
this.lineWidth = parsed.lineWidth ?? 1;
|
|
43
42
|
this.showLabels = parsed.showLabels ?? false;
|
|
44
43
|
this.layerGrouping = parsed.layerGrouping ?? 'frequency';
|
|
45
|
-
this.useAutoSize = parsed.useAutoSize ?? false;
|
|
46
44
|
this.autoSizeMode = parsed.autoSizeMode ?? 'logarithmic';
|
|
47
45
|
this.autoSizeBase = parsed.autoSizeBase ?? 1.0;
|
|
48
46
|
this.useMinCap = parsed.useMinCap ?? false;
|
|
@@ -69,7 +67,6 @@ export class CellDisplayStore {
|
|
|
69
67
|
lineWidth: this.lineWidth,
|
|
70
68
|
showLabels: this.showLabels,
|
|
71
69
|
layerGrouping: this.layerGrouping,
|
|
72
|
-
useAutoSize: this.useAutoSize,
|
|
73
70
|
autoSizeMode: this.autoSizeMode,
|
|
74
71
|
autoSizeBase: this.autoSizeBase,
|
|
75
72
|
useMinCap: this.useMinCap,
|