@smartnet360/svelte-components 0.0.97 → 0.0.99
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 +79 -15
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +1 -0
- package/dist/apps/site-check/data-loader.d.ts +2 -2
- 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 +4 -0
- package/dist/core/TreeView/tree.store.js +33 -0
- package/dist/map-v2/demo/demo-cells.d.ts +2 -1
- package/dist/map-v2/demo/demo-cells.js +107 -21
- package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte +77 -24
- 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,12 +62,19 @@
|
|
|
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
|
+
|
|
68
|
+
// Single Level 1 select mode - only one Level 1 node per parent at a time (radio behavior)
|
|
69
|
+
let singleLevel1Select = $state(false);
|
|
70
|
+
|
|
64
71
|
// Available field options for grouping levels
|
|
65
72
|
const fieldOptions: { value: TreeGroupField; label: string }[] = [
|
|
66
73
|
{ value: 'site', label: 'Site' },
|
|
67
74
|
{ value: 'band', label: 'Band' },
|
|
68
75
|
{ value: 'azimuth', label: 'Azimuth' },
|
|
69
|
-
{ value: 'sector', label: 'Sector' }
|
|
76
|
+
{ value: 'sector', label: 'Sector' },
|
|
77
|
+
{ value: 'cellName', label: 'Cell Name' }
|
|
70
78
|
];
|
|
71
79
|
|
|
72
80
|
// Handlers for level changes
|
|
@@ -92,10 +100,10 @@
|
|
|
92
100
|
return fieldOptions.filter(opt => opt.value !== treeGrouping.level0);
|
|
93
101
|
}); let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
94
102
|
|
|
95
|
-
// Rebuild tree whenever treeGrouping changes
|
|
103
|
+
// Rebuild tree whenever treeGrouping, singleRootSelect, or singleLevel1Select changes
|
|
96
104
|
$effect(() => {
|
|
97
105
|
|
|
98
|
-
log('
|
|
106
|
+
log('🔄 Rebuilding tree with grouping', { treeGrouping, singleRootSelect, singleLevel1Select });
|
|
99
107
|
|
|
100
108
|
// Clear any existing localStorage data to prevent stale state
|
|
101
109
|
const storageKey = 'site-check:treeState';
|
|
@@ -119,11 +127,15 @@
|
|
|
119
127
|
nodes: treeNodes,
|
|
120
128
|
namespace: 'site-check',
|
|
121
129
|
persistState: false, // Don't persist when grouping changes dynamically
|
|
122
|
-
defaultExpandAll: false
|
|
130
|
+
defaultExpandAll: false,
|
|
131
|
+
singleRootSelect, // Pass single root select mode
|
|
132
|
+
singleLevel1Select // Pass single Level 1 select mode
|
|
123
133
|
});
|
|
124
134
|
log('✅ Tree Store Created', {
|
|
125
135
|
namespace: 'site-check',
|
|
126
|
-
grouping: treeGrouping
|
|
136
|
+
grouping: treeGrouping,
|
|
137
|
+
singleRootSelect,
|
|
138
|
+
singleLevel1Select
|
|
127
139
|
});
|
|
128
140
|
});
|
|
129
141
|
|
|
@@ -157,9 +169,9 @@
|
|
|
157
169
|
|
|
158
170
|
// Expand layout based on selected cells and chosen base layout
|
|
159
171
|
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);
|
|
172
|
+
// Pass cellStyling, treeGrouping, colorDimension, and useSectorLineStyles - helper will decide per-section whether to use styling,
|
|
173
|
+
// and generate appropriate labels based on grouping, colors based on colorDimension, and line styles based on useSectorLineStyles
|
|
174
|
+
const expanded = expandLayoutForCells(selectedBaseLayout, filteredData, treeGrouping, colorDimension, useSectorLineStyles, cellStyling);
|
|
163
175
|
log('📐 Chart Layout:', {
|
|
164
176
|
layoutName: selectedBaseLayout.layoutName,
|
|
165
177
|
layoutDefaultColors: selectedBaseLayout.useDefaultChartColors ?? false,
|
|
@@ -355,10 +367,62 @@
|
|
|
355
367
|
>
|
|
356
368
|
<option value="band">Band</option>
|
|
357
369
|
<option value="site">Site</option>
|
|
370
|
+
<option value="sector">Sector</option>
|
|
371
|
+
<option value="cellName">Cell Name</option>
|
|
358
372
|
</select>
|
|
359
373
|
</div>
|
|
360
374
|
</div>
|
|
361
375
|
|
|
376
|
+
<!-- Single Root Select Toggle -->
|
|
377
|
+
<div class="form-check mt-2">
|
|
378
|
+
<input
|
|
379
|
+
class="form-check-input"
|
|
380
|
+
type="checkbox"
|
|
381
|
+
id="singleRootSelectCheck"
|
|
382
|
+
checked={singleRootSelect}
|
|
383
|
+
onchange={(e) => {
|
|
384
|
+
singleRootSelect = e.currentTarget.checked;
|
|
385
|
+
log('🔘 Single root select mode:', singleRootSelect);
|
|
386
|
+
|
|
387
|
+
// When enabling single root mode, uncheck all roots except the first one
|
|
388
|
+
if (singleRootSelect && treeStore) {
|
|
389
|
+
const store = $treeStore;
|
|
390
|
+
if (store) {
|
|
391
|
+
const checkedRoots = store.state.rootPaths.filter(path =>
|
|
392
|
+
store.state.checkedPaths.has(path)
|
|
393
|
+
);
|
|
394
|
+
if (checkedRoots.length > 1) {
|
|
395
|
+
log('🔘 Multiple roots selected, keeping only first one:', checkedRoots[0]);
|
|
396
|
+
// Uncheck all except the first
|
|
397
|
+
for (let i = 1; i < checkedRoots.length; i++) {
|
|
398
|
+
store.toggle(checkedRoots[i]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
<label class="form-check-label small" for="singleRootSelectCheck">
|
|
406
|
+
Single selection on level 0
|
|
407
|
+
</label>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<!-- Single Level 1 Select Toggle -->
|
|
411
|
+
<div class="form-check mt-2">
|
|
412
|
+
<input
|
|
413
|
+
class="form-check-input"
|
|
414
|
+
type="checkbox"
|
|
415
|
+
id="singleLevel1SelectCheck"
|
|
416
|
+
checked={singleLevel1Select}
|
|
417
|
+
onchange={(e) => {
|
|
418
|
+
singleLevel1Select = e.currentTarget.checked;
|
|
419
|
+
log('🔘 Single Level 1 select mode:', singleLevel1Select);
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
<label class="form-check-label small" for="singleLevel1SelectCheck">
|
|
423
|
+
Single selection on level 1
|
|
424
|
+
</label>
|
|
425
|
+
</div>
|
|
362
426
|
</div>
|
|
363
427
|
{/if}
|
|
364
428
|
{/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>;
|
|
@@ -14,11 +14,11 @@ export interface CellTrafficRecord {
|
|
|
14
14
|
/**
|
|
15
15
|
* Tree grouping field types
|
|
16
16
|
*/
|
|
17
|
-
export type TreeGroupField = 'site' | 'azimuth' | 'band' | 'sector';
|
|
17
|
+
export type TreeGroupField = 'site' | 'azimuth' | 'band' | 'sector' | 'cellName';
|
|
18
18
|
/**
|
|
19
19
|
* Color dimension types - determines which field is used for chart coloring
|
|
20
20
|
*/
|
|
21
|
-
export type ColorDimension = 'site' | 'band';
|
|
21
|
+
export type ColorDimension = 'site' | 'azimuth' | 'band' | 'sector' | 'cellName';
|
|
22
22
|
/**
|
|
23
23
|
* Configuration for tree hierarchy grouping
|
|
24
24
|
* Defines which fields appear at each level of the tree
|
|
@@ -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,10 @@ 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;
|
|
79
|
+
/** Single Level 1 selection mode - only one Level 1 node per parent can be checked at a time (radio behavior) */
|
|
80
|
+
singleLevel1Select?: boolean;
|
|
77
81
|
}
|
|
78
82
|
/**
|
|
79
83
|
* Store value exposed to consumers
|
|
@@ -87,6 +87,39 @@ 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
|
+
}
|
|
105
|
+
// STEP 0.5: If singleLevel1Select mode and this is a Level 1 node being checked, uncheck sibling Level 1 nodes
|
|
106
|
+
if (config.singleLevel1Select && newChecked && nodeState.level === 1) {
|
|
107
|
+
log('🔘 Single Level 1 select mode: unchecking sibling Level 1 nodes', { path });
|
|
108
|
+
const parentPath = nodeState.parentPath;
|
|
109
|
+
// Find and uncheck all Level 1 siblings (same parent, same level, different path)
|
|
110
|
+
state.nodes.forEach((node, nodePath) => {
|
|
111
|
+
if (node.level === 1 &&
|
|
112
|
+
node.parentPath === parentPath &&
|
|
113
|
+
nodePath !== path) {
|
|
114
|
+
newCheckedPaths.delete(nodePath);
|
|
115
|
+
// Also uncheck all descendants of this sibling
|
|
116
|
+
const siblingDescendants = getDescendantPaths(nodePath, state.nodes, separator);
|
|
117
|
+
siblingDescendants.forEach(descendantPath => {
|
|
118
|
+
newCheckedPaths.delete(descendantPath);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
90
123
|
// STEP 1: Update this node
|
|
91
124
|
if (newChecked) {
|
|
92
125
|
newCheckedPaths.add(path);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Cell } from '../features/cells/types';
|
|
10
10
|
/**
|
|
11
|
-
* Generate demo cells
|
|
11
|
+
* Generate demo cells with varied density patterns in circular distribution
|
|
12
|
+
* Creates density zones radiating from center with random placement
|
|
12
13
|
*/
|
|
13
14
|
export declare const demoCells: Cell[];
|
|
@@ -6,14 +6,68 @@
|
|
|
6
6
|
* Each sector has 12 cells (all tech-band combinations)
|
|
7
7
|
* Total: 100 sites × 3 sectors × 12 tech-bands = 3,600 cells
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
9
|
+
const BASE_LAT = 47.4979;
|
|
10
|
+
const BASE_LNG = 19.0402;
|
|
11
|
+
// Generate sites in a circular pattern with varying density
|
|
12
|
+
const NUM_SITES = 2000;
|
|
13
|
+
const RADIUS_KM = 15; // 15km radius circle
|
|
14
|
+
const RADIUS_DEGREES = RADIUS_KM / 111; // Approximate conversion
|
|
15
|
+
// Density zones (distance from center)
|
|
16
|
+
const DENSITY_ZONES = [
|
|
17
|
+
{ maxRadius: 0.3, minSpacing: 0.0008, maxSpacing: 0.0015, name: 'Very Dense Core' }, // 0-3km: 80-150m spacing
|
|
18
|
+
{ maxRadius: 0.5, minSpacing: 0.0015, maxSpacing: 0.003, name: 'Dense Inner' }, // 3-5km: 150-300m spacing
|
|
19
|
+
{ maxRadius: 0.7, minSpacing: 0.003, maxSpacing: 0.006, name: 'Medium' }, // 5-7km: 300-600m spacing
|
|
20
|
+
{ maxRadius: 0.85, minSpacing: 0.006, maxSpacing: 0.012, name: 'Sparse Suburban' }, // 7-12km: 600m-1.2km spacing
|
|
21
|
+
{ maxRadius: 1.0, minSpacing: 0.012, maxSpacing: 0.025, name: 'Very Sparse Rural' } // 12-15km: 1.2-2.5km spacing
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Get density zone for a given normalized radius
|
|
25
|
+
*/
|
|
26
|
+
function getDensityZone(normalizedRadius) {
|
|
27
|
+
for (const zone of DENSITY_ZONES) {
|
|
28
|
+
if (normalizedRadius <= zone.maxRadius) {
|
|
29
|
+
return zone;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return DENSITY_ZONES[DENSITY_ZONES.length - 1];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generate random point within circle using polar coordinates
|
|
36
|
+
*/
|
|
37
|
+
function generateRandomPointInCircle() {
|
|
38
|
+
// Use square root for uniform distribution in circle
|
|
39
|
+
const r = Math.sqrt(Math.random()) * RADIUS_DEGREES;
|
|
40
|
+
const theta = Math.random() * 2 * Math.PI;
|
|
41
|
+
const lat = BASE_LAT + r * Math.cos(theta);
|
|
42
|
+
const lng = BASE_LNG + r * Math.sin(theta);
|
|
43
|
+
const normalizedRadius = r / RADIUS_DEGREES;
|
|
44
|
+
return { lat, lng, normalizedRadius };
|
|
45
|
+
}
|
|
46
|
+
// Cluster configuration for varied density
|
|
47
|
+
// (kept for backward compatibility but not used with circular generation)
|
|
48
|
+
const CLUSTERS = [
|
|
49
|
+
// Dense urban cluster (top-left) - very tight spacing
|
|
50
|
+
{ startRow: 0, endRow: 3, startCol: 0, endCol: 3, spacing: 0.3 },
|
|
51
|
+
// Medium density cluster (center) - normal spacing
|
|
52
|
+
{ startRow: 3, endRow: 7, startCol: 3, endCol: 7, spacing: 1.0 },
|
|
53
|
+
// Sparse rural cluster (bottom-right) - wide spacing
|
|
54
|
+
{ startRow: 7, endRow: 10, startCol: 7, endCol: 10, spacing: 2.5 },
|
|
55
|
+
// Random outliers scattered around
|
|
56
|
+
{ startRow: 0, endRow: 10, startCol: 0, endCol: 10, spacing: 1.5 }
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Add random jitter to coordinates for natural variation
|
|
60
|
+
*/
|
|
61
|
+
function addJitter(value, maxJitter) {
|
|
62
|
+
return value + (Math.random() - 0.5) * 2 * maxJitter;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Determine if site should be skipped (for creating gaps)
|
|
66
|
+
*/
|
|
67
|
+
function shouldSkipSite(row, col) {
|
|
68
|
+
// Skip some sites randomly to create density variation (20% skip rate)
|
|
69
|
+
return Math.random() < 0.2;
|
|
70
|
+
}
|
|
17
71
|
// Standard beamwidth for sectors
|
|
18
72
|
const BEAMWIDTH = 65;
|
|
19
73
|
// Cell tech-band definitions with proper fband format
|
|
@@ -51,20 +105,51 @@ const STATUSES = [
|
|
|
51
105
|
'On_Air'
|
|
52
106
|
];
|
|
53
107
|
/**
|
|
54
|
-
* Generate demo cells
|
|
108
|
+
* Generate demo cells with varied density patterns in circular distribution
|
|
109
|
+
* Creates density zones radiating from center with random placement
|
|
55
110
|
*/
|
|
56
111
|
export const demoCells = [];
|
|
57
112
|
let cellCounter = 1;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
let actualSiteIndex = 0;
|
|
114
|
+
// Track used positions to maintain minimum spacing
|
|
115
|
+
const usedPositions = [];
|
|
116
|
+
/**
|
|
117
|
+
* Check if position is too close to existing sites
|
|
118
|
+
*/
|
|
119
|
+
function isTooClose(lat, lng, minSpacing) {
|
|
120
|
+
for (const pos of usedPositions) {
|
|
121
|
+
const distance = Math.sqrt(Math.pow(lat - pos.lat, 2) + Math.pow(lng - pos.lng, 2));
|
|
122
|
+
const requiredSpacing = (minSpacing + pos.minSpacing) / 2;
|
|
123
|
+
if (distance < requiredSpacing) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Generate sites in a circular pattern with density-based placement
|
|
130
|
+
for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; attempt++) {
|
|
131
|
+
// Generate random point in circle
|
|
132
|
+
const { lat, lng, normalizedRadius } = generateRandomPointInCircle();
|
|
133
|
+
// Get density zone for this radius
|
|
134
|
+
const zone = getDensityZone(normalizedRadius);
|
|
135
|
+
// Random spacing within zone range
|
|
136
|
+
const minSpacing = zone.minSpacing + Math.random() * (zone.maxSpacing - zone.minSpacing);
|
|
137
|
+
// Check if too close to existing sites
|
|
138
|
+
if (isTooClose(lat, lng, minSpacing)) {
|
|
139
|
+
continue; // Try another position
|
|
140
|
+
}
|
|
141
|
+
// Add random jitter for natural variation
|
|
142
|
+
const jitterAmount = minSpacing * 0.3; // 30% of spacing
|
|
143
|
+
const siteLat = addJitter(lat, jitterAmount);
|
|
144
|
+
const siteLng = addJitter(lng, jitterAmount);
|
|
145
|
+
// Record position
|
|
146
|
+
usedPositions.push({ lat: siteLat, lng: siteLng, minSpacing });
|
|
147
|
+
const siteId = `DEMO-SITE-${String(actualSiteIndex + 1).padStart(4, '0')}`;
|
|
148
|
+
actualSiteIndex++;
|
|
149
|
+
// Generate 3 sectors per site (with some random 1 or 2 sector sites)
|
|
150
|
+
const numSectors = Math.random() < 0.1 ? (Math.random() < 0.5 ? 1 : 2) : 3; // 10% chance of 1-2 sectors
|
|
151
|
+
const sectorsToGenerate = AZIMUTHS.slice(0, numSectors);
|
|
152
|
+
sectorsToGenerate.forEach((azimuth, sectorIndex) => {
|
|
68
153
|
// Generate 12 tech-bands per sector
|
|
69
154
|
TECH_BANDS.forEach((techBand, techIndex) => {
|
|
70
155
|
const cellId = `CELL-${String(cellCounter).padStart(4, '0')}`;
|
|
@@ -118,10 +203,11 @@ for (let siteIndex = 0; siteIndex < NUM_SITES; siteIndex++) {
|
|
|
118
203
|
// Other
|
|
119
204
|
other: {
|
|
120
205
|
demoCell: true,
|
|
121
|
-
siteNumber:
|
|
206
|
+
siteNumber: actualSiteIndex,
|
|
122
207
|
sector: sectorIndex + 1,
|
|
123
208
|
techBandKey: `${techBand.tech}_${techBand.band}`,
|
|
124
|
-
|
|
209
|
+
radius: normalizedRadius,
|
|
210
|
+
densityZone: zone.name
|
|
125
211
|
},
|
|
126
212
|
customSubgroup: `Sector-${sectorIndex + 1}`
|
|
127
213
|
});
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
onAction,
|
|
49
49
|
actionButtonLabel = 'Process Cluster',
|
|
50
50
|
featureIcon = 'geo-alt-fill',
|
|
51
|
-
idPropertyOptions = ['siteId','sectorId', 'cellName','id'],
|
|
52
|
-
defaultIdProperty = '
|
|
51
|
+
idPropertyOptions = ['none','siteId','sectorId', 'cellName','id'],
|
|
52
|
+
defaultIdProperty = 'none'
|
|
53
53
|
}: Props = $props();
|
|
54
54
|
|
|
55
55
|
// Get map from context
|
|
@@ -114,33 +114,73 @@
|
|
|
114
114
|
const map = get(mapStore);
|
|
115
115
|
if (!map) return;
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Group features by coordinates
|
|
118
|
+
const featuresByLocation = new Map<string, SelectedFeature[]>();
|
|
119
|
+
|
|
120
|
+
for (const feature of features) {
|
|
121
|
+
const lat = feature.properties?.latitude || feature.properties?.lat;
|
|
122
|
+
const lon = feature.properties?.longitude || feature.properties?.lon || feature.properties?.lng;
|
|
123
|
+
|
|
124
|
+
if (lat && lon) {
|
|
125
|
+
const key = `${lon.toFixed(6)},${lat.toFixed(6)}`; // Round to avoid floating point issues
|
|
126
|
+
if (!featuresByLocation.has(key)) {
|
|
127
|
+
featuresByLocation.set(key, []);
|
|
128
|
+
}
|
|
129
|
+
featuresByLocation.get(key)!.push(feature);
|
|
130
|
+
} else {
|
|
131
|
+
console.warn('[FeatureSelectionControl] No coordinates found for feature', feature.id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Track which location keys are currently active
|
|
136
|
+
const activeLocationKeys = new Set(featuresByLocation.keys());
|
|
137
|
+
|
|
138
|
+
// Remove markers that are no longer needed
|
|
139
|
+
for (const [key, marker] of markers.entries()) {
|
|
140
|
+
if (!activeLocationKeys.has(key)) {
|
|
121
141
|
marker.remove();
|
|
122
|
-
markers.delete(
|
|
123
|
-
console.log('[FeatureSelectionControl] Removed marker
|
|
142
|
+
markers.delete(key);
|
|
143
|
+
console.log('[FeatureSelectionControl] Removed marker at', key);
|
|
124
144
|
}
|
|
125
145
|
}
|
|
126
146
|
|
|
127
|
-
//
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
// Create or update markers for each unique location
|
|
148
|
+
for (const [locationKey, featuresAtLocation] of featuresByLocation) {
|
|
149
|
+
const [lon, lat] = locationKey.split(',').map(Number);
|
|
150
|
+
|
|
151
|
+
// Build multi-line label HTML
|
|
152
|
+
const labelHTML = featuresAtLocation
|
|
153
|
+
.map(f => `<div style="padding: 2px 0;">${f.id}</div>`)
|
|
154
|
+
.join('');
|
|
155
|
+
|
|
156
|
+
// Check if marker already exists at this location
|
|
157
|
+
if (markers.has(locationKey)) {
|
|
158
|
+
// Update existing marker's popup
|
|
159
|
+
const marker = markers.get(locationKey)!;
|
|
160
|
+
const popup = marker.getPopup();
|
|
161
|
+
if (popup) {
|
|
162
|
+
popup.setHTML(`<div class="marker-label" style="font-size: 12px; line-height: 1.4;">${labelHTML}</div>`);
|
|
143
163
|
}
|
|
164
|
+
} else {
|
|
165
|
+
// Create new marker with popup
|
|
166
|
+
const popup = new mapboxgl.Popup({
|
|
167
|
+
closeButton: false,
|
|
168
|
+
closeOnClick: false,
|
|
169
|
+
offset: 45,
|
|
170
|
+
className: 'selection-marker-popup',
|
|
171
|
+
anchor: 'bottom'
|
|
172
|
+
}).setHTML(`<div class="marker-label" style="font-size: 12px; line-height: 1.4;">${labelHTML}</div>`);
|
|
173
|
+
|
|
174
|
+
const marker = new mapboxgl.Marker({ color: '#FF6B35' })
|
|
175
|
+
.setLngLat([lon, lat])
|
|
176
|
+
.setPopup(popup)
|
|
177
|
+
.addTo(map);
|
|
178
|
+
|
|
179
|
+
// Show popup immediately
|
|
180
|
+
marker.togglePopup();
|
|
181
|
+
|
|
182
|
+
markers.set(locationKey, marker);
|
|
183
|
+
console.log('[FeatureSelectionControl] Added marker at', locationKey, 'with', featuresAtLocation.length, 'items');
|
|
144
184
|
}
|
|
145
185
|
}
|
|
146
186
|
}
|
|
@@ -416,4 +456,17 @@
|
|
|
416
456
|
color: var(--bs-btn-disabled-color, var(--bs-btn-color, var(--bs-body-color)));
|
|
417
457
|
opacity: var(--bs-btn-disabled-opacity, 0.65);
|
|
418
458
|
}
|
|
459
|
+
|
|
460
|
+
/* Style for marker popup labels */
|
|
461
|
+
:global(.selection-marker-popup .mapboxgl-popup-content) {
|
|
462
|
+
padding: 8px 12px;
|
|
463
|
+
background: rgba(255, 255, 255, 0.95);
|
|
464
|
+
border-radius: 4px;
|
|
465
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
466
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
:global(.selection-marker-popup .mapboxgl-popup-tip) {
|
|
470
|
+
border-top-color: rgba(255, 255, 255, 0.95);
|
|
471
|
+
}
|
|
419
472
|
</style>
|
|
@@ -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,
|