@smartnet360/svelte-components 0.0.102 → 0.0.104

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.
Files changed (85) hide show
  1. package/dist/apps/antenna-pattern/index.d.ts +1 -0
  2. package/dist/apps/antenna-pattern/index.js +1 -0
  3. package/dist/apps/antenna-pattern/utils/load-static-antennas.d.ts +17 -0
  4. package/dist/apps/antenna-pattern/utils/load-static-antennas.js +83 -0
  5. package/dist/apps/site-check/SiteCheck.svelte +13 -81
  6. package/dist/apps/site-check/SiteCheckControls.svelte +0 -7
  7. package/dist/apps/site-check/helper.js +0 -33
  8. package/dist/apps/site-check/transforms.js +15 -65
  9. package/dist/core/CellTable/CellTable.svelte +456 -0
  10. package/dist/core/CellTable/CellTable.svelte.d.ts +27 -0
  11. package/dist/core/CellTable/CellTablePanel.svelte +211 -0
  12. package/dist/core/CellTable/CellTablePanel.svelte.d.ts +49 -0
  13. package/dist/core/CellTable/CellTableToolbar.svelte +218 -0
  14. package/dist/core/CellTable/CellTableToolbar.svelte.d.ts +32 -0
  15. package/dist/core/CellTable/column-config.d.ts +63 -0
  16. package/dist/core/CellTable/column-config.js +465 -0
  17. package/dist/core/CellTable/index.d.ts +10 -0
  18. package/dist/core/CellTable/index.js +11 -0
  19. package/dist/core/CellTable/types.d.ts +166 -0
  20. package/dist/core/CellTable/types.js +6 -0
  21. package/dist/core/Charts/ChartCard.svelte +118 -31
  22. package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
  23. package/dist/core/Charts/ChartComponent.svelte +8 -31
  24. package/dist/core/Charts/data-processor.js +1 -19
  25. package/dist/core/CoverageMap/ai/AITools.d.ts +117 -0
  26. package/dist/core/CoverageMap/ai/AITools.js +380 -0
  27. package/dist/core/CoverageMap/core/CoverageCalculator.d.ts +138 -0
  28. package/dist/core/CoverageMap/core/CoverageCalculator.js +375 -0
  29. package/dist/core/CoverageMap/core/GridCalculator.d.ts +115 -0
  30. package/dist/core/CoverageMap/core/GridCalculator.js +484 -0
  31. package/dist/core/CoverageMap/core/PathLossModels.d.ts +253 -0
  32. package/dist/core/CoverageMap/core/PathLossModels.js +380 -0
  33. package/dist/core/CoverageMap/core/SignalProcessor.d.ts +288 -0
  34. package/dist/core/CoverageMap/core/SignalProcessor.js +424 -0
  35. package/dist/core/CoverageMap/data/AntennaStore.d.ts +165 -0
  36. package/dist/core/CoverageMap/data/AntennaStore.js +327 -0
  37. package/dist/core/CoverageMap/data/SiteStore.d.ts +155 -0
  38. package/dist/core/CoverageMap/data/SiteStore.js +355 -0
  39. package/dist/core/CoverageMap/index.d.ts +74 -0
  40. package/dist/core/CoverageMap/index.js +103 -0
  41. package/dist/core/CoverageMap/types.d.ts +252 -0
  42. package/dist/core/CoverageMap/types.js +7 -0
  43. package/dist/core/CoverageMap/utils/geoUtils.d.ts +223 -0
  44. package/dist/core/CoverageMap/utils/geoUtils.js +374 -0
  45. package/dist/core/CoverageMap/utils/rfUtils.d.ts +329 -0
  46. package/dist/core/CoverageMap/utils/rfUtils.js +434 -0
  47. package/dist/core/CoverageMap/visualization/ColorSchemes.d.ts +149 -0
  48. package/dist/core/CoverageMap/visualization/ColorSchemes.js +377 -0
  49. package/dist/core/TreeView/index.d.ts +4 -4
  50. package/dist/core/TreeView/index.js +5 -5
  51. package/dist/core/TreeView/tree-utils.d.ts +12 -0
  52. package/dist/core/TreeView/tree-utils.js +115 -6
  53. package/dist/core/TreeView/tree.store.svelte.d.ts +94 -0
  54. package/dist/core/TreeView/tree.store.svelte.js +274 -0
  55. package/dist/core/index.d.ts +1 -0
  56. package/dist/core/index.js +2 -0
  57. package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +16 -27
  58. package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +33 -42
  59. package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +12 -19
  60. package/dist/map-v3/core/components/Map.svelte +4 -0
  61. package/dist/map-v3/core/stores/map.store.svelte.js +2 -0
  62. package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -30
  63. package/dist/map-v3/features/coverage/index.d.ts +12 -0
  64. package/dist/map-v3/features/coverage/index.js +16 -0
  65. package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte +198 -0
  66. package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte.d.ts +10 -0
  67. package/dist/map-v3/features/coverage/logic/coloring.d.ts +28 -0
  68. package/dist/map-v3/features/coverage/logic/coloring.js +77 -0
  69. package/dist/map-v3/features/coverage/logic/geometry.d.ts +33 -0
  70. package/dist/map-v3/features/coverage/logic/geometry.js +112 -0
  71. package/dist/map-v3/features/coverage/stores/coverage.data.svelte.d.ts +46 -0
  72. package/dist/map-v3/features/coverage/stores/coverage.data.svelte.js +95 -0
  73. package/dist/map-v3/features/coverage/stores/coverage.display.svelte.d.ts +33 -0
  74. package/dist/map-v3/features/coverage/stores/coverage.display.svelte.js +90 -0
  75. package/dist/map-v3/features/coverage/types.d.ts +52 -0
  76. package/dist/map-v3/features/coverage/types.js +7 -0
  77. package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -20
  78. package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +23 -33
  79. package/dist/map-v3/index.d.ts +4 -0
  80. package/dist/map-v3/index.js +5 -0
  81. package/package.json +4 -3
  82. package/dist/apps/site-check/transforms-old.d.ts +0 -56
  83. package/dist/apps/site-check/transforms-old.js +0 -273
  84. package/dist/core/TreeView/tree.store.d.ts +0 -10
  85. package/dist/core/TreeView/tree.store.js +0 -320
@@ -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
+ }