@smartnet360/svelte-components 0.0.100 → 0.0.102

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 (25) hide show
  1. package/dist/apps/site-check/SiteCheck.svelte +54 -272
  2. package/dist/apps/site-check/SiteCheckControls.svelte +294 -0
  3. package/dist/apps/site-check/SiteCheckControls.svelte.d.ts +30 -0
  4. package/dist/map-v2/demo/DemoMap.svelte +39 -7
  5. package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
  6. package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte +20 -25
  7. package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte.d.ts +2 -4
  8. package/dist/map-v3/demo/DemoMap.svelte +31 -5
  9. package/dist/map-v3/demo/demo-cells.js +51 -22
  10. package/dist/map-v3/features/cells/layers/CellsLayer.svelte +29 -9
  11. package/dist/map-v3/features/cells/logic/geometry.js +3 -0
  12. package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +27 -0
  13. package/dist/map-v3/features/cells/stores/cell.data.svelte.js +65 -0
  14. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +82 -65
  15. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +5 -9
  16. package/dist/map-v3/features/selection/index.d.ts +1 -2
  17. package/dist/map-v3/features/selection/index.js +0 -1
  18. package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +44 -15
  19. package/dist/map-v3/features/selection/stores/selection.store.svelte.js +163 -40
  20. package/dist/map-v3/features/selection/types.d.ts +4 -2
  21. package/dist/shared/ResizableSplitPanel.svelte +175 -0
  22. package/dist/shared/ResizableSplitPanel.svelte.d.ts +17 -0
  23. package/package.json +1 -1
  24. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
  25. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
@@ -87,9 +87,19 @@
87
87
  alert(`Selected ${siteIds.length} sites:\n${siteIds.join(', ')}`);
88
88
  }
89
89
 
90
- // Handler for generic feature selection action button
91
- function handleProcessFeatures(featureIds: string[]) {
92
- alert(`Selected ${featureIds.length} features:\n${featureIds.join(', ')}`);
90
+ // Handler for cluster processing
91
+ function handleProcessCluster(featureIds: string[]) {
92
+ alert(`Processing cluster with ${featureIds.length} features:\n${featureIds.join(', ')}`);
93
+ }
94
+
95
+ // Handler for data export
96
+ function handleExportData(featureIds: string[]) {
97
+ alert(`Exporting ${featureIds.length} features:\n${featureIds.join(', ')}`);
98
+ }
99
+
100
+ // Handler for feature analysis
101
+ function handleAnalyzeFeatures(featureIds: string[]) {
102
+ alert(`Analyzing ${featureIds.length} features:\n${featureIds.join(', ')}`);
93
103
  }
94
104
 
95
105
  // Cell filter grouping configuration
@@ -231,12 +241,34 @@
231
241
  <FeatureSelectionControl
232
242
  position="bottom-left"
233
243
  title="Cluster Tool"
234
- icon="speedometer2"
244
+ icon="bi bi-graph-up"
235
245
  iconOnlyWhenCollapsed={useIconHeaders}
236
- onAction={handleProcessFeatures}
237
- actionButtonLabel="Process Cluster"
238
246
  featureIcon="pin-map-fill"
239
- />
247
+ >
248
+ {#snippet children(selectedIds)}
249
+ <button
250
+ class="btn btn-primary"
251
+ disabled={selectedIds.length === 0}
252
+ onclick={() => handleProcessCluster(selectedIds)}
253
+ >
254
+ <i class="bi bi-lightning-charge-fill"></i> Process Cluster
255
+ </button>
256
+ <button
257
+ class="btn btn-success"
258
+ disabled={selectedIds.length === 0}
259
+ onclick={() => handleExportData(selectedIds)}
260
+ >
261
+ <i class="bi bi-download"></i> Export Data
262
+ </button>
263
+ <button
264
+ class="btn btn-info"
265
+ disabled={selectedIds.length === 0}
266
+ onclick={() => handleAnalyzeFeatures(selectedIds)}
267
+ >
268
+ <i class="bi bi-graph-up"></i> Analyze
269
+ </button>
270
+ {/snippet}
271
+ </FeatureSelectionControl>
240
272
 
241
273
  <!-- Unified feature settings control - Sites, Cells, and Repeaters -->
242
274
  <FeatureSettingsControl
@@ -42,6 +42,7 @@ export function cellsToGeoJSON(cells, currentZoom, baseRadius = 500, groupColorM
42
42
  // Build feature with styling properties
43
43
  return {
44
44
  type: 'Feature',
45
+ id: Number(cell.cellName), // Numeric ID for feature-state (cellName is numeric string)
45
46
  geometry: arc.geometry,
46
47
  properties: {
47
48
  // Cell identification
@@ -28,16 +28,14 @@
28
28
  icon?: string;
29
29
  /** Show icon when collapsed (default: true) */
30
30
  iconOnlyWhenCollapsed?: boolean;
31
- /** Callback when action button clicked */
32
- onAction?: (featureIds: string[]) => void;
33
- /** Action button label */
34
- actionButtonLabel?: string;
35
31
  /** Feature icon (default: geo-alt-fill) */
36
32
  featureIcon?: string;
37
33
  /** Available property names to use as ID (default: ['id', 'siteId', 'cellName']) */
38
34
  idPropertyOptions?: string[];
39
35
  /** Default property to use as ID (default: 'id') */
40
36
  defaultIdProperty?: string;
37
+ /** Slot for custom action buttons that receive selected feature IDs */
38
+ children?: import('svelte').Snippet<[string[]]>;
41
39
  }
42
40
 
43
41
  let {
@@ -45,11 +43,10 @@
45
43
  title = 'Cluster Tool',
46
44
  icon = 'speedometer2',
47
45
  iconOnlyWhenCollapsed = true,
48
- onAction,
49
- actionButtonLabel = 'Process Cluster',
50
46
  featureIcon = 'geo-alt-fill',
51
47
  idPropertyOptions = ['none','siteId','sectorId', 'cellName','id'],
52
- defaultIdProperty = 'none'
48
+ defaultIdProperty = 'none',
49
+ children
53
50
  }: Props = $props();
54
51
 
55
52
  // Get map from context
@@ -228,13 +225,6 @@
228
225
  console.error('Failed to copy:', err);
229
226
  }
230
227
  }
231
-
232
- function handleAction() {
233
- if (onAction && hasSelection) {
234
- const ids = store.getSelectedIds();
235
- onAction(ids);
236
- }
237
- }
238
228
  </script>
239
229
 
240
230
  <MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} onCollapseToggle={handleCollapseToggle}>
@@ -316,17 +306,10 @@
316
306
  </div>
317
307
  {/if}
318
308
 
319
- <!-- Action Button -->
320
- {#if onAction}
321
- <div class="mt-3">
322
- <button
323
- type="button"
324
- class="btn btn-primary w-100"
325
- disabled={!hasSelection}
326
- onclick={handleAction}
327
- >
328
- <i class="bi bi-lightning-charge-fill"></i> {actionButtonLabel}
329
- </button>
309
+ <!-- Custom Action Buttons Slot -->
310
+ {#if children}
311
+ <div class="mt-3 action-slot">
312
+ {@render children(store.getSelectedIds())}
330
313
  </div>
331
314
  {/if}
332
315
  </div>
@@ -435,6 +418,18 @@
435
418
  background-color: #ffcccc;
436
419
  }
437
420
 
421
+ /* Action slot styles */
422
+ .action-slot {
423
+ display: flex;
424
+ flex-direction: column;
425
+ gap: 0.5rem;
426
+ }
427
+
428
+ /* Ensure action slot buttons maintain Bootstrap styling */
429
+ .action-slot :global(.btn) {
430
+ width: 100%;
431
+ }
432
+
438
433
  /* Ensure primary action button keeps Bootstrap styling inside Mapbox control */
439
434
  .feature-selection-control .btn-primary {
440
435
  background-color: var(--bs-btn-bg, var(--bs-primary));
@@ -7,16 +7,14 @@ interface Props {
7
7
  icon?: string;
8
8
  /** Show icon when collapsed (default: true) */
9
9
  iconOnlyWhenCollapsed?: boolean;
10
- /** Callback when action button clicked */
11
- onAction?: (featureIds: string[]) => void;
12
- /** Action button label */
13
- actionButtonLabel?: string;
14
10
  /** Feature icon (default: geo-alt-fill) */
15
11
  featureIcon?: string;
16
12
  /** Available property names to use as ID (default: ['id', 'siteId', 'cellName']) */
17
13
  idPropertyOptions?: string[];
18
14
  /** Default property to use as ID (default: 'id') */
19
15
  defaultIdProperty?: string;
16
+ /** Slot for custom action buttons that receive selected feature IDs */
17
+ children?: import('svelte').Snippet<[string[]]>;
20
18
  }
21
19
  declare const FeatureSelectionControl: import("svelte").Component<Props, {}, "">;
22
20
  type FeatureSelectionControl = ReturnType<typeof FeatureSelectionControl>;
@@ -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)`);
@@ -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();