@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.
- package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
- package/dist/map-v3/demo/DemoMap.svelte +31 -5
- package/dist/map-v3/demo/demo-cells.js +51 -22
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +29 -9
- package/dist/map-v3/features/cells/logic/geometry.js +3 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +27 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.js +65 -0
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +82 -65
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +5 -9
- package/dist/map-v3/features/selection/index.d.ts +1 -2
- package/dist/map-v3/features/selection/index.js +0 -1
- package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +44 -15
- package/dist/map-v3/features/selection/stores/selection.store.svelte.js +163 -40
- package/dist/map-v3/features/selection/types.d.ts +4 -2
- package/package.json +1 -1
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
152
|
-
sectorsToGenerate.forEach((
|
|
153
|
-
// Generate
|
|
154
|
-
TECH_BANDS.forEach((techBand,
|
|
155
|
-
|
|
156
|
-
const
|
|
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:
|
|
160
|
-
txId:
|
|
161
|
-
cellID:
|
|
162
|
-
cellID2G: techBand.tech === '2G' ?
|
|
163
|
-
cellName:
|
|
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 +
|
|
173
|
-
ctrlid: techBand.tech === '2G' ? `CTRL-${
|
|
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 +
|
|
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: `${
|
|
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:
|
|
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-${
|
|
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': [
|
|
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
|
-
['
|
|
79
|
-
|
|
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
|
-
['
|
|
85
|
-
['
|
|
86
|
-
[
|
|
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
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
74
|
+
function handleModeChange(event: Event) {
|
|
75
75
|
const target = event.target as HTMLSelectElement;
|
|
76
|
-
|
|
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 =
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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="
|
|
122
|
+
controlWidth="320px"
|
|
113
123
|
>
|
|
114
124
|
<div class="feature-selection-control">
|
|
115
|
-
<!--
|
|
125
|
+
<!-- Selection Mode Selector -->
|
|
116
126
|
<div class="mb-3">
|
|
117
|
-
<label for="
|
|
127
|
+
<label for="mode-select" class="form-label small fw-semibold">Selection Mode</label>
|
|
118
128
|
<select
|
|
119
|
-
id="
|
|
129
|
+
id="mode-select"
|
|
120
130
|
class="form-select form-select-sm"
|
|
121
|
-
value={
|
|
122
|
-
onchange={
|
|
131
|
+
value={currentMode}
|
|
132
|
+
onchange={handleModeChange}
|
|
123
133
|
>
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
{
|
|
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
|
|
132
|
-
<
|
|
133
|
-
|
|
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-
|
|
192
|
+
<i class="bi bi-grid-3x3-gap-fill feature-icon"></i>
|
|
166
193
|
<div class="feature-info">
|
|
167
|
-
<span class="feature-id">
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
221
|
+
<div class="mt-1">Click on map to select</div>
|
|
189
222
|
</div>
|
|
190
223
|
{/if}
|
|
191
224
|
|
|
192
|
-
<!-- Action
|
|
193
|
-
{#if
|
|
225
|
+
<!-- Custom Action Buttons (snippet) -->
|
|
226
|
+
{#if children}
|
|
194
227
|
<div class="mt-3">
|
|
195
|
-
|
|
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
|
|
2
|
+
import { type SelectionMode } from '../stores/selection.store.svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
3
4
|
interface Props {
|
|
4
|
-
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
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
|
26
|
+
* Set the cell data store for lookup maps
|
|
22
27
|
*/
|
|
23
|
-
|
|
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
|
-
*
|
|
46
|
+
* Handle click on a cell - expand to site/sector based on mode
|
|
38
47
|
*/
|
|
39
|
-
|
|
48
|
+
private handleCellClick;
|
|
40
49
|
/**
|
|
41
|
-
*
|
|
50
|
+
* Handle click on a site marker (if sites layer exists)
|
|
42
51
|
*/
|
|
43
|
-
|
|
52
|
+
private handleSiteClick;
|
|
44
53
|
/**
|
|
45
|
-
* Toggle a
|
|
54
|
+
* Toggle a group of cells (site or sector)
|
|
46
55
|
*/
|
|
47
|
-
|
|
56
|
+
private toggleGroupSelection;
|
|
48
57
|
/**
|
|
49
|
-
*
|
|
58
|
+
* Update Mapbox feature-state for cells
|
|
50
59
|
*/
|
|
51
|
-
|
|
60
|
+
private updateFeatureStates;
|
|
52
61
|
/**
|
|
53
|
-
*
|
|
62
|
+
* Enable selection mode
|
|
54
63
|
*/
|
|
55
|
-
|
|
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
|
|
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
|
-
*
|
|
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('
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
84
|
+
* Handle click on a cell - expand to site/sector based on mode
|
|
73
85
|
*/
|
|
74
|
-
|
|
75
|
-
this.
|
|
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
|
-
*
|
|
122
|
+
* Handle click on a site marker (if sites layer exists)
|
|
79
123
|
*/
|
|
80
|
-
|
|
81
|
-
this.
|
|
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
|
|
132
|
+
* Toggle a group of cells (site or sector)
|
|
85
133
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
94
|
-
this.selectedFeatures.push({
|
|
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
|
-
*
|
|
162
|
+
* Update Mapbox feature-state for cells
|
|
103
163
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
*
|
|
192
|
+
* Enable selection mode
|
|
115
193
|
*/
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
*
|
|
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,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;
|