@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.
Files changed (81) 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 +4 -6
  6. package/dist/core/Charts/ChartCard.svelte +122 -12
  7. package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
  8. package/dist/core/Charts/ChartComponent.svelte +8 -6
  9. package/dist/core/CoverageMap/ai/AITools.d.ts +117 -0
  10. package/dist/core/CoverageMap/ai/AITools.js +380 -0
  11. package/dist/core/CoverageMap/core/CoverageCalculator.d.ts +138 -0
  12. package/dist/core/CoverageMap/core/CoverageCalculator.js +375 -0
  13. package/dist/core/CoverageMap/core/GridCalculator.d.ts +115 -0
  14. package/dist/core/CoverageMap/core/GridCalculator.js +484 -0
  15. package/dist/core/CoverageMap/core/PathLossModels.d.ts +253 -0
  16. package/dist/core/CoverageMap/core/PathLossModels.js +380 -0
  17. package/dist/core/CoverageMap/core/SignalProcessor.d.ts +288 -0
  18. package/dist/core/CoverageMap/core/SignalProcessor.js +424 -0
  19. package/dist/core/CoverageMap/data/AntennaStore.d.ts +165 -0
  20. package/dist/core/CoverageMap/data/AntennaStore.js +327 -0
  21. package/dist/core/CoverageMap/data/SiteStore.d.ts +155 -0
  22. package/dist/core/CoverageMap/data/SiteStore.js +355 -0
  23. package/dist/core/CoverageMap/index.d.ts +74 -0
  24. package/dist/core/CoverageMap/index.js +103 -0
  25. package/dist/core/CoverageMap/types.d.ts +252 -0
  26. package/dist/core/CoverageMap/types.js +7 -0
  27. package/dist/core/CoverageMap/utils/geoUtils.d.ts +223 -0
  28. package/dist/core/CoverageMap/utils/geoUtils.js +374 -0
  29. package/dist/core/CoverageMap/utils/rfUtils.d.ts +329 -0
  30. package/dist/core/CoverageMap/utils/rfUtils.js +434 -0
  31. package/dist/core/CoverageMap/visualization/ColorSchemes.d.ts +149 -0
  32. package/dist/core/CoverageMap/visualization/ColorSchemes.js +377 -0
  33. package/dist/core/TreeView/index.d.ts +4 -4
  34. package/dist/core/TreeView/index.js +5 -5
  35. package/dist/core/TreeView/tree-utils.d.ts +12 -0
  36. package/dist/core/TreeView/tree-utils.js +115 -6
  37. package/dist/core/TreeView/tree.store.svelte.d.ts +94 -0
  38. package/dist/core/TreeView/tree.store.svelte.js +274 -0
  39. package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +16 -27
  40. package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
  41. package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +33 -42
  42. package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +12 -19
  43. package/dist/map-v3/core/components/Map.svelte +4 -0
  44. package/dist/map-v3/core/stores/map.store.svelte.js +2 -0
  45. package/dist/map-v3/demo/DemoMap.svelte +31 -5
  46. package/dist/map-v3/demo/demo-cells.js +51 -22
  47. package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -30
  48. package/dist/map-v3/features/cells/layers/CellsLayer.svelte +29 -9
  49. package/dist/map-v3/features/cells/logic/geometry.js +3 -0
  50. package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +27 -0
  51. package/dist/map-v3/features/cells/stores/cell.data.svelte.js +65 -0
  52. package/dist/map-v3/features/coverage/index.d.ts +12 -0
  53. package/dist/map-v3/features/coverage/index.js +16 -0
  54. package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte +198 -0
  55. package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte.d.ts +10 -0
  56. package/dist/map-v3/features/coverage/logic/coloring.d.ts +28 -0
  57. package/dist/map-v3/features/coverage/logic/coloring.js +77 -0
  58. package/dist/map-v3/features/coverage/logic/geometry.d.ts +33 -0
  59. package/dist/map-v3/features/coverage/logic/geometry.js +112 -0
  60. package/dist/map-v3/features/coverage/stores/coverage.data.svelte.d.ts +46 -0
  61. package/dist/map-v3/features/coverage/stores/coverage.data.svelte.js +95 -0
  62. package/dist/map-v3/features/coverage/stores/coverage.display.svelte.d.ts +33 -0
  63. package/dist/map-v3/features/coverage/stores/coverage.display.svelte.js +90 -0
  64. package/dist/map-v3/features/coverage/types.d.ts +52 -0
  65. package/dist/map-v3/features/coverage/types.js +7 -0
  66. package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -20
  67. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +82 -65
  68. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +5 -9
  69. package/dist/map-v3/features/selection/index.d.ts +1 -2
  70. package/dist/map-v3/features/selection/index.js +0 -1
  71. package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +44 -15
  72. package/dist/map-v3/features/selection/stores/selection.store.svelte.js +163 -40
  73. package/dist/map-v3/features/selection/types.d.ts +4 -2
  74. package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +23 -33
  75. package/dist/map-v3/index.d.ts +4 -0
  76. package/dist/map-v3/index.js +5 -0
  77. package/package.json +2 -2
  78. package/dist/core/TreeView/tree.store.d.ts +0 -10
  79. package/dist/core/TreeView/tree.store.js +0 -320
  80. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
  81. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
@@ -102,11 +102,37 @@
102
102
  <FeatureSelectionControl
103
103
  position="bottom-left"
104
104
  cellDataStore={cellData}
105
- cellDisplayStore={cellDisplay}
106
- title="Feature Selection"
107
- actionButtonLabel="Export Selected"
108
- onAction={(ids) => console.log('Selected features:', ids)}
109
- />
105
+ title="Cell Selection"
106
+ featureIcon="📡"
107
+ defaultSelectionMode="site"
108
+ >
109
+ {#snippet children(selectedIds)}
110
+ <button
111
+ type="button"
112
+ class="btn btn-primary w-100 mb-2"
113
+ disabled={selectedIds.length === 0}
114
+ onclick={() => console.log('Process Selected:', selectedIds)}
115
+ >
116
+ <i class="bi bi-gear-fill"></i> Process ({selectedIds.length})
117
+ </button>
118
+ <button
119
+ type="button"
120
+ class="btn btn-outline-primary w-100 mb-2"
121
+ disabled={selectedIds.length === 0}
122
+ onclick={() => console.log('Export Selected:', selectedIds)}
123
+ >
124
+ <i class="bi bi-download"></i> Export Data
125
+ </button>
126
+ <button
127
+ type="button"
128
+ class="btn btn-outline-secondary w-100"
129
+ disabled={selectedIds.length === 0}
130
+ onclick={() => console.log('Analyze Selected:', selectedIds)}
131
+ >
132
+ <i class="bi bi-graph-up"></i> Analyze
133
+ </button>
134
+ {/snippet}
135
+ </FeatureSelectionControl>
110
136
  </Map>
111
137
  </div>
112
138
 
@@ -87,8 +87,13 @@ const TECH_BANDS = [
87
87
  { tech: '5G', band: '2100', fband: '5G-2100' },
88
88
  { tech: '5G', band: '3500', fband: '5G-3500' }
89
89
  ];
90
- // Three sector azimuths
91
- const AZIMUTHS = [0, 120, 240];
90
+ // Three sector azimuths with sector numbers
91
+ // Sector 1 = 0°, Sector 2 = 120°, Sector 3 = 240°
92
+ const SECTORS = [
93
+ { azimuth: 0, sectorNum: 1 },
94
+ { azimuth: 120, sectorNum: 2 },
95
+ { azimuth: 240, sectorNum: 3 }
96
+ ];
92
97
  // Status rotation for variety
93
98
  const STATUSES = [
94
99
  'On_Air',
@@ -104,6 +109,18 @@ const STATUSES = [
104
109
  'On_Air',
105
110
  'On_Air'
106
111
  ];
112
+ /**
113
+ * Generate 7-digit cellName from site and sector and band index
114
+ * Format: SSSS S BB
115
+ * SSSS = 4-digit site ID
116
+ * S = 1-digit sector number (1, 2, or 3)
117
+ * BB = 2-digit band/cell index (41-52 for 11 tech-bands)
118
+ */
119
+ function generateCellName(siteNum, sectorNum, bandIndex) {
120
+ const siteId = String(siteNum + 1000).padStart(4, '0'); // Start sites from 1000
121
+ const cellSuffix = String(41 + bandIndex).padStart(2, '0'); // Bands: 41-52
122
+ return `${siteId}${sectorNum}${cellSuffix}`;
123
+ }
107
124
  /**
108
125
  * Generate demo cells with varied density patterns in circular distribution
109
126
  * Creates density zones radiating from center with random placement
@@ -144,23 +161,26 @@ for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; at
144
161
  const siteLng = addJitter(lng, jitterAmount);
145
162
  // Record position
146
163
  usedPositions.push({ lat: siteLat, lng: siteLng, minSpacing });
147
- const siteId = `DEMO-SITE-${String(actualSiteIndex + 1).padStart(4, '0')}`;
164
+ // 4-digit site ID (starting from 1000)
165
+ const siteNum = actualSiteIndex;
166
+ const siteId = String(siteNum + 1000).padStart(4, '0');
148
167
  actualSiteIndex++;
149
168
  // Generate 3 sectors per site (with some random 1 or 2 sector sites)
150
169
  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) => {
153
- // Generate 12 tech-bands per sector
154
- TECH_BANDS.forEach((techBand, techIndex) => {
155
- const cellId = `CELL-${String(cellCounter).padStart(4, '0')}`;
156
- const status = STATUSES[techIndex];
170
+ const sectorsToGenerate = SECTORS.slice(0, numSectors);
171
+ sectorsToGenerate.forEach((sector) => {
172
+ // Generate 11 tech-bands per sector (indexes 0-10)
173
+ TECH_BANDS.forEach((techBand, bandIndex) => {
174
+ // Generate 7-digit cellName: SSSS + S + BB (e.g., "1000141")
175
+ const cellName = generateCellName(siteNum, sector.sectorNum, bandIndex);
176
+ const status = STATUSES[bandIndex];
157
177
  demoCells.push({
158
178
  // Core properties
159
- id: cellId,
160
- txId: `TX-${String(cellCounter).padStart(4, '0')}`,
161
- cellID: cellId,
162
- cellID2G: techBand.tech === '2G' ? cellId : '',
163
- cellName: `${siteId} ${techBand.tech}${techBand.band} S${sectorIndex + 1}`,
179
+ id: cellName,
180
+ txId: cellName,
181
+ cellID: cellName,
182
+ cellID2G: techBand.tech === '2G' ? cellName : '',
183
+ cellName: cellName,
164
184
  siteId: siteId,
165
185
  tech: techBand.tech,
166
186
  fband: techBand.fband,
@@ -169,13 +189,13 @@ for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; at
169
189
  status: status,
170
190
  onAirDate: '2024-01-15',
171
191
  // 2G specific
172
- bcch: techBand.tech === '2G' ? 100 + techIndex : 0,
173
- ctrlid: techBand.tech === '2G' ? `CTRL-${cellId}` : '',
192
+ bcch: techBand.tech === '2G' ? 100 + bandIndex : 0,
193
+ ctrlid: techBand.tech === '2G' ? `CTRL-${cellName}` : '',
174
194
  // 4G specific
175
- dlEarfn: techBand.tech === '4G' ? 6200 + techIndex * 100 : 0,
195
+ dlEarfn: techBand.tech === '4G' ? 6200 + bandIndex * 100 : 0,
176
196
  // Physical properties
177
197
  antenna: 'DEMO-ANTENNA-MODEL',
178
- azimuth: azimuth,
198
+ azimuth: sector.azimuth,
179
199
  height: 30, // 30 meters antenna height
180
200
  electricalTilt: '3',
181
201
  beamwidth: BEAMWIDTH,
@@ -186,7 +206,7 @@ for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; at
186
206
  siteLatitude: siteLat,
187
207
  siteLongitude: siteLng,
188
208
  // Planning
189
- comment: `Demo ${techBand.tech} ${techBand.band} cell at azimuth ${azimuth}°`,
209
+ comment: `Demo ${techBand.tech} ${techBand.band} cell at azimuth ${sector.azimuth}°`,
190
210
  planner: 'Demo User',
191
211
  // Atoll properties
192
212
  atollETP: 43.0,
@@ -194,7 +214,7 @@ for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; at
194
214
  atollRS: 500.0 + (techBand.band === '700' ? 200 : 0), // Lower freq = longer range
195
215
  atollBW: parseFloat(techBand.band) / 100, // Simplified bandwidth
196
216
  // Network properties
197
- cellId3: `${cellId}-3G`,
217
+ cellId3: `${cellName}-3G`,
198
218
  nwtP1: 20,
199
219
  nwtP2: 40,
200
220
  pci1: (cellCounter % 504), // Physical Cell ID for LTE
@@ -204,14 +224,23 @@ for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; at
204
224
  other: {
205
225
  demoCell: true,
206
226
  siteNumber: actualSiteIndex,
207
- sector: sectorIndex + 1,
227
+ sector: sector.sectorNum,
208
228
  techBandKey: `${techBand.tech}_${techBand.band}`,
209
229
  radius: normalizedRadius,
210
230
  densityZone: zone.name
211
231
  },
212
- customSubgroup: `Sector-${sectorIndex + 1}`
232
+ customSubgroup: `Sector-${sector.sectorNum}`
213
233
  });
214
234
  cellCounter++;
215
235
  });
216
236
  });
217
237
  }
238
+ // Summary of generated data structure
239
+ console.log(`[Demo Data] Generated ${demoCells.length} cells across ${actualSiteIndex} sites`);
240
+ console.log(`[Demo Data] CellName format: 7-digit numeric (e.g., "1000141")`);
241
+ console.log(`[Demo Data] Structure: SSSS (site) + S (sector 1-3) + BB (band 41-51)`);
242
+ console.log(`[Demo Data] Site ID range: 1000-${1000 + actualSiteIndex - 1}`);
243
+ console.log(`[Demo Data] Example Site 1000:`);
244
+ console.log(` - Sector 1 (0°): 1000141-1000151 (11 bands)`);
245
+ console.log(` - Sector 2 (120°): 1000241-1000251 (11 bands)`);
246
+ console.log(` - Sector 3 (240°): 1000341-1000351 (11 bands)`);
@@ -1,8 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy, untrack } from 'svelte';
3
3
  import { MapControl } from '../../../shared';
4
- import { createTreeStore } from '../../../../core/TreeView/tree.store';
5
- import TreeView from '../../../../core/TreeView/TreeView.svelte';
4
+ import { createTreeStore, TreeView } from '../../../../core/TreeView';
6
5
  import type { CellDataStore } from '../stores/cell.data.svelte';
7
6
  import type { CellRegistry } from '../stores/cell.registry.svelte';
8
7
  import type { CellDisplayStore } from '../stores/cell.display.svelte';
@@ -64,36 +63,31 @@
64
63
 
65
64
  // Sync Tree Selection -> Cell Registry Visibility
66
65
  $effect(() => {
67
- const unsubscribe = treeStore.subscribe((val) => {
68
- // Iterate all leaf nodes to sync visibility
69
- // This is a bit heavy but ensures consistency.
70
- // Optimization: Only update changed nodes if we had a diff.
66
+ const val = treeStore;
67
+ // Iterate all leaf nodes to sync visibility
68
+ // This is a bit heavy but ensures consistency.
69
+ // Optimization: Only update changed nodes if we had a diff.
70
+
71
+ // We only care about leaf nodes (bands)
72
+ let changes = 0;
73
+ val.state.nodes.forEach((nodeState) => {
74
+ if (nodeState.node.children && nodeState.node.children.length > 0) return; // Skip folders
75
+
76
+ const groupId = nodeState.node.id;
77
+ // IMPORTANT: Read from checkedPaths set, NOT nodeState.checked (which is static/stale)
78
+ const isVisible = val.state.checkedPaths.has(nodeState.path);
71
79
 
72
- // We only care about leaf nodes (bands)
73
- let changes = 0;
74
- val.state.nodes.forEach((nodeState) => {
75
- if (nodeState.node.children && nodeState.node.children.length > 0) return; // Skip folders
76
-
77
- const groupId = nodeState.node.id;
78
- // IMPORTANT: Read from checkedPaths set, NOT nodeState.checked (which is static/stale)
79
- const isVisible = val.state.checkedPaths.has(nodeState.path);
80
-
81
- // Update registry if different
82
- const currentStyle = registry.getStyle(groupId, '#000'); // Color doesn't matter here
83
- if (currentStyle.visible !== isVisible) {
84
- // console.log(`[CellFilterControl] Syncing ${groupId}: ${currentStyle.visible} -> ${isVisible}`);
85
- registry.toggleVisibility(groupId);
86
- changes++;
87
- }
88
- });
89
- if (changes > 0) {
90
- console.log(`[CellFilterControl] Synced ${changes} visibility changes to registry`);
80
+ // Update registry if different
81
+ const currentStyle = registry.getStyle(groupId, '#000'); // Color doesn't matter here
82
+ if (currentStyle.visible !== isVisible) {
83
+ // console.log(`[CellFilterControl] Syncing ${groupId}: ${currentStyle.visible} -> ${isVisible}`);
84
+ registry.toggleVisibility(groupId);
85
+ changes++;
91
86
  }
92
87
  });
93
-
94
- return () => {
95
- unsubscribe();
96
- };
88
+ if (changes > 0) {
89
+ console.log(`[CellFilterControl] Synced ${changes} visibility changes to registry`);
90
+ }
97
91
  });
98
92
 
99
93
  function handleColorChange(groupId: string, event: Event) {
@@ -184,7 +178,7 @@
184
178
  No cells loaded.
185
179
  </div>
186
180
  {:else}
187
- <TreeView showControls={false} store={$treeStore} height="300px">
181
+ <TreeView showControls={false} store={treeStore} height="300px">
188
182
  {#snippet children({ node, state })}
189
183
  <!-- Color Picker (Only for leaves) -->
190
184
  {#if !node.children || node.children.length === 0}
@@ -66,24 +66,44 @@
66
66
  }
67
67
 
68
68
  if (!map.getLayer(lineLayerId)) {
69
- // Line Layer (Border) - Status-based styling
69
+ // Line Layer (Border) - Status-based styling with feature-state support
70
70
  map.addLayer({
71
71
  id: lineLayerId,
72
72
  type: 'line',
73
73
  source: sourceId,
74
74
  paint: {
75
- 'line-color': ['get', 'lineColor'],
75
+ 'line-color': [
76
+ 'case',
77
+ ['boolean', ['feature-state', 'selected'], false],
78
+ '#FF6B00', // Selected: orange
79
+ ['get', 'lineColor'] // Default: status color
80
+ ],
76
81
  'line-width': [
77
- '*',
78
- ['get', 'lineWidth'],
79
- displayStore.lineWidth
82
+ 'case',
83
+ ['boolean', ['feature-state', 'selected'], false],
84
+ 4, // Selected: thick
85
+ [
86
+ '*',
87
+ ['get', 'lineWidth'],
88
+ displayStore.lineWidth
89
+ ] // Default: scaled by display setting
90
+ ],
91
+ 'line-opacity': [
92
+ 'case',
93
+ ['boolean', ['feature-state', 'selected'], false],
94
+ 1, // Selected: full opacity
95
+ ['get', 'lineOpacity'] // Default: status opacity
80
96
  ],
81
- 'line-opacity': ['get', 'lineOpacity'],
82
97
  'line-dasharray': [
83
98
  'case',
84
- ['>', ['length', ['get', 'dashArray']], 0],
85
- ['get', 'dashArray'],
86
- ['literal', []]
99
+ ['boolean', ['feature-state', 'selected'], false],
100
+ ['literal', []], // Selected: solid line
101
+ [
102
+ 'case',
103
+ ['>', ['length', ['get', 'dashArray']], 0],
104
+ ['get', 'dashArray'],
105
+ ['literal', []]
106
+ ] // Default: status dash
87
107
  ]
88
108
  },
89
109
  layout: {
@@ -79,6 +79,9 @@ export function generateCellArc(cell, radiusMeters, zIndex, color, beamwidthOver
79
79
  const sector = turf.sector(center, radiusMeters / 1000, bearing1, bearing2, {
80
80
  steps: 10 // Low steps for performance, increase if jagged
81
81
  });
82
+ // Set numeric ID at feature level for Mapbox feature-state support
83
+ // cellName is a numeric string (e.g., "4080141"), convert to number
84
+ sector.id = Number(cell.cellName);
82
85
  // Get status style
83
86
  const statusStyle = DEFAULT_STATUS_STYLES[cell.status] || DEFAULT_STATUS_STYLES['On_Air'];
84
87
  // Attach properties for styling and interaction
@@ -4,8 +4,35 @@ export declare class CellDataStore {
4
4
  rawCells: Cell[];
5
5
  filterOnAir: boolean;
6
6
  siteDistanceStore: SiteDistanceStore;
7
+ private _siteToCellsMap;
8
+ private _sectorToCellsMap;
9
+ private _cellNameToIdMap;
7
10
  constructor();
8
11
  setCells(cells: Cell[]): void;
12
+ /**
13
+ * Rebuild lookup maps when cell data changes
14
+ */
15
+ private rebuildLookupMaps;
9
16
  get filteredCells(): Cell[];
17
+ /**
18
+ * Get all cell names belonging to a site (by site ID)
19
+ */
20
+ getCellsBySiteId(siteId: string): string[];
21
+ /**
22
+ * Get all cell names belonging to a sector (by sector ID)
23
+ */
24
+ getCellsBySectorId(sectorId: string): string[];
25
+ /**
26
+ * Get numeric ID for a cell name
27
+ */
28
+ getNumericId(cellName: string): number | undefined;
29
+ /**
30
+ * Extract site ID from cell name (first 4 digits)
31
+ */
32
+ getSiteIdFromCellName(cellName: string): string;
33
+ /**
34
+ * Extract sector ID from cell name (first 5 digits)
35
+ */
36
+ getSectorIdFromCellName(cellName: string): string;
10
37
  }
11
38
  export declare function createCellDataStore(): CellDataStore;
@@ -4,6 +4,10 @@ export class CellDataStore {
4
4
  filterOnAir = $state(false);
5
5
  // Internal site distance store for auto-sizing
6
6
  siteDistanceStore;
7
+ // Cached lookup maps (rebuilt when cells change)
8
+ _siteToCellsMap = $state(new Map());
9
+ _sectorToCellsMap = $state(new Map());
10
+ _cellNameToIdMap = $state(new Map());
7
11
  constructor() {
8
12
  this.siteDistanceStore = new SiteDistanceStore();
9
13
  }
@@ -11,12 +15,73 @@ export class CellDataStore {
11
15
  this.rawCells = cells;
12
16
  // Automatically update site distances when cells are loaded
13
17
  this.siteDistanceStore.updateDistances(cells);
18
+ // Rebuild lookup maps
19
+ this.rebuildLookupMaps();
20
+ }
21
+ /**
22
+ * Rebuild lookup maps when cell data changes
23
+ */
24
+ rebuildLookupMaps() {
25
+ console.log('[CellDataStore] Rebuilding lookup maps for', this.rawCells.length, 'cells');
26
+ // Clear existing maps
27
+ this._siteToCellsMap.clear();
28
+ this._sectorToCellsMap.clear();
29
+ this._cellNameToIdMap.clear();
30
+ // Build all maps in one pass
31
+ for (const cell of this.rawCells) {
32
+ const siteId = cell.cellName.substring(0, 4);
33
+ const sectorId = cell.cellName.substring(0, 5);
34
+ const numericId = Number(cell.cellName);
35
+ // Site map
36
+ if (!this._siteToCellsMap.has(siteId)) {
37
+ this._siteToCellsMap.set(siteId, []);
38
+ }
39
+ this._siteToCellsMap.get(siteId).push(cell.cellName);
40
+ // Sector map
41
+ if (!this._sectorToCellsMap.has(sectorId)) {
42
+ this._sectorToCellsMap.set(sectorId, []);
43
+ }
44
+ this._sectorToCellsMap.get(sectorId).push(cell.cellName);
45
+ // Cell name to ID map
46
+ this._cellNameToIdMap.set(cell.cellName, numericId);
47
+ }
48
+ console.log('[CellDataStore] Lookup maps built:', this._siteToCellsMap.size, 'sites,', this._sectorToCellsMap.size, 'sectors,', this._cellNameToIdMap.size, 'cells');
14
49
  }
15
50
  get filteredCells() {
16
51
  if (!this.filterOnAir)
17
52
  return this.rawCells;
18
53
  return this.rawCells.filter(c => c.status === 'On_Air');
19
54
  }
55
+ /**
56
+ * Get all cell names belonging to a site (by site ID)
57
+ */
58
+ getCellsBySiteId(siteId) {
59
+ return this._siteToCellsMap.get(siteId) || [];
60
+ }
61
+ /**
62
+ * Get all cell names belonging to a sector (by sector ID)
63
+ */
64
+ getCellsBySectorId(sectorId) {
65
+ return this._sectorToCellsMap.get(sectorId) || [];
66
+ }
67
+ /**
68
+ * Get numeric ID for a cell name
69
+ */
70
+ getNumericId(cellName) {
71
+ return this._cellNameToIdMap.get(cellName);
72
+ }
73
+ /**
74
+ * Extract site ID from cell name (first 4 digits)
75
+ */
76
+ getSiteIdFromCellName(cellName) {
77
+ return cellName.substring(0, 4);
78
+ }
79
+ /**
80
+ * Extract sector ID from cell name (first 5 digits)
81
+ */
82
+ getSectorIdFromCellName(cellName) {
83
+ return cellName.substring(0, 5);
84
+ }
20
85
  }
21
86
  export function createCellDataStore() {
22
87
  return new CellDataStore();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Coverage Feature - Public Exports
3
+ *
4
+ * Self-contained RF coverage prediction and visualization.
5
+ * Zero dependencies on cells/sites features.
6
+ */
7
+ export * from './types';
8
+ export { CoverageDataStore, createCoverageDataStore } from './stores/coverage.data.svelte';
9
+ export { CoverageDisplayStore, createCoverageDisplayStore } from './stores/coverage.display.svelte';
10
+ export { default as CoverageLayer } from './layers/CoverageLayer.svelte';
11
+ export { gridToGeoJSON, getGridBounds, getSignalRange } from './logic/geometry';
12
+ export { getColorForSignal, getLegendItems } from './logic/coloring';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Coverage Feature - Public Exports
3
+ *
4
+ * Self-contained RF coverage prediction and visualization.
5
+ * Zero dependencies on cells/sites features.
6
+ */
7
+ // Types
8
+ export * from './types';
9
+ // Stores
10
+ export { CoverageDataStore, createCoverageDataStore } from './stores/coverage.data.svelte';
11
+ export { CoverageDisplayStore, createCoverageDisplayStore } from './stores/coverage.display.svelte';
12
+ // Layer
13
+ export { default as CoverageLayer } from './layers/CoverageLayer.svelte';
14
+ // Utilities (for advanced usage)
15
+ export { gridToGeoJSON, getGridBounds, getSignalRange } from './logic/geometry';
16
+ export { getColorForSignal, getLegendItems } from './logic/coloring';
@@ -0,0 +1,198 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Coverage Layer
4
+ *
5
+ * Renders coverage grid as Mapbox fill layer.
6
+ * Self-contained - only needs map instance from context.
7
+ */
8
+
9
+ import { getContext, onMount } from 'svelte';
10
+ import type { MapStore } from '../../../core/stores/map.store.svelte';
11
+ import type { CoverageDataStore } from '../stores/coverage.data.svelte';
12
+ import type { CoverageDisplayStore } from '../stores/coverage.display.svelte';
13
+ import { gridToGeoJSON, getGridBounds } from '../logic/geometry';
14
+ import type mapboxgl from 'mapbox-gl';
15
+
16
+ interface Props {
17
+ dataStore: CoverageDataStore;
18
+ displayStore: CoverageDisplayStore;
19
+ autoFitBounds?: boolean; // Automatically fit map to coverage bounds
20
+ }
21
+
22
+ let { dataStore, displayStore, autoFitBounds = true }: Props = $props();
23
+
24
+ const mapStore = getContext<MapStore>('MAP_CONTEXT');
25
+ const sourceId = 'coverage-source';
26
+ const layerId = 'coverage-layer';
27
+ const componentId = Math.random().toString(36).substring(7);
28
+
29
+ let layerInitialized = $state(false);
30
+
31
+ console.log(`[CoverageLayer-${componentId}] Component created, mapStore:`, mapStore);
32
+
33
+ // Main effect: manage layer lifecycle - runs once (avoid reactive dependencies!)
34
+ $effect(() => {
35
+ const map = mapStore.map;
36
+ if (!map) return;
37
+
38
+ // Initialize layers when style is loaded
39
+ const initializeLayers = () => {
40
+ // Always check if layer already exists (style might have reloaded)
41
+ if (map.getLayer(layerId)) {
42
+ console.log(`[CoverageLayer-${componentId}] Layer already exists, skipping initialization`);
43
+ layerInitialized = true;
44
+ return;
45
+ }
46
+
47
+ console.log(`[CoverageLayer-${componentId}] Initializing layer...`);
48
+
49
+ // Add source if it doesn't exist
50
+ if (!map.getSource(sourceId)) {
51
+ map.addSource(sourceId, {
52
+ type: 'geojson',
53
+ data: { type: 'FeatureCollection', features: [] }
54
+ });
55
+ console.log(`[CoverageLayer-${componentId}] Source added:`, sourceId);
56
+ }
57
+
58
+ // Add layer - use static defaults
59
+ map.addLayer({
60
+ id: layerId,
61
+ type: 'fill',
62
+ source: sourceId,
63
+ paint: {
64
+ 'fill-color': ['get', 'color'],
65
+ 'fill-opacity': 0.7 // Will be updated by separate effect
66
+ },
67
+ layout: {
68
+ 'visibility': 'visible' // Will be updated by separate effect
69
+ }
70
+ });
71
+ console.log(`[CoverageLayer-${componentId}] Layer added:`, layerId);
72
+
73
+ layerInitialized = true;
74
+ };
75
+
76
+ // Listen to style.load event - fires when style finishes loading (and on style changes)
77
+ const handleStyleLoad = () => {
78
+ console.log(`[CoverageLayer-${componentId}] Style loaded, reinitializing layers...`);
79
+ layerInitialized = false; // Reset flag so layers are recreated
80
+ initializeLayers();
81
+ };
82
+
83
+ // Initialize immediately if style is already loaded
84
+ if (map.isStyleLoaded()) {
85
+ initializeLayers();
86
+ } else {
87
+ map.once('style.load', initializeLayers);
88
+ }
89
+
90
+ // Re-initialize layers when style changes/reloads
91
+ map.on('style.load', handleStyleLoad);
92
+
93
+ // Cleanup on unmount only
94
+ return () => {
95
+ console.log(`[CoverageLayer-${componentId}] Component unmounting, cleaning up...`);
96
+ map.off('style.load', handleStyleLoad);
97
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
98
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
99
+ layerInitialized = false;
100
+ };
101
+ });
102
+
103
+ // Update layer visibility and opacity dynamically
104
+ $effect(() => {
105
+ const map = mapStore.map;
106
+ if (!map || !layerInitialized) return;
107
+
108
+ const opacity = displayStore.opacity;
109
+ const visible = displayStore.visible;
110
+
111
+ if (map.getLayer(layerId)) {
112
+ map.setPaintProperty(layerId, 'fill-opacity', opacity);
113
+ map.setLayoutProperty(layerId, 'visibility', visible ? 'visible' : 'none');
114
+ console.log(`[CoverageLayer-${componentId}] Updated opacity:`, opacity, 'visible:', visible);
115
+ }
116
+ });
117
+
118
+ // React to data changes - separate effect
119
+ $effect(() => {
120
+ const map = mapStore.map;
121
+ if (!map || !layerInitialized) {
122
+ return;
123
+ }
124
+
125
+ // Explicitly read dependencies for reactivity
126
+ const hasResults = dataStore.hasResults;
127
+ const colorScheme = displayStore.colorScheme;
128
+ const minSignal = displayStore.minSignalThreshold;
129
+ const maxSignal = displayStore.maxSignalThreshold;
130
+
131
+ console.log(`[CoverageLayer-${componentId}] Data/settings changed, hasResults:`, hasResults);
132
+
133
+ if (hasResults) {
134
+ updateLayer(map);
135
+ }
136
+ });
137
+
138
+ function updateLayer(map: mapboxgl.Map) {
139
+ if (!dataStore.result) {
140
+ console.log(`[CoverageLayer-${componentId}] No coverage data to render`);
141
+ return;
142
+ }
143
+
144
+ const grid = dataStore.result.grid;
145
+ const thresholds = dataStore.result.config.signalThresholds;
146
+
147
+ console.log(`[CoverageLayer-${componentId}] Grid structure:`, {
148
+ rows: grid.cells.length,
149
+ cols: grid.cells[0]?.length,
150
+ cellSizeMeters: grid.cellSizeMeters,
151
+ bounds: grid.bounds,
152
+ firstCell: grid.cells[0]?.[0]
153
+ });
154
+
155
+ // Convert grid to GeoJSON
156
+ const geojson = gridToGeoJSON(
157
+ grid,
158
+ displayStore.colorScheme,
159
+ displayStore.minSignalThreshold,
160
+ displayStore.maxSignalThreshold,
161
+ thresholds
162
+ );
163
+
164
+ console.log(`[CoverageLayer-${componentId}] GeoJSON created:`, {
165
+ features: geojson.features.length,
166
+ firstFeature: geojson.features[0],
167
+ firstFeatureCoords: geojson.features[0]?.geometry.coordinates,
168
+ firstFeatureColor: geojson.features[0]?.properties?.color
169
+ });
170
+
171
+ // Check if layer exists
172
+ console.log(`[CoverageLayer-${componentId}] Layer exists?`, map.getLayer(layerId) !== undefined);
173
+ console.log(`[CoverageLayer-${componentId}] Source exists?`, map.getSource(sourceId) !== undefined);
174
+
175
+ // Update source
176
+ const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
177
+ if (source) {
178
+ source.setData(geojson);
179
+ console.log(`[CoverageLayer-${componentId}] Source data updated successfully`);
180
+
181
+ // Force a repaint
182
+ map.triggerRepaint();
183
+ } else {
184
+ console.error('[CoverageLayer] Source not found:', sourceId);
185
+ }
186
+
187
+ // Auto-fit bounds on first render
188
+ if (autoFitBounds && geojson.features.length > 0) {
189
+ const bounds = getGridBounds(grid);
190
+ console.log(`[CoverageLayer-${componentId}] Fitting bounds:`, bounds);
191
+ map.fitBounds(bounds, {
192
+ padding: 50,
193
+ maxZoom: 14,
194
+ duration: 1000
195
+ });
196
+ }
197
+ }
198
+ </script>