@smartnet360/svelte-components 0.0.101 → 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.
@@ -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
@@ -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();
@@ -3,58 +3,58 @@
3
3
  import { MapControl } from '../../../shared';
4
4
  import type { MapStore } from '../../../core/stores/map.store.svelte';
5
5
  import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
6
- import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
7
- import { createFeatureSelectionStore } from '../stores/selection.store.svelte';
8
- import SelectionHighlightLayers from '../layers/SelectionHighlightLayers.svelte';
6
+ import { createFeatureSelectionStore, type SelectionMode } from '../stores/selection.store.svelte';
9
7
  import type { SelectedFeature } from '../types';
8
+ import type { Snippet } from 'svelte';
10
9
 
11
10
  interface Props {
12
- cellDataStore?: CellDataStore;
13
- cellDisplayStore?: CellDisplayStore;
11
+ cellDataStore: CellDataStore;
14
12
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
15
13
  title?: string;
16
14
  icon?: string;
17
15
  iconOnlyWhenCollapsed?: boolean;
18
- onAction?: (featureIds: string[]) => void;
19
- actionButtonLabel?: string;
20
16
  featureIcon?: string;
21
- idPropertyOptions?: string[];
22
- defaultIdProperty?: string;
23
- highlightColor?: string;
24
- highlightWidth?: number;
17
+ defaultSelectionMode?: SelectionMode;
18
+ children?: Snippet<[string[]]>;
25
19
  }
26
20
 
27
21
  let {
28
22
  cellDataStore,
29
- cellDisplayStore,
30
23
  position = 'top-left',
31
24
  title = 'Feature Selection',
32
25
  icon = 'cursor-fill',
33
26
  iconOnlyWhenCollapsed = true,
34
- onAction,
35
- actionButtonLabel = 'Process Selection',
36
27
  featureIcon = 'geo-alt-fill',
37
- idPropertyOptions = ['siteId', 'sectorId', 'cellName'],
38
- defaultIdProperty = 'siteId',
39
- highlightColor = '#FF6B00',
40
- highlightWidth = 4
28
+ defaultSelectionMode = 'site',
29
+ children
41
30
  }: Props = $props();
42
31
 
43
32
  const mapStore = getContext<MapStore>('MAP_CONTEXT');
44
33
  const store = createFeatureSelectionStore();
45
34
 
46
- store.setIdProperty(defaultIdProperty);
47
-
35
+ // Initialize selection mode
36
+ store.setIdProperty(defaultSelectionMode);
37
+
48
38
  let selectedFeatures = $derived(store.getSelectedFeatures());
49
39
  let selectionCount = $derived(store.count);
40
+ let cellCount = $derived(store.cellCount);
41
+ let selectedIds = $derived(store.getSelectedIds());
50
42
  let hasSelection = $derived(selectionCount > 0);
51
43
  let isCollapsed = $state(false);
44
+ let currentMode = $state<SelectionMode>(defaultSelectionMode);
45
+ let isInitialized = $state(false);
52
46
 
47
+ // Initialize store with map and cell data (run once)
53
48
  $effect(() => {
49
+ if (isInitialized) return;
50
+
54
51
  const map = mapStore.map;
55
- if (map && !store['map']) {
52
+ if (map) {
53
+ console.log('[FeatureSelectionControl] Initializing selection store');
56
54
  store.setMap(map);
55
+ store.setCellDataStore(cellDataStore);
57
56
  store.enableSelectionMode();
57
+ isInitialized = true;
58
58
  }
59
59
  });
60
60
 
@@ -71,9 +71,11 @@
71
71
  }
72
72
  }
73
73
 
74
- function handleIdPropertyChange(event: Event) {
74
+ function handleModeChange(event: Event) {
75
75
  const target = event.target as HTMLSelectElement;
76
- store.setIdProperty(target.value);
76
+ const mode = target.value as SelectionMode;
77
+ currentMode = mode;
78
+ store.setIdProperty(mode);
77
79
  }
78
80
 
79
81
  function handleRemoveFeature(featureId: string) {
@@ -85,7 +87,7 @@
85
87
  }
86
88
 
87
89
  async function handleCopy() {
88
- const ids = store.getSelectedIds().join(',');
90
+ const ids = selectedIds.join(',');
89
91
  try {
90
92
  await navigator.clipboard.writeText(ids);
91
93
  console.log('[FeatureSelection] Copied to clipboard:', ids);
@@ -94,14 +96,22 @@
94
96
  }
95
97
  }
96
98
 
97
- function handleAction() {
98
- if (onAction && hasSelection) {
99
- const ids = store.getSelectedIds();
100
- onAction(ids);
99
+ // Get display label for selection mode
100
+ function getModeLabel(mode: SelectionMode): string {
101
+ switch (mode) {
102
+ case 'site': return 'Site (all sectors)';
103
+ case 'sector': return 'Sector (all bands)';
104
+ case 'cell': return 'Individual Cell';
101
105
  }
102
106
  }
103
- </script>
104
107
 
108
+ // Get cell count description for a feature
109
+ function getCellCountText(feature: SelectedFeature): string {
110
+ const count = feature.cellNames?.length || 0;
111
+ if (count <= 1) return '';
112
+ return ` (${count} cells)`;
113
+ }
114
+ </script>
105
115
  <MapControl
106
116
  {position}
107
117
  {title}
@@ -109,28 +119,45 @@
109
119
  {iconOnlyWhenCollapsed}
110
120
  collapsible={true}
111
121
  onCollapseToggle={handleCollapseToggle}
112
- controlWidth="300px"
122
+ controlWidth="320px"
113
123
  >
114
124
  <div class="feature-selection-control">
115
- <!-- ID Property Selector -->
125
+ <!-- Selection Mode Selector -->
116
126
  <div class="mb-3">
117
- <label for="id-property-select" class="form-label small fw-semibold">Select By</label>
127
+ <label for="mode-select" class="form-label small fw-semibold">Selection Mode</label>
118
128
  <select
119
- id="id-property-select"
129
+ id="mode-select"
120
130
  class="form-select form-select-sm"
121
- value={store.idProperty}
122
- onchange={handleIdPropertyChange}
131
+ value={currentMode}
132
+ onchange={handleModeChange}
123
133
  >
124
- {#each idPropertyOptions as option}
125
- <option value={option}>{option}</option>
126
- {/each}
134
+ <option value="site">{getModeLabel('site')}</option>
135
+ <option value="sector">{getModeLabel('sector')}</option>
136
+ <option value="cell">{getModeLabel('cell')}</option>
127
137
  </select>
138
+ <div class="form-text small mt-1">
139
+ {#if currentMode === 'site'}
140
+ Click any cell to select all cells at that site
141
+ {:else if currentMode === 'sector'}
142
+ Click any cell to select all cells in that sector
143
+ {:else}
144
+ Click to select individual cells
145
+ {/if}
146
+ </div>
128
147
  </div>
129
148
 
130
149
  <!-- Selection Stats -->
131
- <div class="selection-stats mb-2 text-secondary">
132
- <strong>{selectionCount}</strong>
133
- {selectionCount === 1 ? 'item' : 'items'} selected
150
+ <div class="selection-stats mb-2">
151
+ <div class="d-flex justify-content-between align-items-center">
152
+ <span class="text-secondary">
153
+ <strong>{selectionCount}</strong> {selectionCount === 1 ? 'group' : 'groups'}
154
+ </span>
155
+ {#if cellCount > selectionCount}
156
+ <span class="badge bg-secondary">
157
+ {cellCount} {cellCount === 1 ? 'cell' : 'cells'} total
158
+ </span>
159
+ {/if}
160
+ </div>
134
161
  </div>
135
162
 
136
163
  <!-- Action Buttons -->
@@ -162,11 +189,17 @@
162
189
  <div class="feature-list">
163
190
  {#each selectedFeatures as feature (feature.id)}
164
191
  <div class="feature-item">
165
- <i class="bi bi-{featureIcon} feature-icon"></i>
192
+ <i class="bi bi-grid-3x3-gap-fill feature-icon"></i>
166
193
  <div class="feature-info">
167
- <span class="feature-id">{feature.id}</span>
168
- {#if feature.layerId}
169
- <small class="feature-layer text-muted">{feature.layerId}</small>
194
+ <span class="feature-id">
195
+ {feature.id}{getCellCountText(feature)}
196
+ </span>
197
+ {#if currentMode === 'site'}
198
+ <small class="feature-layer text-muted">Site</small>
199
+ {:else if currentMode === 'sector'}
200
+ <small class="feature-layer text-muted">Sector</small>
201
+ {:else}
202
+ <small class="feature-layer text-muted">Cell</small>
170
203
  {/if}
171
204
  </div>
172
205
  <button
@@ -185,35 +218,19 @@
185
218
  <div class="text-muted small text-center py-3">
186
219
  <i class="bi bi-inbox" style="font-size: 2rem; opacity: 0.3;"></i>
187
220
  <div class="mt-2">No items selected</div>
188
- <div class="mt-1">Click on features to select</div>
221
+ <div class="mt-1">Click on map to select</div>
189
222
  </div>
190
223
  {/if}
191
224
 
192
- <!-- Action Button -->
193
- {#if onAction}
225
+ <!-- Custom Action Buttons (snippet) -->
226
+ {#if children}
194
227
  <div class="mt-3">
195
- <button
196
- type="button"
197
- class="btn btn-primary w-100"
198
- disabled={!hasSelection}
199
- onclick={handleAction}
200
- >
201
- <i class="bi bi-lightning-charge-fill"></i> {actionButtonLabel}
202
- </button>
228
+ {@render children(selectedIds)}
203
229
  </div>
204
230
  {/if}
205
231
  </div>
206
232
  </MapControl>
207
233
 
208
- <!-- Highlight Layers -->
209
- <SelectionHighlightLayers
210
- {selectedFeatures}
211
- {cellDataStore}
212
- {cellDisplayStore}
213
- {highlightColor}
214
- {highlightWidth}
215
- />
216
-
217
234
  <style>
218
235
  .feature-selection-control {
219
236
  width: 100%;
@@ -1,19 +1,15 @@
1
1
  import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
2
- import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
2
+ import { type SelectionMode } from '../stores/selection.store.svelte';
3
+ import type { Snippet } from 'svelte';
3
4
  interface Props {
4
- cellDataStore?: CellDataStore;
5
- cellDisplayStore?: CellDisplayStore;
5
+ cellDataStore: CellDataStore;
6
6
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
7
7
  title?: string;
8
8
  icon?: string;
9
9
  iconOnlyWhenCollapsed?: boolean;
10
- onAction?: (featureIds: string[]) => void;
11
- actionButtonLabel?: string;
12
10
  featureIcon?: string;
13
- idPropertyOptions?: string[];
14
- defaultIdProperty?: string;
15
- highlightColor?: string;
16
- highlightWidth?: number;
11
+ defaultSelectionMode?: SelectionMode;
12
+ children?: Snippet<[string[]]>;
17
13
  }
18
14
  declare const FeatureSelectionControl: import("svelte").Component<Props, {}, "">;
19
15
  type FeatureSelectionControl = ReturnType<typeof FeatureSelectionControl>;
@@ -4,6 +4,5 @@
4
4
  * Exports all selection-related components, stores, and types.
5
5
  */
6
6
  export type { SelectedFeature, SelectionStoreState } from './types';
7
- export { createFeatureSelectionStore, FeatureSelectionStore } from './stores/selection.store.svelte';
7
+ export { createFeatureSelectionStore, FeatureSelectionStore, type SelectionMode } from './stores/selection.store.svelte';
8
8
  export { default as FeatureSelectionControl } from './components/FeatureSelectionControl.svelte';
9
- export { default as SelectionHighlightLayers } from './layers/SelectionHighlightLayers.svelte';
@@ -7,4 +7,3 @@
7
7
  export { createFeatureSelectionStore, FeatureSelectionStore } from './stores/selection.store.svelte';
8
8
  // Components
9
9
  export { default as FeatureSelectionControl } from './components/FeatureSelectionControl.svelte';
10
- export { default as SelectionHighlightLayers } from './layers/SelectionHighlightLayers.svelte';
@@ -2,14 +2,19 @@
2
2
  * Feature Selection Store - Svelte 5 Runes Implementation
3
3
  *
4
4
  * Manages selection of map features (cells, sites) with click detection.
5
+ * Supports multi-cell selection based on site/sector hierarchy.
5
6
  */
6
7
  import type { Map as MapboxMap } from 'mapbox-gl';
7
8
  import type { SelectedFeature } from '../types';
9
+ import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
10
+ export type SelectionMode = 'cell' | 'sector' | 'site';
8
11
  export declare class FeatureSelectionStore {
9
12
  private selectedFeatures;
13
+ private selectedCellNames;
10
14
  private map;
15
+ private cellDataStore;
11
16
  selectionMode: boolean;
12
- idProperty: string;
17
+ idProperty: SelectionMode;
13
18
  queryLayers: string[];
14
19
  private clickHandler;
15
20
  private onSelectionChange?;
@@ -18,9 +23,13 @@ export declare class FeatureSelectionStore {
18
23
  */
19
24
  setMap(mapInstance: MapboxMap): void;
20
25
  /**
21
- * Set which property to use as the ID
26
+ * Set the cell data store for lookup maps
22
27
  */
23
- setIdProperty(property: string): void;
28
+ setCellDataStore(dataStore: CellDataStore): void;
29
+ /**
30
+ * Set which property to use as the ID (selection mode)
31
+ */
32
+ setIdProperty(property: SelectionMode): void;
24
33
  /**
25
34
  * Set which layers to query for features
26
35
  */
@@ -34,45 +43,65 @@ export declare class FeatureSelectionStore {
34
43
  */
35
44
  private setupClickHandler;
36
45
  /**
37
- * Enable selection mode
46
+ * Handle click on a cell - expand to site/sector based on mode
38
47
  */
39
- enableSelectionMode(): void;
48
+ private handleCellClick;
40
49
  /**
41
- * Disable selection mode
50
+ * Handle click on a site marker (if sites layer exists)
42
51
  */
43
- disableSelectionMode(): void;
52
+ private handleSiteClick;
44
53
  /**
45
- * Toggle a feature in the selection
54
+ * Toggle a group of cells (site or sector)
46
55
  */
47
- toggleFeatureSelection(id: string, layerId?: string, properties?: Record<string, any>, siteId?: string, cellName?: string): void;
56
+ private toggleGroupSelection;
48
57
  /**
49
- * Add a feature to the selection
58
+ * Update Mapbox feature-state for cells
50
59
  */
51
- addFeatureSelection(id: string, layerId?: string, properties?: Record<string, any>, siteId?: string, cellName?: string): void;
60
+ private updateFeatureStates;
52
61
  /**
53
- * Remove a feature from the selection
62
+ * Enable selection mode
54
63
  */
55
- removeFeatureSelection(id: string): void;
64
+ enableSelectionMode(): void;
65
+ /**
66
+ * Disable selection mode
67
+ */
68
+ disableSelectionMode(): void;
56
69
  /**
57
70
  * Clear all selections
58
71
  */
59
72
  clearSelection(): void;
73
+ /**
74
+ * Remove a selection group by ID
75
+ */
76
+ removeFeatureSelection(id: string): void;
60
77
  /**
61
78
  * Get all selected features
62
79
  */
63
80
  getSelectedFeatures(): SelectedFeature[];
64
81
  /**
65
- * Get selected feature IDs only
82
+ * Get selected group IDs (site/sector/cell IDs)
66
83
  */
67
84
  getSelectedIds(): string[];
85
+ /**
86
+ * Get all selected cell names (flattened from all groups)
87
+ */
88
+ getSelectedCellNames(): string[];
68
89
  /**
69
90
  * Check if a feature is selected
70
91
  */
71
92
  isFeatureSelected(id: string): boolean;
72
93
  /**
73
- * Get selection count
94
+ * Check if a specific cell is selected
95
+ */
96
+ isCellSelected(cellName: string): boolean;
97
+ /**
98
+ * Get selection count (number of groups, not individual cells)
74
99
  */
75
100
  get count(): number;
101
+ /**
102
+ * Get total number of selected cells
103
+ */
104
+ get cellCount(): number;
76
105
  /**
77
106
  * Cleanup - remove event handlers
78
107
  */
@@ -2,12 +2,15 @@
2
2
  * Feature Selection Store - Svelte 5 Runes Implementation
3
3
  *
4
4
  * Manages selection of map features (cells, sites) with click detection.
5
+ * Supports multi-cell selection based on site/sector hierarchy.
5
6
  */
6
7
  export class FeatureSelectionStore {
7
8
  selectedFeatures = $state([]);
9
+ selectedCellNames = $state(new Set()); // Track all selected cell names
8
10
  map = $state(null);
11
+ cellDataStore = null;
9
12
  selectionMode = $state(false);
10
- idProperty = $state('siteId');
13
+ idProperty = $state('site'); // 'cell', 'sector', or 'site'
11
14
  queryLayers = $state([
12
15
  'cells-layer',
13
16
  'sites-layer'
@@ -22,7 +25,13 @@ export class FeatureSelectionStore {
22
25
  this.setupClickHandler();
23
26
  }
24
27
  /**
25
- * Set which property to use as the ID
28
+ * Set the cell data store for lookup maps
29
+ */
30
+ setCellDataStore(dataStore) {
31
+ this.cellDataStore = dataStore;
32
+ }
33
+ /**
34
+ * Set which property to use as the ID (selection mode)
26
35
  */
27
36
  setIdProperty(property) {
28
37
  this.idProperty = property;
@@ -55,13 +64,16 @@ export class FeatureSelectionStore {
55
64
  if (features && features.length > 0) {
56
65
  // Get the topmost feature with an id
57
66
  for (const feature of features) {
58
- // Use the configured property as the ID
59
- const featureId = feature.properties?.[this.idProperty] || feature.id;
60
- const siteId = feature.properties?.siteId;
61
67
  const cellName = feature.properties?.cellName;
62
- if (featureId) {
63
- this.toggleFeatureSelection(String(featureId), feature.layer?.id, feature.properties || undefined, siteId, cellName);
64
- break; // Only select the topmost feature
68
+ const siteId = feature.properties?.siteId;
69
+ if (cellName && this.cellDataStore) {
70
+ this.handleCellClick(cellName, feature.layer?.id, feature.properties || undefined);
71
+ break;
72
+ }
73
+ else if (siteId) {
74
+ // Fallback for site clicks (if sites layer exists)
75
+ this.handleSiteClick(siteId, feature.layer?.id, feature.properties || undefined);
76
+ break;
65
77
  }
66
78
  }
67
79
  }
@@ -69,64 +81,156 @@ export class FeatureSelectionStore {
69
81
  this.map.on('click', this.clickHandler);
70
82
  }
71
83
  /**
72
- * Enable selection mode
84
+ * Handle click on a cell - expand to site/sector based on mode
73
85
  */
74
- enableSelectionMode() {
75
- this.selectionMode = true;
86
+ handleCellClick(cellName, layerId, properties) {
87
+ if (!this.cellDataStore)
88
+ return;
89
+ console.log('[Selection] Clicked cell:', cellName, 'Mode:', this.idProperty);
90
+ let cellNamesToSelect = [];
91
+ let selectionId;
92
+ switch (this.idProperty) {
93
+ case 'site': {
94
+ // Select all cells in the site (first 4 digits)
95
+ const siteId = this.cellDataStore.getSiteIdFromCellName(cellName);
96
+ cellNamesToSelect = this.cellDataStore.getCellsBySiteId(siteId);
97
+ selectionId = siteId;
98
+ console.log('[Selection] Site mode - selecting', cellNamesToSelect.length, 'cells for site', siteId);
99
+ break;
100
+ }
101
+ case 'sector': {
102
+ // Select all cells in the sector (first 5 digits)
103
+ const sectorId = this.cellDataStore.getSectorIdFromCellName(cellName);
104
+ cellNamesToSelect = this.cellDataStore.getCellsBySectorId(sectorId);
105
+ selectionId = sectorId;
106
+ console.log('[Selection] Sector mode - selecting', cellNamesToSelect.length, 'cells for sector', sectorId);
107
+ break;
108
+ }
109
+ case 'cell':
110
+ default: {
111
+ // Select just this cell
112
+ cellNamesToSelect = [cellName];
113
+ selectionId = cellName;
114
+ console.log('[Selection] Cell mode - selecting 1 cell');
115
+ break;
116
+ }
117
+ }
118
+ // Toggle the selection group
119
+ this.toggleGroupSelection(selectionId, cellNamesToSelect, layerId, properties);
76
120
  }
77
121
  /**
78
- * Disable selection mode
122
+ * Handle click on a site marker (if sites layer exists)
79
123
  */
80
- disableSelectionMode() {
81
- this.selectionMode = false;
124
+ handleSiteClick(siteId, layerId, properties) {
125
+ if (!this.cellDataStore)
126
+ return;
127
+ // Always select all cells in the site when clicking site marker
128
+ const cellNamesToSelect = this.cellDataStore.getCellsBySiteId(siteId);
129
+ this.toggleGroupSelection(siteId, cellNamesToSelect, layerId, properties);
82
130
  }
83
131
  /**
84
- * Toggle a feature in the selection
132
+ * Toggle a group of cells (site or sector)
85
133
  */
86
- toggleFeatureSelection(id, layerId, properties, siteId, cellName) {
87
- const index = this.selectedFeatures.findIndex(f => f.id === id);
88
- if (index >= 0) {
89
- // Remove if already selected
90
- this.selectedFeatures.splice(index, 1);
134
+ toggleGroupSelection(groupId, cellNames, layerId, properties) {
135
+ // Check if this group is already selected
136
+ const isSelected = this.selectedFeatures.some(f => f.id === groupId);
137
+ if (isSelected) {
138
+ // Remove the group
139
+ this.selectedFeatures = this.selectedFeatures.filter(f => f.id !== groupId);
140
+ // Remove all cell names from the set
141
+ cellNames.forEach(name => this.selectedCellNames.delete(name));
91
142
  }
92
143
  else {
93
- // Add if not selected
94
- this.selectedFeatures.push({ id, layerId, properties, siteId, cellName });
144
+ // Add the group
145
+ this.selectedFeatures.push({
146
+ id: groupId,
147
+ layerId,
148
+ properties,
149
+ cellNames // Store which cells belong to this group
150
+ });
151
+ // Add all cell names to the set
152
+ cellNames.forEach(name => this.selectedCellNames.add(name));
95
153
  }
154
+ // Update feature-state for all affected cells
155
+ this.updateFeatureStates(cellNames, !isSelected);
96
156
  // Trigger callback
97
157
  if (this.onSelectionChange) {
98
158
  this.onSelectionChange(this.selectedFeatures);
99
159
  }
100
160
  }
101
161
  /**
102
- * Add a feature to the selection
162
+ * Update Mapbox feature-state for cells
103
163
  */
104
- addFeatureSelection(id, layerId, properties, siteId, cellName) {
105
- const exists = this.selectedFeatures.some(f => f.id === id);
106
- if (!exists) {
107
- this.selectedFeatures.push({ id, layerId, properties, siteId, cellName });
108
- if (this.onSelectionChange) {
109
- this.onSelectionChange(this.selectedFeatures);
164
+ updateFeatureStates(cellNames, selected) {
165
+ if (!this.map || !this.cellDataStore)
166
+ return;
167
+ console.log('[Selection] Updating feature-state for', cellNames.length, 'cells, selected:', selected);
168
+ const startTime = performance.now();
169
+ let successCount = 0;
170
+ let failCount = 0;
171
+ for (const cellName of cellNames) {
172
+ const numericId = this.cellDataStore.getNumericId(cellName);
173
+ if (numericId !== undefined) {
174
+ try {
175
+ this.map.setFeatureState({ source: 'cells-source', id: numericId }, { selected });
176
+ successCount++;
177
+ }
178
+ catch (error) {
179
+ console.error('[Selection] Failed to set feature-state for', cellName, numericId, error);
180
+ failCount++;
181
+ }
182
+ }
183
+ else {
184
+ console.warn('[Selection] No numeric ID found for cell:', cellName);
185
+ failCount++;
110
186
  }
111
187
  }
188
+ const endTime = performance.now();
189
+ console.log('[Selection] Feature-state update complete:', successCount, 'success,', failCount, 'failed in', (endTime - startTime).toFixed(2), 'ms');
112
190
  }
113
191
  /**
114
- * Remove a feature from the selection
192
+ * Enable selection mode
115
193
  */
116
- removeFeatureSelection(id) {
117
- const index = this.selectedFeatures.findIndex(f => f.id === id);
118
- if (index >= 0) {
119
- this.selectedFeatures.splice(index, 1);
120
- if (this.onSelectionChange) {
121
- this.onSelectionChange(this.selectedFeatures);
122
- }
123
- }
194
+ enableSelectionMode() {
195
+ this.selectionMode = true;
196
+ }
197
+ /**
198
+ * Disable selection mode
199
+ */
200
+ disableSelectionMode() {
201
+ this.selectionMode = false;
124
202
  }
125
203
  /**
126
204
  * Clear all selections
127
205
  */
128
206
  clearSelection() {
207
+ // Clear feature-state for all currently selected cells
208
+ if (this.map && this.cellDataStore) {
209
+ for (const cellName of this.selectedCellNames) {
210
+ const numericId = this.cellDataStore.getNumericId(cellName);
211
+ if (numericId !== undefined) {
212
+ this.map.setFeatureState({ source: 'cells-source', id: numericId }, { selected: false });
213
+ }
214
+ }
215
+ }
129
216
  this.selectedFeatures = [];
217
+ this.selectedCellNames.clear();
218
+ if (this.onSelectionChange) {
219
+ this.onSelectionChange(this.selectedFeatures);
220
+ }
221
+ }
222
+ /**
223
+ * Remove a selection group by ID
224
+ */
225
+ removeFeatureSelection(id) {
226
+ const feature = this.selectedFeatures.find(f => f.id === id);
227
+ if (feature && feature.cellNames) {
228
+ // Clear feature-state for cells in this group
229
+ this.updateFeatureStates(feature.cellNames, false);
230
+ // Remove cell names from set
231
+ feature.cellNames.forEach(name => this.selectedCellNames.delete(name));
232
+ }
233
+ this.selectedFeatures = this.selectedFeatures.filter(f => f.id !== id);
130
234
  if (this.onSelectionChange) {
131
235
  this.onSelectionChange(this.selectedFeatures);
132
236
  }
@@ -138,11 +242,17 @@ export class FeatureSelectionStore {
138
242
  return this.selectedFeatures;
139
243
  }
140
244
  /**
141
- * Get selected feature IDs only
245
+ * Get selected group IDs (site/sector/cell IDs)
142
246
  */
143
247
  getSelectedIds() {
144
248
  return this.selectedFeatures.map(f => f.id);
145
249
  }
250
+ /**
251
+ * Get all selected cell names (flattened from all groups)
252
+ */
253
+ getSelectedCellNames() {
254
+ return Array.from(this.selectedCellNames);
255
+ }
146
256
  /**
147
257
  * Check if a feature is selected
148
258
  */
@@ -150,11 +260,23 @@ export class FeatureSelectionStore {
150
260
  return this.selectedFeatures.some(f => f.id === id);
151
261
  }
152
262
  /**
153
- * Get selection count
263
+ * Check if a specific cell is selected
264
+ */
265
+ isCellSelected(cellName) {
266
+ return this.selectedCellNames.has(cellName);
267
+ }
268
+ /**
269
+ * Get selection count (number of groups, not individual cells)
154
270
  */
155
271
  get count() {
156
272
  return this.selectedFeatures.length;
157
273
  }
274
+ /**
275
+ * Get total number of selected cells
276
+ */
277
+ get cellCount() {
278
+ return this.selectedCellNames.size;
279
+ }
158
280
  /**
159
281
  * Cleanup - remove event handlers
160
282
  */
@@ -164,6 +286,7 @@ export class FeatureSelectionStore {
164
286
  }
165
287
  this.clearSelection();
166
288
  this.map = null;
289
+ this.cellDataStore = null;
167
290
  }
168
291
  }
169
292
  /**
@@ -2,12 +2,14 @@
2
2
  * Feature Selection - Type Definitions
3
3
  */
4
4
  export interface SelectedFeature {
5
- /** Feature identifier */
5
+ /** Feature identifier (can be site ID, sector ID, or cell name) */
6
6
  id: string;
7
7
  /** Optional site ID */
8
8
  siteId?: string;
9
- /** Optional cell name */
9
+ /** Optional cell name (for single cell selections) */
10
10
  cellName?: string;
11
+ /** Array of cell names (for site/sector selections) */
12
+ cellNames?: string[];
11
13
  /** Layer ID where feature was selected from */
12
14
  layerId?: string;
13
15
  /** Feature properties */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.101",
3
+ "version": "0.0.102",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1,209 +0,0 @@
1
- <script lang="ts">
2
- import { getContext } from 'svelte';
3
- import type { MapStore } from '../../../core/stores/map.store.svelte';
4
- import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
5
- import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
6
- import type { SelectedFeature } from '../types';
7
- import { generateCellArc, calculateRadiusInMeters } from '../../cells/logic/geometry';
8
- import type mapboxgl from 'mapbox-gl';
9
-
10
- interface Props {
11
- selectedFeatures: SelectedFeature[];
12
- cellDataStore?: CellDataStore;
13
- cellDisplayStore?: CellDisplayStore;
14
- highlightColor?: string;
15
- highlightWidth?: number;
16
- }
17
-
18
- let {
19
- selectedFeatures,
20
- cellDataStore,
21
- cellDisplayStore,
22
- highlightColor = '#FF6B00',
23
- highlightWidth = 4
24
- }: Props = $props();
25
-
26
- const mapStore = getContext<MapStore>('MAP_CONTEXT');
27
-
28
- let cellsSourceId = 'cells-selection-source';
29
- let cellsLayerId = 'cells-selection-highlight';
30
- let sitesSourceId = 'sites-selection-source';
31
- let sitesLayerId = 'sites-selection-highlight';
32
-
33
- // Track current zoom level for radius calculation
34
- let currentZoom = $state(13);
35
- let centerLat = $state(0);
36
-
37
- // Update zoom level when map changes
38
- $effect(() => {
39
- const map = mapStore.map;
40
- if (!map) return;
41
-
42
- const updateZoom = () => {
43
- currentZoom = map.getZoom();
44
- centerLat = map.getCenter().lat;
45
- };
46
-
47
- updateZoom();
48
- map.on('zoom', updateZoom);
49
- map.on('move', updateZoom);
50
-
51
- return () => {
52
- map.off('zoom', updateZoom);
53
- map.off('move', updateZoom);
54
- };
55
- });
56
-
57
- // Derive highlighted cells based on selected features
58
- let highlightedCells = $derived.by(() => {
59
- if (!cellDataStore) return [];
60
-
61
- const cellIds = selectedFeatures
62
- .filter(f => f.layerId === 'cells-layer')
63
- .map(f => f.id);
64
-
65
- if (cellIds.length === 0) return [];
66
-
67
- return cellDataStore.filteredCells.filter(cell =>
68
- cellIds.includes(cell.siteId) ||
69
- cellIds.includes(cell.cellName) ||
70
- cellIds.includes(cell.cellID)
71
- );
72
- });
73
-
74
- // Derive highlighted sites based on selected features
75
- // TODO: Add site highlighting when SiteDataStore is available
76
- let highlightedSites = $derived.by(() => {
77
- return [];
78
- });
79
-
80
- // Generate GeoJSON for cell highlights
81
- let cellHighlightGeoJSON = $derived.by(() => {
82
- if (!cellDisplayStore || highlightedCells.length === 0) {
83
- return { type: 'FeatureCollection', features: [] };
84
- }
85
-
86
- // Calculate radius for current zoom level (same as CellsLayer does)
87
- const radiusMeters = calculateRadiusInMeters(centerLat, currentZoom, cellDisplayStore.targetPixelSize);
88
-
89
- const features = highlightedCells.map(cell =>
90
- generateCellArc(cell, radiusMeters, 100, highlightColor)
91
- );
92
-
93
- return {
94
- type: 'FeatureCollection' as const,
95
- features
96
- };
97
- });
98
-
99
- // Generate GeoJSON for site highlights
100
- let siteHighlightGeoJSON = $derived.by(() => {
101
- if (highlightedSites.length === 0) {
102
- return { type: 'FeatureCollection', features: [] };
103
- }
104
-
105
- const features = highlightedSites.map((site: any) => ({
106
- type: 'Feature' as const,
107
- geometry: {
108
- type: 'Point' as const,
109
- coordinates: [site.longitude, site.latitude]
110
- },
111
- properties: { siteId: site.siteId }
112
- }));
113
-
114
- return {
115
- type: 'FeatureCollection' as const,
116
- features
117
- };
118
- });
119
-
120
- // Initialize and manage layers
121
- $effect(() => {
122
- const map = mapStore.map;
123
- if (!map) return;
124
-
125
- const addLayers = () => {
126
- // Cell Selection Highlight Layer
127
- if (!map.getSource(cellsSourceId)) {
128
- map.addSource(cellsSourceId, {
129
- type: 'geojson',
130
- data: { type: 'FeatureCollection', features: [] }
131
- });
132
- }
133
-
134
- if (!map.getLayer(cellsLayerId)) {
135
- map.addLayer({
136
- id: cellsLayerId,
137
- type: 'line',
138
- source: cellsSourceId,
139
- paint: {
140
- 'line-color': highlightColor,
141
- 'line-width': highlightWidth,
142
- 'line-opacity': 1
143
- }
144
- });
145
- }
146
-
147
- // Site Selection Highlight Layer
148
- if (!map.getSource(sitesSourceId)) {
149
- map.addSource(sitesSourceId, {
150
- type: 'geojson',
151
- data: { type: 'FeatureCollection', features: [] }
152
- });
153
- }
154
-
155
- if (!map.getLayer(sitesLayerId)) {
156
- map.addLayer({
157
- id: sitesLayerId,
158
- type: 'circle',
159
- source: sitesSourceId,
160
- paint: {
161
- 'circle-radius': 12,
162
- 'circle-color': 'transparent',
163
- 'circle-stroke-color': highlightColor,
164
- 'circle-stroke-width': highlightWidth,
165
- 'circle-stroke-opacity': 1
166
- }
167
- });
168
- }
169
- };
170
-
171
- // Initial setup
172
- addLayers();
173
-
174
- // Events
175
- map.on('style.load', addLayers);
176
-
177
- // Cleanup
178
- return () => {
179
- map.off('style.load', addLayers);
180
-
181
- if (map.getLayer(cellsLayerId)) map.removeLayer(cellsLayerId);
182
- if (map.getLayer(sitesLayerId)) map.removeLayer(sitesLayerId);
183
- if (map.getSource(cellsSourceId)) map.removeSource(cellsSourceId);
184
- if (map.getSource(sitesSourceId)) map.removeSource(sitesSourceId);
185
- };
186
- });
187
-
188
- // Update cell highlight source
189
- $effect(() => {
190
- const map = mapStore.map;
191
- if (!map) return;
192
-
193
- const source = map.getSource(cellsSourceId) as mapboxgl.GeoJSONSource;
194
- if (source) {
195
- source.setData(cellHighlightGeoJSON as any);
196
- }
197
- });
198
-
199
- // Update site highlight source
200
- $effect(() => {
201
- const map = mapStore.map;
202
- if (!map) return;
203
-
204
- const source = map.getSource(sitesSourceId) as mapboxgl.GeoJSONSource;
205
- if (source) {
206
- source.setData(siteHighlightGeoJSON as any);
207
- }
208
- });
209
- </script>
@@ -1,13 +0,0 @@
1
- import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
2
- import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
3
- import type { SelectedFeature } from '../types';
4
- interface Props {
5
- selectedFeatures: SelectedFeature[];
6
- cellDataStore?: CellDataStore;
7
- cellDisplayStore?: CellDisplayStore;
8
- highlightColor?: string;
9
- highlightWidth?: number;
10
- }
11
- declare const SelectionHighlightLayers: import("svelte").Component<Props, {}, "">;
12
- type SelectionHighlightLayers = ReturnType<typeof SelectionHighlightLayers>;
13
- export default SelectionHighlightLayers;