@smartnet360/svelte-components 0.0.101 → 0.0.103
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/antenna-pattern/index.d.ts +1 -0
- package/dist/apps/antenna-pattern/index.js +1 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.d.ts +17 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.js +83 -0
- package/dist/apps/site-check/SiteCheck.svelte +4 -6
- package/dist/core/Charts/ChartCard.svelte +122 -12
- package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
- package/dist/core/Charts/ChartComponent.svelte +8 -6
- package/dist/core/CoverageMap/ai/AITools.d.ts +117 -0
- package/dist/core/CoverageMap/ai/AITools.js +380 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.d.ts +138 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.js +375 -0
- package/dist/core/CoverageMap/core/GridCalculator.d.ts +115 -0
- package/dist/core/CoverageMap/core/GridCalculator.js +484 -0
- package/dist/core/CoverageMap/core/PathLossModels.d.ts +253 -0
- package/dist/core/CoverageMap/core/PathLossModels.js +380 -0
- package/dist/core/CoverageMap/core/SignalProcessor.d.ts +288 -0
- package/dist/core/CoverageMap/core/SignalProcessor.js +424 -0
- package/dist/core/CoverageMap/data/AntennaStore.d.ts +165 -0
- package/dist/core/CoverageMap/data/AntennaStore.js +327 -0
- package/dist/core/CoverageMap/data/SiteStore.d.ts +155 -0
- package/dist/core/CoverageMap/data/SiteStore.js +355 -0
- package/dist/core/CoverageMap/index.d.ts +74 -0
- package/dist/core/CoverageMap/index.js +103 -0
- package/dist/core/CoverageMap/types.d.ts +252 -0
- package/dist/core/CoverageMap/types.js +7 -0
- package/dist/core/CoverageMap/utils/geoUtils.d.ts +223 -0
- package/dist/core/CoverageMap/utils/geoUtils.js +374 -0
- package/dist/core/CoverageMap/utils/rfUtils.d.ts +329 -0
- package/dist/core/CoverageMap/utils/rfUtils.js +434 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.d.ts +149 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.js +377 -0
- package/dist/core/TreeView/index.d.ts +4 -4
- package/dist/core/TreeView/index.js +5 -5
- package/dist/core/TreeView/tree-utils.d.ts +12 -0
- package/dist/core/TreeView/tree-utils.js +115 -6
- package/dist/core/TreeView/tree.store.svelte.d.ts +94 -0
- package/dist/core/TreeView/tree.store.svelte.js +274 -0
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +16 -27
- package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
- package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +33 -42
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +12 -19
- package/dist/map-v3/core/components/Map.svelte +4 -0
- package/dist/map-v3/core/stores/map.store.svelte.js +2 -0
- package/dist/map-v3/demo/DemoMap.svelte +31 -5
- package/dist/map-v3/demo/demo-cells.js +51 -22
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -30
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +29 -9
- package/dist/map-v3/features/cells/logic/geometry.js +3 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +27 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.js +65 -0
- package/dist/map-v3/features/coverage/index.d.ts +12 -0
- package/dist/map-v3/features/coverage/index.js +16 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte +198 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte.d.ts +10 -0
- package/dist/map-v3/features/coverage/logic/coloring.d.ts +28 -0
- package/dist/map-v3/features/coverage/logic/coloring.js +77 -0
- package/dist/map-v3/features/coverage/logic/geometry.d.ts +33 -0
- package/dist/map-v3/features/coverage/logic/geometry.js +112 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.d.ts +46 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.js +95 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.d.ts +33 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.js +90 -0
- package/dist/map-v3/features/coverage/types.d.ts +52 -0
- package/dist/map-v3/features/coverage/types.js +7 -0
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -20
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +82 -65
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +5 -9
- package/dist/map-v3/features/selection/index.d.ts +1 -2
- package/dist/map-v3/features/selection/index.js +0 -1
- package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +44 -15
- package/dist/map-v3/features/selection/stores/selection.store.svelte.js +163 -40
- package/dist/map-v3/features/selection/types.d.ts +4 -2
- package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +23 -33
- package/dist/map-v3/index.d.ts +4 -0
- package/dist/map-v3/index.js +5 -0
- package/package.json +2 -2
- package/dist/core/TreeView/tree.store.d.ts +0 -10
- package/dist/core/TreeView/tree.store.js +0 -320
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid Calculator
|
|
3
|
+
*
|
|
4
|
+
* This module implements grid-based coverage calculation.
|
|
5
|
+
* It divides the area around an antenna into a grid of cells
|
|
6
|
+
* and calculates signal strength at each cell center.
|
|
7
|
+
*
|
|
8
|
+
* Grid-based approach advantages:
|
|
9
|
+
* - Regular, predictable structure
|
|
10
|
+
* - Easy to visualize as heatmap
|
|
11
|
+
* - Efficient for large areas
|
|
12
|
+
* - Simple to parallelize (future enhancement)
|
|
13
|
+
*
|
|
14
|
+
* Process:
|
|
15
|
+
* 1. Create grid around antenna position
|
|
16
|
+
* 2. For each grid cell:
|
|
17
|
+
* a. Calculate distance to antenna
|
|
18
|
+
* b. Calculate bearing to antenna
|
|
19
|
+
* c. Calculate path loss
|
|
20
|
+
* d. Calculate antenna gain in direction
|
|
21
|
+
* e. Calculate received signal strength
|
|
22
|
+
* 3. Aggregate results and generate statistics
|
|
23
|
+
*/
|
|
24
|
+
import { calculateDistance, calculateBearing, calculateDestinationPoint, calculateBounds, calculateBoundsArea } from '../utils/geoUtils';
|
|
25
|
+
import { calculatePathLoss } from './PathLossModels';
|
|
26
|
+
import { calculateSignalStrength, calculateMultiSectorSignal } from './SignalProcessor';
|
|
27
|
+
import { classifySignalQuality } from '../utils/rfUtils';
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// GRID GENERATION
|
|
30
|
+
// ============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Generate grid structure
|
|
33
|
+
*
|
|
34
|
+
* Creates a 2D grid of geographic positions centered on a point.
|
|
35
|
+
* Grid cells are square (in meters), though they appear slightly
|
|
36
|
+
* rectangular on the map due to Earth's curvature.
|
|
37
|
+
*
|
|
38
|
+
* Grid sizing:
|
|
39
|
+
* - Total size = 2 × maxRadius (covers circle)
|
|
40
|
+
* - Number of cells = (2 × maxRadius) / cellSize in each dimension
|
|
41
|
+
* - Example: 10 km radius, 50m cells → 400 × 400 = 160,000 cells
|
|
42
|
+
*
|
|
43
|
+
* Cell numbering:
|
|
44
|
+
* - Rows increase North to South (row 0 = northernmost)
|
|
45
|
+
* - Cols increase West to East (col 0 = westernmost)
|
|
46
|
+
* - [row][col] indexing for 2D array
|
|
47
|
+
*
|
|
48
|
+
* Memory consideration:
|
|
49
|
+
* - Each GridCell object: ~100 bytes
|
|
50
|
+
* - 160,000 cells × 100 bytes = 16 MB
|
|
51
|
+
* - Acceptable for modern browsers
|
|
52
|
+
*
|
|
53
|
+
* @param settings - Grid configuration
|
|
54
|
+
* @returns 2D array of positions (not yet populated with signal data)
|
|
55
|
+
*/
|
|
56
|
+
export function generateGridPositions(settings) {
|
|
57
|
+
const { centerPosition, cellSizeMeters, maxRadiusKm } = settings;
|
|
58
|
+
// Convert cell size from meters to kilometers
|
|
59
|
+
const cellSizeKm = cellSizeMeters / 1000;
|
|
60
|
+
// Calculate how many cells fit in each dimension
|
|
61
|
+
// Add 1 to ensure we cover the full radius
|
|
62
|
+
const numCellsPerSide = Math.ceil((maxRadiusKm * 2) / cellSizeKm);
|
|
63
|
+
// Calculate grid bounds
|
|
64
|
+
const bounds = calculateBounds(centerPosition, maxRadiusKm);
|
|
65
|
+
// Calculate step size in degrees (approximate)
|
|
66
|
+
// This is a simplification; actual degree size varies with latitude
|
|
67
|
+
const latStep = (bounds.north - bounds.south) / numCellsPerSide;
|
|
68
|
+
const lngStep = (bounds.east - bounds.west) / numCellsPerSide;
|
|
69
|
+
// Generate grid
|
|
70
|
+
const grid = [];
|
|
71
|
+
for (let row = 0; row < numCellsPerSide; row++) {
|
|
72
|
+
const rowData = [];
|
|
73
|
+
for (let col = 0; col < numCellsPerSide; col++) {
|
|
74
|
+
// Calculate cell center position
|
|
75
|
+
const lat = bounds.north - row * latStep - latStep / 2;
|
|
76
|
+
const lng = bounds.west + col * lngStep + lngStep / 2;
|
|
77
|
+
rowData.push({ lat, lng });
|
|
78
|
+
}
|
|
79
|
+
grid.push(rowData);
|
|
80
|
+
}
|
|
81
|
+
return grid;
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// SIGNAL CALCULATION FOR SINGLE SECTOR
|
|
85
|
+
// ============================================================================
|
|
86
|
+
/**
|
|
87
|
+
* Calculate coverage grid for a single antenna/sector
|
|
88
|
+
*
|
|
89
|
+
* This is the core coverage calculation function. It:
|
|
90
|
+
* 1. Generates a grid of positions
|
|
91
|
+
* 2. For each position, calculates:
|
|
92
|
+
* - Distance to antenna
|
|
93
|
+
* - Path loss based on distance and model
|
|
94
|
+
* - Signal strength based on antenna pattern
|
|
95
|
+
* 3. Classifies signal quality
|
|
96
|
+
* 4. Generates statistics
|
|
97
|
+
*
|
|
98
|
+
* Progress reporting:
|
|
99
|
+
* - Calls progress callback after each row (or every N cells)
|
|
100
|
+
* - Allows UI to show progress bar
|
|
101
|
+
* - Keeps browser responsive for large grids
|
|
102
|
+
*
|
|
103
|
+
* Optimization notes:
|
|
104
|
+
* - Path loss calculation is expensive (log operations)
|
|
105
|
+
* - Could cache path loss vs distance (future optimization)
|
|
106
|
+
* - Could use Web Workers for parallel calculation
|
|
107
|
+
* - Current implementation: ~0.5ms per cell on modern CPU
|
|
108
|
+
* (160,000 cells = ~80 seconds, acceptable for one-time calculation)
|
|
109
|
+
*
|
|
110
|
+
* @param rfParams - Complete RF configuration
|
|
111
|
+
* @param gridSettings - Grid size and resolution
|
|
112
|
+
* @param thresholds - Signal quality thresholds
|
|
113
|
+
* @param onProgress - Progress callback (optional)
|
|
114
|
+
* @returns Complete coverage grid with signal data
|
|
115
|
+
*/
|
|
116
|
+
export function calculateSingleSectorCoverage(rfParams, gridSettings, thresholds, onProgress) {
|
|
117
|
+
const startTime = Date.now();
|
|
118
|
+
// Report initialization
|
|
119
|
+
onProgress?.({
|
|
120
|
+
stage: 'initializing',
|
|
121
|
+
progress: 0,
|
|
122
|
+
message: 'Generating grid positions...'
|
|
123
|
+
});
|
|
124
|
+
// Generate grid positions
|
|
125
|
+
const positions = generateGridPositions(gridSettings);
|
|
126
|
+
const rows = positions.length;
|
|
127
|
+
const cols = positions[0]?.length || 0;
|
|
128
|
+
const totalCells = rows * cols;
|
|
129
|
+
// Initialize result grid
|
|
130
|
+
const cells = [];
|
|
131
|
+
// Statistics tracking
|
|
132
|
+
let coveredCells = 0;
|
|
133
|
+
let excellentCells = 0;
|
|
134
|
+
let goodCells = 0;
|
|
135
|
+
let fairCells = 0;
|
|
136
|
+
let poorCells = 0;
|
|
137
|
+
let maxRange = 0;
|
|
138
|
+
let signalSum = 0;
|
|
139
|
+
let signalCount = 0;
|
|
140
|
+
// Report calculation start
|
|
141
|
+
onProgress?.({
|
|
142
|
+
stage: 'calculating',
|
|
143
|
+
progress: 0,
|
|
144
|
+
message: 'Calculating coverage...',
|
|
145
|
+
cellsProcessed: 0,
|
|
146
|
+
totalCells
|
|
147
|
+
});
|
|
148
|
+
// Process each grid cell
|
|
149
|
+
for (let row = 0; row < rows; row++) {
|
|
150
|
+
const rowData = [];
|
|
151
|
+
for (let col = 0; col < cols; col++) {
|
|
152
|
+
const position = positions[row][col];
|
|
153
|
+
// Calculate distance to antenna
|
|
154
|
+
const distance = calculateDistance(rfParams.position, position);
|
|
155
|
+
// Skip cells beyond max radius (optimization)
|
|
156
|
+
if (distance > gridSettings.maxRadiusKm) {
|
|
157
|
+
// Cell outside coverage area
|
|
158
|
+
rowData.push({
|
|
159
|
+
position,
|
|
160
|
+
signalStrength: -999,
|
|
161
|
+
quality: 'no-signal',
|
|
162
|
+
sectors: {},
|
|
163
|
+
color: '#CCCCCC' // Gray for no signal
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Calculate path loss
|
|
168
|
+
const pathLoss = calculatePathLoss(gridSettings.pathLossModel, distance, rfParams.frequency, rfParams.position.height);
|
|
169
|
+
// Calculate signal strength
|
|
170
|
+
const signalStrength = calculateSignalStrength(rfParams, position, pathLoss);
|
|
171
|
+
// Classify signal quality
|
|
172
|
+
const quality = classifySignalQuality(signalStrength);
|
|
173
|
+
// Update statistics
|
|
174
|
+
if (signalStrength >= thresholds.edge) {
|
|
175
|
+
coveredCells++;
|
|
176
|
+
signalSum += signalStrength;
|
|
177
|
+
signalCount++;
|
|
178
|
+
// Track max range
|
|
179
|
+
if (distance > maxRange) {
|
|
180
|
+
maxRange = distance;
|
|
181
|
+
}
|
|
182
|
+
// Count by quality
|
|
183
|
+
if (signalStrength >= thresholds.excellent)
|
|
184
|
+
excellentCells++;
|
|
185
|
+
else if (signalStrength >= thresholds.good)
|
|
186
|
+
goodCells++;
|
|
187
|
+
else if (signalStrength >= thresholds.fair)
|
|
188
|
+
fairCells++;
|
|
189
|
+
else
|
|
190
|
+
poorCells++;
|
|
191
|
+
}
|
|
192
|
+
// Create grid cell
|
|
193
|
+
const cell = {
|
|
194
|
+
position,
|
|
195
|
+
signalStrength,
|
|
196
|
+
quality,
|
|
197
|
+
sectors: {
|
|
198
|
+
[rfParams.position.lat + ',' + rfParams.position.lng]: signalStrength
|
|
199
|
+
},
|
|
200
|
+
color: getSignalColor(signalStrength, thresholds)
|
|
201
|
+
};
|
|
202
|
+
rowData.push(cell);
|
|
203
|
+
}
|
|
204
|
+
cells.push(rowData);
|
|
205
|
+
// Report progress after each row
|
|
206
|
+
const progress = ((row + 1) / rows) * 100;
|
|
207
|
+
const cellsProcessed = (row + 1) * cols;
|
|
208
|
+
onProgress?.({
|
|
209
|
+
stage: 'calculating',
|
|
210
|
+
progress,
|
|
211
|
+
message: `Calculating coverage... ${Math.round(progress)}%`,
|
|
212
|
+
cellsProcessed,
|
|
213
|
+
totalCells
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Calculate final statistics
|
|
217
|
+
onProgress?.({
|
|
218
|
+
stage: 'analyzing',
|
|
219
|
+
progress: 99,
|
|
220
|
+
message: 'Generating statistics...'
|
|
221
|
+
});
|
|
222
|
+
const bounds = calculateBounds(gridSettings.centerPosition, gridSettings.maxRadiusKm);
|
|
223
|
+
const totalAreaKm2 = calculateBoundsArea(bounds);
|
|
224
|
+
const cellAreaKm2 = (gridSettings.cellSizeMeters / 1000) ** 2;
|
|
225
|
+
const stats = {
|
|
226
|
+
totalAreaKm2,
|
|
227
|
+
coveredAreaKm2: coveredCells * cellAreaKm2,
|
|
228
|
+
coveragePercentage: (coveredCells / totalCells) * 100,
|
|
229
|
+
excellentAreaKm2: excellentCells * cellAreaKm2,
|
|
230
|
+
goodAreaKm2: goodCells * cellAreaKm2,
|
|
231
|
+
fairAreaKm2: fairCells * cellAreaKm2,
|
|
232
|
+
poorAreaKm2: poorCells * cellAreaKm2,
|
|
233
|
+
sectorStats: {
|
|
234
|
+
single: {
|
|
235
|
+
maxRangeKm: maxRange,
|
|
236
|
+
avgSignalDbm: signalCount > 0 ? signalSum / signalCount : -999,
|
|
237
|
+
coverageAreaKm2: coveredCells * cellAreaKm2
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const calculationTime = Date.now() - startTime;
|
|
242
|
+
onProgress?.({
|
|
243
|
+
stage: 'complete',
|
|
244
|
+
progress: 100,
|
|
245
|
+
message: `Complete! Calculated ${totalCells} cells in ${(calculationTime / 1000).toFixed(1)}s`
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
centerPosition: gridSettings.centerPosition,
|
|
249
|
+
cellSizeMeters: gridSettings.cellSizeMeters,
|
|
250
|
+
bounds,
|
|
251
|
+
rows,
|
|
252
|
+
cols,
|
|
253
|
+
cells,
|
|
254
|
+
stats
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// MULTI-SECTOR COVERAGE
|
|
259
|
+
// ============================================================================
|
|
260
|
+
/**
|
|
261
|
+
* Calculate coverage grid for multiple sectors
|
|
262
|
+
*
|
|
263
|
+
* Common scenario: Cell site with 3 sectors covering 360°.
|
|
264
|
+
* This function calculates signal from all sectors at each point
|
|
265
|
+
* and determines which sector provides strongest signal.
|
|
266
|
+
*
|
|
267
|
+
* Additional capabilities:
|
|
268
|
+
* - Identifies dominant server (which sector serves each location)
|
|
269
|
+
* - Calculates overlap areas (where multiple sectors are strong)
|
|
270
|
+
* - Detects potential interference zones
|
|
271
|
+
*
|
|
272
|
+
* Performance consideration:
|
|
273
|
+
* - Calculates N × grid_size signal strengths (N = number of sectors)
|
|
274
|
+
* - 3 sectors × 160,000 cells = 480,000 calculations
|
|
275
|
+
* - Still manageable (few minutes on modern hardware)
|
|
276
|
+
*
|
|
277
|
+
* @param sectors - Array of sector configurations
|
|
278
|
+
* @param gridSettings - Grid size and resolution
|
|
279
|
+
* @param thresholds - Signal quality thresholds
|
|
280
|
+
* @param onProgress - Progress callback (optional)
|
|
281
|
+
* @returns Coverage grid with multi-sector data
|
|
282
|
+
*/
|
|
283
|
+
export function calculateMultiSectorCoverage(sectors, gridSettings, thresholds, onProgress) {
|
|
284
|
+
const startTime = Date.now();
|
|
285
|
+
// Filter to enabled sectors only
|
|
286
|
+
const activeSectors = sectors.filter((s) => s.enabled);
|
|
287
|
+
if (activeSectors.length === 0) {
|
|
288
|
+
throw new Error('No active sectors to calculate');
|
|
289
|
+
}
|
|
290
|
+
onProgress?.({
|
|
291
|
+
stage: 'initializing',
|
|
292
|
+
progress: 0,
|
|
293
|
+
message: `Initializing ${activeSectors.length} sector calculation...`
|
|
294
|
+
});
|
|
295
|
+
// Generate grid positions
|
|
296
|
+
const positions = generateGridPositions(gridSettings);
|
|
297
|
+
const rows = positions.length;
|
|
298
|
+
const cols = positions[0]?.length || 0;
|
|
299
|
+
const totalCells = rows * cols;
|
|
300
|
+
// Initialize result grid and statistics
|
|
301
|
+
const cells = [];
|
|
302
|
+
const sectorStats = {};
|
|
303
|
+
// Initialize per-sector statistics
|
|
304
|
+
for (const sector of activeSectors) {
|
|
305
|
+
sectorStats[sector.sectorId] = {
|
|
306
|
+
maxRangeKm: 0,
|
|
307
|
+
avgSignalDbm: 0,
|
|
308
|
+
coverageAreaKm2: 0
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
let totalSignalSum = {};
|
|
312
|
+
let totalSignalCount = {};
|
|
313
|
+
let coveredCells = 0;
|
|
314
|
+
let excellentCells = 0;
|
|
315
|
+
let goodCells = 0;
|
|
316
|
+
let fairCells = 0;
|
|
317
|
+
let poorCells = 0;
|
|
318
|
+
for (const sector of activeSectors) {
|
|
319
|
+
totalSignalSum[sector.sectorId] = 0;
|
|
320
|
+
totalSignalCount[sector.sectorId] = 0;
|
|
321
|
+
}
|
|
322
|
+
// Calculate overlap tracking
|
|
323
|
+
const overlapAreas = [];
|
|
324
|
+
// Process each grid cell
|
|
325
|
+
for (let row = 0; row < rows; row++) {
|
|
326
|
+
const rowData = [];
|
|
327
|
+
for (let col = 0; col < cols; col++) {
|
|
328
|
+
const position = positions[row][col];
|
|
329
|
+
// Calculate path losses for each sector
|
|
330
|
+
const pathLosses = [];
|
|
331
|
+
for (const sector of activeSectors) {
|
|
332
|
+
const distance = calculateDistance(sector.position, position);
|
|
333
|
+
if (distance > gridSettings.maxRadiusKm) {
|
|
334
|
+
pathLosses.push(999); // Effectively infinite loss
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const pathLoss = calculatePathLoss(gridSettings.pathLossModel, distance, sector.frequency, sector.position.height);
|
|
338
|
+
pathLosses.push(pathLoss);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Calculate signal from all sectors
|
|
342
|
+
const result = calculateMultiSectorSignal(activeSectors.map((s) => ({ ...s, sectorId: s.sectorId })), position, pathLosses);
|
|
343
|
+
const { signals, dominantSector, dominantSignal } = result;
|
|
344
|
+
// Classify signal quality based on best signal
|
|
345
|
+
const quality = classifySignalQuality(dominantSignal);
|
|
346
|
+
// Update statistics
|
|
347
|
+
if (dominantSignal >= thresholds.edge && dominantSector) {
|
|
348
|
+
coveredCells++;
|
|
349
|
+
// Update per-sector stats
|
|
350
|
+
for (const sectorId in signals) {
|
|
351
|
+
const signal = signals[sectorId];
|
|
352
|
+
totalSignalSum[sectorId] += signal;
|
|
353
|
+
totalSignalCount[sectorId]++;
|
|
354
|
+
// Update max range for dominant sector
|
|
355
|
+
if (sectorId === dominantSector) {
|
|
356
|
+
const sector = activeSectors.find((s) => s.sectorId === sectorId);
|
|
357
|
+
if (sector) {
|
|
358
|
+
const distance = calculateDistance(sector.position, position);
|
|
359
|
+
if (distance > sectorStats[sectorId].maxRangeKm) {
|
|
360
|
+
sectorStats[sectorId].maxRangeKm = distance;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Count by quality
|
|
366
|
+
if (dominantSignal >= thresholds.excellent)
|
|
367
|
+
excellentCells++;
|
|
368
|
+
else if (dominantSignal >= thresholds.good)
|
|
369
|
+
goodCells++;
|
|
370
|
+
else if (dominantSignal >= thresholds.fair)
|
|
371
|
+
fairCells++;
|
|
372
|
+
else
|
|
373
|
+
poorCells++;
|
|
374
|
+
}
|
|
375
|
+
// Create grid cell
|
|
376
|
+
const cell = {
|
|
377
|
+
position,
|
|
378
|
+
signalStrength: dominantSignal,
|
|
379
|
+
quality,
|
|
380
|
+
dominantSector: dominantSector || undefined,
|
|
381
|
+
sectors: signals,
|
|
382
|
+
color: getSignalColor(dominantSignal, thresholds)
|
|
383
|
+
};
|
|
384
|
+
rowData.push(cell);
|
|
385
|
+
}
|
|
386
|
+
cells.push(rowData);
|
|
387
|
+
// Report progress
|
|
388
|
+
const progress = ((row + 1) / rows) * 100;
|
|
389
|
+
onProgress?.({
|
|
390
|
+
stage: 'calculating',
|
|
391
|
+
progress,
|
|
392
|
+
message: `Calculating multi-sector coverage... ${Math.round(progress)}%`,
|
|
393
|
+
cellsProcessed: (row + 1) * cols,
|
|
394
|
+
totalCells
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
// Finalize statistics
|
|
398
|
+
const bounds = calculateBounds(gridSettings.centerPosition, gridSettings.maxRadiusKm);
|
|
399
|
+
const totalAreaKm2 = calculateBoundsArea(bounds);
|
|
400
|
+
const cellAreaKm2 = (gridSettings.cellSizeMeters / 1000) ** 2;
|
|
401
|
+
// Calculate average signals
|
|
402
|
+
for (const sectorId in sectorStats) {
|
|
403
|
+
if (totalSignalCount[sectorId] > 0) {
|
|
404
|
+
sectorStats[sectorId].avgSignalDbm = totalSignalSum[sectorId] / totalSignalCount[sectorId];
|
|
405
|
+
sectorStats[sectorId].coverageAreaKm2 = totalSignalCount[sectorId] * cellAreaKm2;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const stats = {
|
|
409
|
+
totalAreaKm2,
|
|
410
|
+
coveredAreaKm2: coveredCells * cellAreaKm2,
|
|
411
|
+
coveragePercentage: (coveredCells / totalCells) * 100,
|
|
412
|
+
excellentAreaKm2: excellentCells * cellAreaKm2,
|
|
413
|
+
goodAreaKm2: goodCells * cellAreaKm2,
|
|
414
|
+
fairAreaKm2: fairCells * cellAreaKm2,
|
|
415
|
+
poorAreaKm2: poorCells * cellAreaKm2,
|
|
416
|
+
sectorStats,
|
|
417
|
+
overlapAreas
|
|
418
|
+
};
|
|
419
|
+
const calculationTime = Date.now() - startTime;
|
|
420
|
+
onProgress?.({
|
|
421
|
+
stage: 'complete',
|
|
422
|
+
progress: 100,
|
|
423
|
+
message: `Complete! Calculated ${activeSectors.length} sectors in ${(calculationTime / 1000).toFixed(1)}s`
|
|
424
|
+
});
|
|
425
|
+
return {
|
|
426
|
+
centerPosition: gridSettings.centerPosition,
|
|
427
|
+
cellSizeMeters: gridSettings.cellSizeMeters,
|
|
428
|
+
bounds,
|
|
429
|
+
rows,
|
|
430
|
+
cols,
|
|
431
|
+
cells,
|
|
432
|
+
stats
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// ============================================================================
|
|
436
|
+
// UTILITY FUNCTIONS
|
|
437
|
+
// ============================================================================
|
|
438
|
+
/**
|
|
439
|
+
* Get color for signal strength (heatmap coloring)
|
|
440
|
+
*
|
|
441
|
+
* Maps signal strength to a color for visualization.
|
|
442
|
+
* Uses a gradient from red (strong) → yellow → green → blue (weak)
|
|
443
|
+
*
|
|
444
|
+
* @param signalDbm - Signal strength in dBm
|
|
445
|
+
* @param thresholds - Quality thresholds
|
|
446
|
+
* @returns Hex color string
|
|
447
|
+
*/
|
|
448
|
+
function getSignalColor(signalDbm, thresholds) {
|
|
449
|
+
if (signalDbm >= thresholds.excellent) {
|
|
450
|
+
return '#FF0000'; // Red - Excellent
|
|
451
|
+
}
|
|
452
|
+
else if (signalDbm >= thresholds.good) {
|
|
453
|
+
return '#FFA500'; // Orange - Good
|
|
454
|
+
}
|
|
455
|
+
else if (signalDbm >= thresholds.fair) {
|
|
456
|
+
return '#FFFF00'; // Yellow - Fair
|
|
457
|
+
}
|
|
458
|
+
else if (signalDbm >= thresholds.edge) {
|
|
459
|
+
return '#00FF00'; // Green - Poor but connected
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
return '#CCCCCC'; // Gray - No signal
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Extract summary metrics for AI analysis
|
|
467
|
+
*
|
|
468
|
+
* Converts coverage grid into structured metrics suitable for
|
|
469
|
+
* AI interpretation and recommendation generation.
|
|
470
|
+
*
|
|
471
|
+
* @param grid - Coverage grid
|
|
472
|
+
* @returns Metrics object
|
|
473
|
+
*/
|
|
474
|
+
export function extractMetrics(grid) {
|
|
475
|
+
return {
|
|
476
|
+
coveragePercentage: grid.stats.coveragePercentage,
|
|
477
|
+
excellentPercent: (grid.stats.excellentAreaKm2 / grid.stats.totalAreaKm2) * 100,
|
|
478
|
+
goodPercent: (grid.stats.goodAreaKm2 / grid.stats.totalAreaKm2) * 100,
|
|
479
|
+
fairPercent: (grid.stats.fairAreaKm2 / grid.stats.totalAreaKm2) * 100,
|
|
480
|
+
poorPercent: (grid.stats.poorAreaKm2 / grid.stats.totalAreaKm2) * 100,
|
|
481
|
+
maxRange: Object.values(grid.stats.sectorStats)[0]?.maxRangeKm || 0,
|
|
482
|
+
avgSignal: Object.values(grid.stats.sectorStats)[0]?.avgSignalDbm || -999
|
|
483
|
+
};
|
|
484
|
+
}
|