@smartnet360/svelte-components 0.0.89 → 0.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/map-v3/demo/DemoMap.svelte +11 -1
- package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte +54 -4
- package/dist/map-v3/features/cells/constants.d.ts +7 -0
- package/dist/map-v3/features/cells/constants.js +123 -1
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +34 -19
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte.d.ts +2 -0
- package/dist/map-v3/features/cells/logic/geometry.d.ts +8 -1
- package/dist/map-v3/features/cells/logic/geometry.js +32 -0
- package/dist/map-v3/features/cells/logic/site-distance.d.ts +34 -0
- package/dist/map-v3/features/cells/logic/site-distance.js +71 -0
- package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +7 -1
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +14 -0
- package/dist/map-v3/features/cells/stores/site.distance.svelte.d.ts +40 -0
- package/dist/map-v3/features/cells/stores/site.distance.svelte.js +146 -0
- package/dist/map-v3/features/cells/types.d.ts +12 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { createCellDataStore } from '../features/cells/stores/cell.data.svelte';
|
|
6
6
|
import { createCellRegistry } from '../features/cells/stores/cell.registry.svelte';
|
|
7
7
|
import { CellDisplayStore } from '../features/cells/stores/cell.display.svelte';
|
|
8
|
+
import { SiteDistanceStore } from '../features/cells/stores/site.distance.svelte';
|
|
8
9
|
import FeatureSettingsControl from '../core/controls/FeatureSettingsControl.svelte';
|
|
9
10
|
import CellFilterControl from '../features/cells/components/CellFilterControl.svelte';
|
|
10
11
|
import { createRepeaterDataStore } from '../features/repeaters/stores/repeater.data.svelte';
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
const cellData = createCellDataStore();
|
|
34
35
|
const cellRegistry = createCellRegistry('demo-map');
|
|
35
36
|
const cellDisplay = new CellDisplayStore();
|
|
37
|
+
const siteDistanceStore = new SiteDistanceStore();
|
|
36
38
|
|
|
37
39
|
const siteData = createSiteDataStore(cellData);
|
|
38
40
|
const siteRegistry = createSiteRegistry('demo-map');
|
|
@@ -47,6 +49,9 @@
|
|
|
47
49
|
// Need to cast or map if types slightly differ, but they should match
|
|
48
50
|
cellData.setCells(demoCells as any);
|
|
49
51
|
repeaterData.setRepeaters(demoRepeaters);
|
|
52
|
+
|
|
53
|
+
// Compute site distances
|
|
54
|
+
siteDistanceStore.updateDistances(demoCells as any);
|
|
50
55
|
});
|
|
51
56
|
</script>
|
|
52
57
|
|
|
@@ -90,7 +95,12 @@
|
|
|
90
95
|
|
|
91
96
|
<SitesLayer dataStore={siteData} displayStore={siteDisplay} registry={siteRegistry} />
|
|
92
97
|
<SiteLabelsLayer dataStore={siteData} displayStore={siteDisplay} registry={siteRegistry} />
|
|
93
|
-
<CellsLayer
|
|
98
|
+
<CellsLayer
|
|
99
|
+
dataStore={cellData}
|
|
100
|
+
registry={cellRegistry}
|
|
101
|
+
displayStore={cellDisplay}
|
|
102
|
+
siteDistanceStore={siteDistanceStore}
|
|
103
|
+
/>
|
|
94
104
|
<CellLabelsLayer dataStore={cellData} registry={cellRegistry} displayStore={cellDisplay} />
|
|
95
105
|
<RepeatersLayer dataStore={repeaterData} registry={repeaterRegistry} displayStore={repeaterDisplay} />
|
|
96
106
|
<RepeaterLabelsLayer dataStore={repeaterData} registry={repeaterRegistry} displayStore={repeaterDisplay} />
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
id="cell-radius-slider"
|
|
22
22
|
type="range"
|
|
23
23
|
class="form-range w-100"
|
|
24
|
-
min="
|
|
25
|
-
max="
|
|
26
|
-
step="5"
|
|
24
|
+
min="5"
|
|
25
|
+
max="50"
|
|
26
|
+
step="2.5"
|
|
27
27
|
bind:value={displayStore.targetPixelSize}
|
|
28
28
|
/>
|
|
29
29
|
</div>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
id="cell-line-width-slider"
|
|
41
41
|
type="range"
|
|
42
42
|
class="form-range w-100"
|
|
43
|
-
min="0
|
|
43
|
+
min="0"
|
|
44
44
|
max="5"
|
|
45
45
|
step="0.5"
|
|
46
46
|
bind:value={displayStore.lineWidth}
|
|
@@ -67,6 +67,56 @@
|
|
|
67
67
|
</div>
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
|
+
<!-- Layer Grouping -->
|
|
71
|
+
<div class="row align-items-center g-2 mb-3">
|
|
72
|
+
<div class="col-4 text-secondary fw-semibold small text-uppercase">Layer Grouping</div>
|
|
73
|
+
<div class="col-8">
|
|
74
|
+
<select
|
|
75
|
+
class="form-select form-select-sm"
|
|
76
|
+
bind:value={displayStore.layerGrouping}
|
|
77
|
+
>
|
|
78
|
+
<option value="frequency">Classic (frequency-priority)</option>
|
|
79
|
+
<option value="technology">Technology Priority</option>
|
|
80
|
+
<option value="balanced">Balanced</option>
|
|
81
|
+
<option value="ltePriority">LTE Priority</option>
|
|
82
|
+
</select>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Auto Size -->
|
|
87
|
+
<div class="row align-items-center g-2 mb-3">
|
|
88
|
+
<div class="col-4 text-secondary fw-semibold small text-uppercase">Auto Size</div>
|
|
89
|
+
<div class="col-3"></div>
|
|
90
|
+
<div class="col-5">
|
|
91
|
+
<div class="form-check form-switch m-0 d-flex align-items-center justify-content-end">
|
|
92
|
+
<input
|
|
93
|
+
id="cell-autosize-toggle"
|
|
94
|
+
type="checkbox"
|
|
95
|
+
class="form-check-input"
|
|
96
|
+
role="switch"
|
|
97
|
+
bind:checked={displayStore.useAutoSize}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{#if displayStore.useAutoSize}
|
|
104
|
+
<!-- Auto Size Mode -->
|
|
105
|
+
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
106
|
+
<div class="col-4 text-secondary small">Mode</div>
|
|
107
|
+
<div class="col-8">
|
|
108
|
+
<select
|
|
109
|
+
class="form-select form-select-sm"
|
|
110
|
+
bind:value={displayStore.autoSizeMode}
|
|
111
|
+
>
|
|
112
|
+
<option value="logarithmic">Logarithmic (smooth)</option>
|
|
113
|
+
<option value="percentage">Proportional (40%)</option>
|
|
114
|
+
<option value="tiered">Tiered (4 levels)</option>
|
|
115
|
+
</select>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
{/if}
|
|
119
|
+
|
|
70
120
|
<div class="border-top my-3"></div>
|
|
71
121
|
|
|
72
122
|
<!-- Show Labels -->
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* Higher frequency bands typically rendered on top
|
|
6
6
|
*/
|
|
7
7
|
import type { TechnologyBandKey } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Layer Grouping Presets
|
|
10
|
+
* Controls visual layering strategy for overlapping sectors
|
|
11
|
+
*/
|
|
12
|
+
export type LayerGroupingPreset = 'frequency' | 'technology' | 'balanced' | 'ltePriority';
|
|
13
|
+
export declare const Z_INDEX_PRESETS: Record<LayerGroupingPreset, Record<TechnologyBandKey, number>>;
|
|
14
|
+
export declare const BEAMWIDTH_BOOST_PRESETS: Record<LayerGroupingPreset, Record<TechnologyBandKey, number>>;
|
|
8
15
|
export declare const Z_INDEX_BY_BAND: Record<TechnologyBandKey, number>;
|
|
9
16
|
export declare const BEAMWIDTH_BOOST_BY_BAND: Record<TechnologyBandKey, number>;
|
|
10
17
|
/**
|
|
@@ -4,6 +4,128 @@
|
|
|
4
4
|
* Controls which sectors appear on top when overlapping
|
|
5
5
|
* Higher frequency bands typically rendered on top
|
|
6
6
|
*/
|
|
7
|
+
export const Z_INDEX_PRESETS = {
|
|
8
|
+
'frequency': {
|
|
9
|
+
//low
|
|
10
|
+
'4G_700': 1,
|
|
11
|
+
'5G_700': 3,
|
|
12
|
+
'4G_800': 5,
|
|
13
|
+
'2G_900': 7,
|
|
14
|
+
'4G_900': 9,
|
|
15
|
+
//mid
|
|
16
|
+
'2G_1800': 11,
|
|
17
|
+
'4G_1800': 13,
|
|
18
|
+
'4G_2100': 15,
|
|
19
|
+
'5G_2100': 17,
|
|
20
|
+
'4G_2600': 19,
|
|
21
|
+
'5G_3500': 21,
|
|
22
|
+
},
|
|
23
|
+
'ltePriority': {
|
|
24
|
+
// Low band
|
|
25
|
+
'4G_700': 1,
|
|
26
|
+
'5G_700': 0.99,
|
|
27
|
+
'4G_800': 5,
|
|
28
|
+
'2G_900': 7,
|
|
29
|
+
'4G_900': 9,
|
|
30
|
+
// Mid band
|
|
31
|
+
'4G_1800': 15,
|
|
32
|
+
'2G_1800': 14.99,
|
|
33
|
+
'4G_2100': 19,
|
|
34
|
+
'5G_2100': 18.99,
|
|
35
|
+
'4G_2600': 23,
|
|
36
|
+
// High band
|
|
37
|
+
'5G_3500': 25,
|
|
38
|
+
},
|
|
39
|
+
'technology': {
|
|
40
|
+
// 5G on top
|
|
41
|
+
'5G_700': 30,
|
|
42
|
+
'5G_2100': 32,
|
|
43
|
+
'5G_3500': 34,
|
|
44
|
+
// 4G middle
|
|
45
|
+
'4G_700': 12,
|
|
46
|
+
'4G_800': 15,
|
|
47
|
+
'4G_900': 18,
|
|
48
|
+
'4G_1800': 19,
|
|
49
|
+
'4G_2100': 23,
|
|
50
|
+
'4G_2600': 26,
|
|
51
|
+
// 2G bottom
|
|
52
|
+
'2G_900': 5,
|
|
53
|
+
'2G_1800': 8,
|
|
54
|
+
},
|
|
55
|
+
'balanced': {
|
|
56
|
+
// Mix: newer tech gets boost within frequency range
|
|
57
|
+
'4G_700': 1,
|
|
58
|
+
'5G_700': 3,
|
|
59
|
+
'4G_800': 5,
|
|
60
|
+
'2G_900': 7,
|
|
61
|
+
'4G_900': 10,
|
|
62
|
+
// Mid band
|
|
63
|
+
'2G_1800': 14,
|
|
64
|
+
'4G_1800': 16,
|
|
65
|
+
'4G_2100': 19,
|
|
66
|
+
'5G_2100': 21,
|
|
67
|
+
'4G_2600': 23,
|
|
68
|
+
// High band
|
|
69
|
+
'5G_3500': 27,
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
export const BEAMWIDTH_BOOST_PRESETS = {
|
|
73
|
+
'frequency': {
|
|
74
|
+
'4G_700': 0,
|
|
75
|
+
'5G_700': 0,
|
|
76
|
+
'4G_800': 0,
|
|
77
|
+
'2G_900': 0,
|
|
78
|
+
'4G_900': 0,
|
|
79
|
+
'2G_1800': 0,
|
|
80
|
+
'4G_1800': 0,
|
|
81
|
+
'4G_2100': 0,
|
|
82
|
+
'5G_2100': 0,
|
|
83
|
+
'4G_2600': 0,
|
|
84
|
+
'5G_3500': 0
|
|
85
|
+
},
|
|
86
|
+
'ltePriority': {
|
|
87
|
+
// Boost 5G and 2G for visibility when frequency is priority
|
|
88
|
+
'4G_700': 0,
|
|
89
|
+
'5G_700': 15,
|
|
90
|
+
'4G_800': 0,
|
|
91
|
+
'2G_900': -15,
|
|
92
|
+
'4G_900': 0,
|
|
93
|
+
'2G_1800': 15,
|
|
94
|
+
'4G_1800': 0,
|
|
95
|
+
'4G_2100': 0,
|
|
96
|
+
'5G_2100': 15,
|
|
97
|
+
'4G_2600': 0,
|
|
98
|
+
'5G_3500': 15
|
|
99
|
+
},
|
|
100
|
+
'technology': {
|
|
101
|
+
// No boost needed - z-index separates clearly
|
|
102
|
+
'4G_700': 0,
|
|
103
|
+
'5G_700': 0,
|
|
104
|
+
'4G_800': 0,
|
|
105
|
+
'2G_900': 0,
|
|
106
|
+
'4G_900': 0,
|
|
107
|
+
'2G_1800': 0,
|
|
108
|
+
'4G_1800': 0,
|
|
109
|
+
'4G_2100': 0,
|
|
110
|
+
'5G_2100': 0,
|
|
111
|
+
'4G_2600': 0,
|
|
112
|
+
'5G_3500': 0
|
|
113
|
+
},
|
|
114
|
+
'balanced': {
|
|
115
|
+
// Moderate boost for 5G only
|
|
116
|
+
'4G_700': 0,
|
|
117
|
+
'5G_700': 0,
|
|
118
|
+
'4G_800': 0,
|
|
119
|
+
'2G_900': 0,
|
|
120
|
+
'4G_900': 0,
|
|
121
|
+
'2G_1800': 0,
|
|
122
|
+
'4G_1800': 0,
|
|
123
|
+
'4G_2100': 0,
|
|
124
|
+
'5G_2100': 0,
|
|
125
|
+
'4G_2600': 0,
|
|
126
|
+
'5G_3500': 0
|
|
127
|
+
}
|
|
128
|
+
};
|
|
7
129
|
export const Z_INDEX_BY_BAND = {
|
|
8
130
|
// '2G_900': 1,
|
|
9
131
|
// '2G_1800': 3,
|
|
@@ -59,7 +181,7 @@ export const BEAMWIDTH_BOOST_BY_BAND = {
|
|
|
59
181
|
'5G_2100': 15,
|
|
60
182
|
'4G_2600': 0,
|
|
61
183
|
// High band
|
|
62
|
-
'5G_3500':
|
|
184
|
+
'5G_3500': 15
|
|
63
185
|
};
|
|
64
186
|
/**
|
|
65
187
|
* Base Z-Index for Mapbox layer ordering
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import type { CellDataStore } from '../stores/cell.data.svelte';
|
|
5
5
|
import type { CellRegistry } from '../stores/cell.registry.svelte';
|
|
6
6
|
import type { CellDisplayStore } from '../stores/cell.display.svelte';
|
|
7
|
+
import type { SiteDistanceStore } from '../stores/site.distance.svelte';
|
|
7
8
|
import { groupCells, getColorForGroup } from '../logic/grouping';
|
|
8
|
-
import { generateCellArc, calculateRadiusInMeters } from '../logic/geometry';
|
|
9
|
-
import { Z_INDEX_BY_BAND, BEAMWIDTH_BOOST_BY_BAND } from '../constants';
|
|
9
|
+
import { generateCellArc, calculateRadiusInMeters, calculateAutoRadius } from '../logic/geometry';
|
|
10
10
|
import type { TechnologyBandKey } from '../types';
|
|
11
11
|
import type mapboxgl from 'mapbox-gl';
|
|
12
12
|
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
dataStore: CellDataStore;
|
|
15
15
|
registry: CellRegistry;
|
|
16
16
|
displayStore: CellDisplayStore;
|
|
17
|
+
siteDistanceStore: SiteDistanceStore;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
let { dataStore, registry, displayStore }: Props = $props();
|
|
20
|
+
let { dataStore, registry, displayStore, siteDistanceStore }: Props = $props();
|
|
20
21
|
|
|
21
22
|
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
22
23
|
let sourceId = 'cells-source';
|
|
@@ -127,6 +128,9 @@
|
|
|
127
128
|
const _registryVersion = registry.version;
|
|
128
129
|
const _l1 = displayStore.level1;
|
|
129
130
|
const _l2 = displayStore.level2;
|
|
131
|
+
const _layerGrouping = displayStore.layerGrouping;
|
|
132
|
+
const _useAutoSize = displayStore.useAutoSize;
|
|
133
|
+
const _autoSizeMode = displayStore.autoSizeMode;
|
|
130
134
|
|
|
131
135
|
updateLayer();
|
|
132
136
|
});
|
|
@@ -150,9 +154,9 @@
|
|
|
150
154
|
|
|
151
155
|
console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
|
|
152
156
|
|
|
153
|
-
// 1. Calculate
|
|
154
|
-
const
|
|
155
|
-
console.log(`[CellsLayer]
|
|
157
|
+
// 1. Calculate base radius
|
|
158
|
+
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
159
|
+
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
156
160
|
|
|
157
161
|
// 2. Group cells (Level 1=Tech, Level 2=Band for now hardcoded)
|
|
158
162
|
// In real app, this comes from a store
|
|
@@ -175,22 +179,33 @@
|
|
|
175
179
|
if (bounds.contains([cell.longitude, cell.latitude])) {
|
|
176
180
|
// 5. Z-Index Lookup
|
|
177
181
|
const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
|
|
178
|
-
const zIndex =
|
|
179
|
-
|
|
180
|
-
// 6. Calculate
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
|
|
183
|
+
|
|
184
|
+
// 6. Calculate radius with z-index scaling
|
|
185
|
+
const MAX_Z = 35;
|
|
186
|
+
let radiusMeters: number;
|
|
187
|
+
|
|
188
|
+
if (displayStore.useAutoSize) {
|
|
189
|
+
// Auto-size mode: get target radius for this site
|
|
190
|
+
const siteDistance = siteDistanceStore.getDistance(cell.siteId, 500);
|
|
191
|
+
const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
|
|
192
|
+
|
|
193
|
+
// Scale based on z-index for stacking visibility
|
|
194
|
+
// Lower z-index (background) = larger, higher z-index (foreground) = smaller
|
|
195
|
+
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08; // 8% per layer
|
|
196
|
+
radiusMeters = autoRadius * scaleFactor;
|
|
197
|
+
} else {
|
|
198
|
+
// Manual mode: base from pixel size, then scale by z-index
|
|
199
|
+
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
200
|
+
radiusMeters = baseRadiusMeters * scaleFactor;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 7. Apply beamwidth boost from displayStore preset
|
|
204
|
+
const beamwidthBoost = displayStore.currentBeamwidthBoost[zIndexKey] || 0;
|
|
190
205
|
const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
|
|
191
206
|
|
|
192
207
|
// 8. Generate Arc
|
|
193
|
-
const feature = generateCellArc(cell,
|
|
208
|
+
const feature = generateCellArc(cell, radiusMeters, zIndex, style.color, adjustedBeamwidth);
|
|
194
209
|
features.push(feature);
|
|
195
210
|
}
|
|
196
211
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CellDataStore } from '../stores/cell.data.svelte';
|
|
2
2
|
import type { CellRegistry } from '../stores/cell.registry.svelte';
|
|
3
3
|
import type { CellDisplayStore } from '../stores/cell.display.svelte';
|
|
4
|
+
import type { SiteDistanceStore } from '../stores/site.distance.svelte';
|
|
4
5
|
interface Props {
|
|
5
6
|
dataStore: CellDataStore;
|
|
6
7
|
registry: CellRegistry;
|
|
7
8
|
displayStore: CellDisplayStore;
|
|
9
|
+
siteDistanceStore: SiteDistanceStore;
|
|
8
10
|
}
|
|
9
11
|
declare const CellsLayer: import("svelte").Component<Props, {}, "">;
|
|
10
12
|
type CellsLayer = ReturnType<typeof CellsLayer>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Cell } from '../types';
|
|
1
|
+
import type { Cell, AutoSizeMode } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Calculates the radius in meters required to achieve a target pixel size
|
|
4
4
|
* at a specific latitude and zoom level.
|
|
@@ -6,6 +6,13 @@ import type { Cell } from '../types';
|
|
|
6
6
|
* Formula: meters_per_pixel = 156543.03392 * cos(lat * PI / 180) / 2^zoom
|
|
7
7
|
*/
|
|
8
8
|
export declare function calculateRadiusInMeters(latitude: number, zoom: number, targetPixelSize: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* Calculate auto-size radius based on nearest site distance
|
|
11
|
+
* @param nearestSiteDistance Distance to nearest site in meters
|
|
12
|
+
* @param mode Sizing algorithm to use
|
|
13
|
+
* @returns Recommended radius in meters (base radius before z-index scaling)
|
|
14
|
+
*/
|
|
15
|
+
export declare function calculateAutoRadius(nearestSiteDistance: number, mode?: AutoSizeMode): number;
|
|
9
16
|
/**
|
|
10
17
|
* Generates a sector arc GeoJSON feature for a cell
|
|
11
18
|
*/
|
|
@@ -10,6 +10,38 @@ export function calculateRadiusInMeters(latitude, zoom, targetPixelSize) {
|
|
|
10
10
|
const metersPerPixel = (156543.03392 * Math.cos((latitude * Math.PI) / 180)) / Math.pow(2, zoom);
|
|
11
11
|
return targetPixelSize * metersPerPixel;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Calculate auto-size radius based on nearest site distance
|
|
15
|
+
* @param nearestSiteDistance Distance to nearest site in meters
|
|
16
|
+
* @param mode Sizing algorithm to use
|
|
17
|
+
* @returns Recommended radius in meters (base radius before z-index scaling)
|
|
18
|
+
*/
|
|
19
|
+
export function calculateAutoRadius(nearestSiteDistance, mode = 'logarithmic') {
|
|
20
|
+
switch (mode) {
|
|
21
|
+
case 'logarithmic': {
|
|
22
|
+
// Logarithmic scale with clamping
|
|
23
|
+
// Returns BASE radius - z-index scaling will be applied on top
|
|
24
|
+
const base = Math.log10(Math.max(100, nearestSiteDistance)) * 30;
|
|
25
|
+
return Math.max(30, Math.min(base, 200));
|
|
26
|
+
}
|
|
27
|
+
case 'percentage': {
|
|
28
|
+
// Simple percentage of distance (20% for base, allows z-index scaling on top)
|
|
29
|
+
return Math.max(30, Math.min(nearestSiteDistance * 0.2, 250));
|
|
30
|
+
}
|
|
31
|
+
case 'tiered': {
|
|
32
|
+
// Discrete tiers for consistent sizing (base values)
|
|
33
|
+
if (nearestSiteDistance < 300)
|
|
34
|
+
return 40;
|
|
35
|
+
if (nearestSiteDistance < 600)
|
|
36
|
+
return 80;
|
|
37
|
+
if (nearestSiteDistance < 1200)
|
|
38
|
+
return 120;
|
|
39
|
+
return 180;
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
return 80;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
13
45
|
/**
|
|
14
46
|
* Generates a sector arc GeoJSON feature for a cell
|
|
15
47
|
*/
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Distance Calculation Logic
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for computing inter-site distances
|
|
5
|
+
* Used for auto-sizing cells based on site density
|
|
6
|
+
*/
|
|
7
|
+
import type { Cell } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Calculate distance between two geographic points using Haversine formula
|
|
10
|
+
* @param loc1 [longitude, latitude] of first point
|
|
11
|
+
* @param loc2 [longitude, latitude] of second point
|
|
12
|
+
* @returns Distance in meters
|
|
13
|
+
*/
|
|
14
|
+
export declare function haversineDistance(loc1: [number, number], loc2: [number, number]): number;
|
|
15
|
+
/**
|
|
16
|
+
* Group cells by site ID
|
|
17
|
+
* @param cells Array of cells
|
|
18
|
+
* @returns Map of siteId -> cells at that site
|
|
19
|
+
*/
|
|
20
|
+
export declare function groupBySite(cells: Cell[]): Map<string, Cell[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Extract unique site locations from cells
|
|
23
|
+
* @param cells Array of cells
|
|
24
|
+
* @returns Map of siteId -> [longitude, latitude]
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractSiteLocations(cells: Cell[]): Map<string, [number, number]>;
|
|
27
|
+
/**
|
|
28
|
+
* Compute nearest neighbor distance for a single site
|
|
29
|
+
* @param siteId Site to compute distance for
|
|
30
|
+
* @param siteLocation Location of the site
|
|
31
|
+
* @param allSiteLocations Map of all site locations
|
|
32
|
+
* @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeNearestNeighbor(siteId: string, siteLocation: [number, number], allSiteLocations: Map<string, [number, number]>): number;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Distance Calculation Logic
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for computing inter-site distances
|
|
5
|
+
* Used for auto-sizing cells based on site density
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Calculate distance between two geographic points using Haversine formula
|
|
9
|
+
* @param loc1 [longitude, latitude] of first point
|
|
10
|
+
* @param loc2 [longitude, latitude] of second point
|
|
11
|
+
* @returns Distance in meters
|
|
12
|
+
*/
|
|
13
|
+
export function haversineDistance(loc1, loc2) {
|
|
14
|
+
const [lon1, lat1] = loc1;
|
|
15
|
+
const [lon2, lat2] = loc2;
|
|
16
|
+
const R = 6371e3; // Earth radius in meters
|
|
17
|
+
const φ1 = (lat1 * Math.PI) / 180;
|
|
18
|
+
const φ2 = (lat2 * Math.PI) / 180;
|
|
19
|
+
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
|
20
|
+
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
|
21
|
+
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
|
22
|
+
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
|
23
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
24
|
+
return R * c; // meters
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Group cells by site ID
|
|
28
|
+
* @param cells Array of cells
|
|
29
|
+
* @returns Map of siteId -> cells at that site
|
|
30
|
+
*/
|
|
31
|
+
export function groupBySite(cells) {
|
|
32
|
+
const groups = new Map();
|
|
33
|
+
for (const cell of cells) {
|
|
34
|
+
if (!groups.has(cell.siteId)) {
|
|
35
|
+
groups.set(cell.siteId, []);
|
|
36
|
+
}
|
|
37
|
+
groups.get(cell.siteId).push(cell);
|
|
38
|
+
}
|
|
39
|
+
return groups;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract unique site locations from cells
|
|
43
|
+
* @param cells Array of cells
|
|
44
|
+
* @returns Map of siteId -> [longitude, latitude]
|
|
45
|
+
*/
|
|
46
|
+
export function extractSiteLocations(cells) {
|
|
47
|
+
const sites = groupBySite(cells);
|
|
48
|
+
const locations = new Map();
|
|
49
|
+
for (const [siteId, siteCells] of sites) {
|
|
50
|
+
const first = siteCells[0];
|
|
51
|
+
locations.set(siteId, [first.longitude, first.latitude]);
|
|
52
|
+
}
|
|
53
|
+
return locations;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compute nearest neighbor distance for a single site
|
|
57
|
+
* @param siteId Site to compute distance for
|
|
58
|
+
* @param siteLocation Location of the site
|
|
59
|
+
* @param allSiteLocations Map of all site locations
|
|
60
|
+
* @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
|
|
61
|
+
*/
|
|
62
|
+
export function computeNearestNeighbor(siteId, siteLocation, allSiteLocations) {
|
|
63
|
+
let minDist = Infinity;
|
|
64
|
+
for (const [otherId, otherLoc] of allSiteLocations) {
|
|
65
|
+
if (siteId === otherId)
|
|
66
|
+
continue;
|
|
67
|
+
const dist = haversineDistance(siteLocation, otherLoc);
|
|
68
|
+
minDist = Math.min(minDist, dist);
|
|
69
|
+
}
|
|
70
|
+
return minDist;
|
|
71
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { CellGroupingField } from '../types';
|
|
1
|
+
import type { CellGroupingField, AutoSizeMode } from '../types';
|
|
2
|
+
import type { LayerGroupingPreset } from '../constants';
|
|
2
3
|
export declare class CellDisplayStore {
|
|
3
4
|
key: string;
|
|
4
5
|
targetPixelSize: number;
|
|
5
6
|
fillOpacity: number;
|
|
6
7
|
lineWidth: number;
|
|
7
8
|
showLabels: boolean;
|
|
9
|
+
layerGrouping: LayerGroupingPreset;
|
|
10
|
+
useAutoSize: boolean;
|
|
11
|
+
autoSizeMode: AutoSizeMode;
|
|
8
12
|
level1: CellGroupingField;
|
|
9
13
|
level2: CellGroupingField;
|
|
14
|
+
currentZIndex: Record<string, number>;
|
|
15
|
+
currentBeamwidthBoost: Record<string, number>;
|
|
10
16
|
labelPixelDistance: number;
|
|
11
17
|
labelFontSize: number;
|
|
12
18
|
labelAzimuthTolerance: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { browser } from '$app/environment';
|
|
2
|
+
import { Z_INDEX_PRESETS, BEAMWIDTH_BOOST_PRESETS } from '../constants';
|
|
2
3
|
export class CellDisplayStore {
|
|
3
4
|
key = 'map-v3-cell-display';
|
|
4
5
|
// State
|
|
@@ -6,9 +7,16 @@ export class CellDisplayStore {
|
|
|
6
7
|
fillOpacity = $state(0.6);
|
|
7
8
|
lineWidth = $state(1);
|
|
8
9
|
showLabels = $state(false);
|
|
10
|
+
layerGrouping = $state('frequency');
|
|
11
|
+
// Auto-size settings
|
|
12
|
+
useAutoSize = $state(false);
|
|
13
|
+
autoSizeMode = $state('logarithmic');
|
|
9
14
|
// Grouping
|
|
10
15
|
level1 = $state('tech');
|
|
11
16
|
level2 = $state('fband');
|
|
17
|
+
// Derived preset configurations
|
|
18
|
+
currentZIndex = $derived.by(() => Z_INDEX_PRESETS[this.layerGrouping]);
|
|
19
|
+
currentBeamwidthBoost = $derived.by(() => BEAMWIDTH_BOOST_PRESETS[this.layerGrouping]);
|
|
12
20
|
// Label Settings
|
|
13
21
|
labelPixelDistance = $state(60);
|
|
14
22
|
labelFontSize = $state(12);
|
|
@@ -29,6 +37,9 @@ export class CellDisplayStore {
|
|
|
29
37
|
this.fillOpacity = parsed.fillOpacity ?? 0.6;
|
|
30
38
|
this.lineWidth = parsed.lineWidth ?? 1;
|
|
31
39
|
this.showLabels = parsed.showLabels ?? false;
|
|
40
|
+
this.layerGrouping = parsed.layerGrouping ?? 'frequency';
|
|
41
|
+
this.useAutoSize = parsed.useAutoSize ?? false;
|
|
42
|
+
this.autoSizeMode = parsed.autoSizeMode ?? 'logarithmic';
|
|
32
43
|
this.level1 = parsed.level1 ?? 'tech';
|
|
33
44
|
this.level2 = parsed.level2 ?? 'fband';
|
|
34
45
|
this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
|
|
@@ -50,6 +61,9 @@ export class CellDisplayStore {
|
|
|
50
61
|
fillOpacity: this.fillOpacity,
|
|
51
62
|
lineWidth: this.lineWidth,
|
|
52
63
|
showLabels: this.showLabels,
|
|
64
|
+
layerGrouping: this.layerGrouping,
|
|
65
|
+
useAutoSize: this.useAutoSize,
|
|
66
|
+
autoSizeMode: this.autoSizeMode,
|
|
53
67
|
level1: this.level1,
|
|
54
68
|
level2: this.level2,
|
|
55
69
|
labelPixelDistance: this.labelPixelDistance,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Distance Store
|
|
3
|
+
*
|
|
4
|
+
* Manages cached distances between sites for auto-sizing
|
|
5
|
+
* Incrementally updates when new sites are detected
|
|
6
|
+
*/
|
|
7
|
+
import type { Cell } from '../types';
|
|
8
|
+
export declare class SiteDistanceStore {
|
|
9
|
+
key: string;
|
|
10
|
+
distances: Map<string, number>;
|
|
11
|
+
computedSites: Set<string>;
|
|
12
|
+
lastComputeTime: number;
|
|
13
|
+
computedCount: number;
|
|
14
|
+
constructor();
|
|
15
|
+
/**
|
|
16
|
+
* Incrementally update distances for new sites
|
|
17
|
+
* Only computes distances for sites not in cache
|
|
18
|
+
*/
|
|
19
|
+
updateDistances(cells: Cell[]): void;
|
|
20
|
+
/**
|
|
21
|
+
* Update existing sites if needed (when no new sites but data might have changed)
|
|
22
|
+
*/
|
|
23
|
+
private updateExistingSitesIfNeeded;
|
|
24
|
+
/**
|
|
25
|
+
* Get distance for a site, with fallback
|
|
26
|
+
*/
|
|
27
|
+
getDistance(siteId: string, fallback?: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* Clear all cached distances (useful for testing or data refresh)
|
|
30
|
+
*/
|
|
31
|
+
clear(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Persist to localStorage
|
|
34
|
+
*/
|
|
35
|
+
private persist;
|
|
36
|
+
/**
|
|
37
|
+
* Load from localStorage
|
|
38
|
+
*/
|
|
39
|
+
private load;
|
|
40
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Distance Store
|
|
3
|
+
*
|
|
4
|
+
* Manages cached distances between sites for auto-sizing
|
|
5
|
+
* Incrementally updates when new sites are detected
|
|
6
|
+
*/
|
|
7
|
+
import { browser } from '$app/environment';
|
|
8
|
+
import { extractSiteLocations, computeNearestNeighbor, haversineDistance } from '../logic/site-distance';
|
|
9
|
+
export class SiteDistanceStore {
|
|
10
|
+
key = 'map-v3-site-distances';
|
|
11
|
+
// Cached nearest neighbor distances
|
|
12
|
+
distances = $state(new Map());
|
|
13
|
+
// Track which sites have been computed
|
|
14
|
+
computedSites = $state(new Set());
|
|
15
|
+
// Performance tracking
|
|
16
|
+
lastComputeTime = $state(0);
|
|
17
|
+
computedCount = $state(0);
|
|
18
|
+
constructor() {
|
|
19
|
+
if (browser) {
|
|
20
|
+
this.load();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Incrementally update distances for new sites
|
|
25
|
+
* Only computes distances for sites not in cache
|
|
26
|
+
*/
|
|
27
|
+
updateDistances(cells) {
|
|
28
|
+
if (!cells || cells.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
const startTime = performance.now();
|
|
31
|
+
const siteLocations = extractSiteLocations(cells);
|
|
32
|
+
// Find NEW sites (not in cache)
|
|
33
|
+
const newSites = [...siteLocations.keys()].filter((id) => !this.computedSites.has(id));
|
|
34
|
+
if (newSites.length === 0) {
|
|
35
|
+
// No new sites, but check if we need to update existing sites
|
|
36
|
+
// (in case a new site is closer than previously computed)
|
|
37
|
+
this.updateExistingSitesIfNeeded(siteLocations);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(`[SiteDistance] Computing distances for ${newSites.length} new sites`);
|
|
41
|
+
// Compute distances for NEW sites
|
|
42
|
+
for (const siteId of newSites) {
|
|
43
|
+
const location = siteLocations.get(siteId);
|
|
44
|
+
const nearestDist = computeNearestNeighbor(siteId, location, siteLocations);
|
|
45
|
+
this.distances.set(siteId, nearestDist);
|
|
46
|
+
this.computedSites.add(siteId);
|
|
47
|
+
}
|
|
48
|
+
// Update EXISTING sites if a new site is closer
|
|
49
|
+
for (const [existingId, existingLoc] of siteLocations) {
|
|
50
|
+
if (newSites.includes(existingId))
|
|
51
|
+
continue; // Skip new sites
|
|
52
|
+
let currentMin = this.distances.get(existingId) || Infinity;
|
|
53
|
+
for (const newId of newSites) {
|
|
54
|
+
const newLoc = siteLocations.get(newId);
|
|
55
|
+
const dist = haversineDistance(existingLoc, newLoc);
|
|
56
|
+
currentMin = Math.min(currentMin, dist);
|
|
57
|
+
}
|
|
58
|
+
if (currentMin < (this.distances.get(existingId) || Infinity)) {
|
|
59
|
+
this.distances.set(existingId, currentMin);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const endTime = performance.now();
|
|
63
|
+
this.lastComputeTime = endTime - startTime;
|
|
64
|
+
this.computedCount = this.distances.size;
|
|
65
|
+
console.log(`[SiteDistance] Computed ${newSites.length} new sites in ${this.lastComputeTime.toFixed(1)}ms (total: ${this.computedCount})`);
|
|
66
|
+
this.persist();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Update existing sites if needed (when no new sites but data might have changed)
|
|
70
|
+
*/
|
|
71
|
+
updateExistingSitesIfNeeded(siteLocations) {
|
|
72
|
+
// Check if all computed sites still exist in current data
|
|
73
|
+
const currentSiteIds = new Set(siteLocations.keys());
|
|
74
|
+
let removed = 0;
|
|
75
|
+
for (const computedId of this.computedSites) {
|
|
76
|
+
if (!currentSiteIds.has(computedId)) {
|
|
77
|
+
this.distances.delete(computedId);
|
|
78
|
+
this.computedSites.delete(computedId);
|
|
79
|
+
removed++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (removed > 0) {
|
|
83
|
+
console.log(`[SiteDistance] Removed ${removed} stale sites from cache`);
|
|
84
|
+
this.persist();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get distance for a site, with fallback
|
|
89
|
+
*/
|
|
90
|
+
getDistance(siteId, fallback = 500) {
|
|
91
|
+
return this.distances.get(siteId) ?? fallback;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Clear all cached distances (useful for testing or data refresh)
|
|
95
|
+
*/
|
|
96
|
+
clear() {
|
|
97
|
+
this.distances.clear();
|
|
98
|
+
this.computedSites.clear();
|
|
99
|
+
this.lastComputeTime = 0;
|
|
100
|
+
this.computedCount = 0;
|
|
101
|
+
if (browser) {
|
|
102
|
+
localStorage.removeItem(this.key);
|
|
103
|
+
}
|
|
104
|
+
console.log('[SiteDistance] Cache cleared');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Persist to localStorage
|
|
108
|
+
*/
|
|
109
|
+
persist() {
|
|
110
|
+
if (!browser)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
const data = {
|
|
114
|
+
distances: Array.from(this.distances.entries()),
|
|
115
|
+
computedSites: Array.from(this.computedSites),
|
|
116
|
+
lastComputeTime: this.lastComputeTime,
|
|
117
|
+
computedCount: this.computedCount
|
|
118
|
+
};
|
|
119
|
+
localStorage.setItem(this.key, JSON.stringify(data));
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
console.error('[SiteDistance] Failed to persist cache', e);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load from localStorage
|
|
127
|
+
*/
|
|
128
|
+
load() {
|
|
129
|
+
if (!browser)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
const saved = localStorage.getItem(this.key);
|
|
133
|
+
if (saved) {
|
|
134
|
+
const data = JSON.parse(saved);
|
|
135
|
+
this.distances = new Map(data.distances || []);
|
|
136
|
+
this.computedSites = new Set(data.computedSites || []);
|
|
137
|
+
this.lastComputeTime = data.lastComputeTime || 0;
|
|
138
|
+
this.computedCount = data.computedCount || 0;
|
|
139
|
+
console.log(`[SiteDistance] Loaded ${this.computedCount} cached distances`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
console.error('[SiteDistance] Failed to load cache', e);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -60,3 +60,15 @@ export type TechnologyBandKey = string;
|
|
|
60
60
|
* Grouping fields for Tree View
|
|
61
61
|
*/
|
|
62
62
|
export type CellGroupingField = 'tech' | 'fband' | 'frq' | 'status' | 'siteId' | 'none';
|
|
63
|
+
/**
|
|
64
|
+
* Auto-size calculation modes
|
|
65
|
+
*/
|
|
66
|
+
export type AutoSizeMode = 'logarithmic' | 'percentage' | 'tiered';
|
|
67
|
+
/**
|
|
68
|
+
* Site distance data for auto-sizing
|
|
69
|
+
*/
|
|
70
|
+
export interface SiteDistanceData {
|
|
71
|
+
siteId: string;
|
|
72
|
+
nearestDistance: number;
|
|
73
|
+
computedAt: number;
|
|
74
|
+
}
|