@smartnet360/svelte-components 0.0.101 → 0.0.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/antenna-pattern/index.d.ts +1 -0
- package/dist/apps/antenna-pattern/index.js +1 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.d.ts +17 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.js +83 -0
- package/dist/apps/site-check/SiteCheck.svelte +4 -6
- package/dist/core/Charts/ChartCard.svelte +122 -12
- package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
- package/dist/core/Charts/ChartComponent.svelte +8 -6
- package/dist/core/CoverageMap/ai/AITools.d.ts +117 -0
- package/dist/core/CoverageMap/ai/AITools.js +380 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.d.ts +138 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.js +375 -0
- package/dist/core/CoverageMap/core/GridCalculator.d.ts +115 -0
- package/dist/core/CoverageMap/core/GridCalculator.js +484 -0
- package/dist/core/CoverageMap/core/PathLossModels.d.ts +253 -0
- package/dist/core/CoverageMap/core/PathLossModels.js +380 -0
- package/dist/core/CoverageMap/core/SignalProcessor.d.ts +288 -0
- package/dist/core/CoverageMap/core/SignalProcessor.js +424 -0
- package/dist/core/CoverageMap/data/AntennaStore.d.ts +165 -0
- package/dist/core/CoverageMap/data/AntennaStore.js +327 -0
- package/dist/core/CoverageMap/data/SiteStore.d.ts +155 -0
- package/dist/core/CoverageMap/data/SiteStore.js +355 -0
- package/dist/core/CoverageMap/index.d.ts +74 -0
- package/dist/core/CoverageMap/index.js +103 -0
- package/dist/core/CoverageMap/types.d.ts +252 -0
- package/dist/core/CoverageMap/types.js +7 -0
- package/dist/core/CoverageMap/utils/geoUtils.d.ts +223 -0
- package/dist/core/CoverageMap/utils/geoUtils.js +374 -0
- package/dist/core/CoverageMap/utils/rfUtils.d.ts +329 -0
- package/dist/core/CoverageMap/utils/rfUtils.js +434 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.d.ts +149 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.js +377 -0
- package/dist/core/TreeView/index.d.ts +4 -4
- package/dist/core/TreeView/index.js +5 -5
- package/dist/core/TreeView/tree-utils.d.ts +12 -0
- package/dist/core/TreeView/tree-utils.js +115 -6
- package/dist/core/TreeView/tree.store.svelte.d.ts +94 -0
- package/dist/core/TreeView/tree.store.svelte.js +274 -0
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +16 -27
- package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
- package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +33 -42
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +12 -19
- package/dist/map-v3/core/components/Map.svelte +4 -0
- package/dist/map-v3/core/stores/map.store.svelte.js +2 -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/components/CellFilterControl.svelte +24 -30
- 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/coverage/index.d.ts +12 -0
- package/dist/map-v3/features/coverage/index.js +16 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte +198 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte.d.ts +10 -0
- package/dist/map-v3/features/coverage/logic/coloring.d.ts +28 -0
- package/dist/map-v3/features/coverage/logic/coloring.js +77 -0
- package/dist/map-v3/features/coverage/logic/geometry.d.ts +33 -0
- package/dist/map-v3/features/coverage/logic/geometry.js +112 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.d.ts +46 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.js +95 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.d.ts +33 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.js +90 -0
- package/dist/map-v3/features/coverage/types.d.ts +52 -0
- package/dist/map-v3/features/coverage/types.js +7 -0
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -20
- 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/dist/map-v3/features/sites/components/SiteFilterControl.svelte +23 -33
- package/dist/map-v3/index.d.ts +4 -0
- package/dist/map-v3/index.js +5 -0
- package/package.json +2 -2
- package/dist/core/TreeView/tree.store.d.ts +0 -10
- package/dist/core/TreeView/tree.store.js +0 -320
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
|
@@ -102,11 +102,37 @@
|
|
|
102
102
|
<FeatureSelectionControl
|
|
103
103
|
position="bottom-left"
|
|
104
104
|
cellDataStore={cellData}
|
|
105
|
-
|
|
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)`);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy, untrack } from 'svelte';
|
|
3
3
|
import { MapControl } from '../../../shared';
|
|
4
|
-
import { createTreeStore } from '../../../../core/TreeView
|
|
5
|
-
import TreeView from '../../../../core/TreeView/TreeView.svelte';
|
|
4
|
+
import { createTreeStore, TreeView } from '../../../../core/TreeView';
|
|
6
5
|
import type { CellDataStore } from '../stores/cell.data.svelte';
|
|
7
6
|
import type { CellRegistry } from '../stores/cell.registry.svelte';
|
|
8
7
|
import type { CellDisplayStore } from '../stores/cell.display.svelte';
|
|
@@ -64,36 +63,31 @@
|
|
|
64
63
|
|
|
65
64
|
// Sync Tree Selection -> Cell Registry Visibility
|
|
66
65
|
$effect(() => {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
const val = treeStore;
|
|
67
|
+
// Iterate all leaf nodes to sync visibility
|
|
68
|
+
// This is a bit heavy but ensures consistency.
|
|
69
|
+
// Optimization: Only update changed nodes if we had a diff.
|
|
70
|
+
|
|
71
|
+
// We only care about leaf nodes (bands)
|
|
72
|
+
let changes = 0;
|
|
73
|
+
val.state.nodes.forEach((nodeState) => {
|
|
74
|
+
if (nodeState.node.children && nodeState.node.children.length > 0) return; // Skip folders
|
|
75
|
+
|
|
76
|
+
const groupId = nodeState.node.id;
|
|
77
|
+
// IMPORTANT: Read from checkedPaths set, NOT nodeState.checked (which is static/stale)
|
|
78
|
+
const isVisible = val.state.checkedPaths.has(nodeState.path);
|
|
71
79
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// IMPORTANT: Read from checkedPaths set, NOT nodeState.checked (which is static/stale)
|
|
79
|
-
const isVisible = val.state.checkedPaths.has(nodeState.path);
|
|
80
|
-
|
|
81
|
-
// Update registry if different
|
|
82
|
-
const currentStyle = registry.getStyle(groupId, '#000'); // Color doesn't matter here
|
|
83
|
-
if (currentStyle.visible !== isVisible) {
|
|
84
|
-
// console.log(`[CellFilterControl] Syncing ${groupId}: ${currentStyle.visible} -> ${isVisible}`);
|
|
85
|
-
registry.toggleVisibility(groupId);
|
|
86
|
-
changes++;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
if (changes > 0) {
|
|
90
|
-
console.log(`[CellFilterControl] Synced ${changes} visibility changes to registry`);
|
|
80
|
+
// Update registry if different
|
|
81
|
+
const currentStyle = registry.getStyle(groupId, '#000'); // Color doesn't matter here
|
|
82
|
+
if (currentStyle.visible !== isVisible) {
|
|
83
|
+
// console.log(`[CellFilterControl] Syncing ${groupId}: ${currentStyle.visible} -> ${isVisible}`);
|
|
84
|
+
registry.toggleVisibility(groupId);
|
|
85
|
+
changes++;
|
|
91
86
|
}
|
|
92
87
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
};
|
|
88
|
+
if (changes > 0) {
|
|
89
|
+
console.log(`[CellFilterControl] Synced ${changes} visibility changes to registry`);
|
|
90
|
+
}
|
|
97
91
|
});
|
|
98
92
|
|
|
99
93
|
function handleColorChange(groupId: string, event: Event) {
|
|
@@ -184,7 +178,7 @@
|
|
|
184
178
|
No cells loaded.
|
|
185
179
|
</div>
|
|
186
180
|
{:else}
|
|
187
|
-
<TreeView showControls={false} store={
|
|
181
|
+
<TreeView showControls={false} store={treeStore} height="300px">
|
|
188
182
|
{#snippet children({ node, state })}
|
|
189
183
|
<!-- Color Picker (Only for leaves) -->
|
|
190
184
|
{#if !node.children || node.children.length === 0}
|
|
@@ -66,24 +66,44 @@
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (!map.getLayer(lineLayerId)) {
|
|
69
|
-
// Line Layer (Border) - Status-based styling
|
|
69
|
+
// Line Layer (Border) - Status-based styling with feature-state support
|
|
70
70
|
map.addLayer({
|
|
71
71
|
id: lineLayerId,
|
|
72
72
|
type: 'line',
|
|
73
73
|
source: sourceId,
|
|
74
74
|
paint: {
|
|
75
|
-
'line-color': [
|
|
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();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage Feature - Public Exports
|
|
3
|
+
*
|
|
4
|
+
* Self-contained RF coverage prediction and visualization.
|
|
5
|
+
* Zero dependencies on cells/sites features.
|
|
6
|
+
*/
|
|
7
|
+
export * from './types';
|
|
8
|
+
export { CoverageDataStore, createCoverageDataStore } from './stores/coverage.data.svelte';
|
|
9
|
+
export { CoverageDisplayStore, createCoverageDisplayStore } from './stores/coverage.display.svelte';
|
|
10
|
+
export { default as CoverageLayer } from './layers/CoverageLayer.svelte';
|
|
11
|
+
export { gridToGeoJSON, getGridBounds, getSignalRange } from './logic/geometry';
|
|
12
|
+
export { getColorForSignal, getLegendItems } from './logic/coloring';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage Feature - Public Exports
|
|
3
|
+
*
|
|
4
|
+
* Self-contained RF coverage prediction and visualization.
|
|
5
|
+
* Zero dependencies on cells/sites features.
|
|
6
|
+
*/
|
|
7
|
+
// Types
|
|
8
|
+
export * from './types';
|
|
9
|
+
// Stores
|
|
10
|
+
export { CoverageDataStore, createCoverageDataStore } from './stores/coverage.data.svelte';
|
|
11
|
+
export { CoverageDisplayStore, createCoverageDisplayStore } from './stores/coverage.display.svelte';
|
|
12
|
+
// Layer
|
|
13
|
+
export { default as CoverageLayer } from './layers/CoverageLayer.svelte';
|
|
14
|
+
// Utilities (for advanced usage)
|
|
15
|
+
export { gridToGeoJSON, getGridBounds, getSignalRange } from './logic/geometry';
|
|
16
|
+
export { getColorForSignal, getLegendItems } from './logic/coloring';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Coverage Layer
|
|
4
|
+
*
|
|
5
|
+
* Renders coverage grid as Mapbox fill layer.
|
|
6
|
+
* Self-contained - only needs map instance from context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getContext, onMount } from 'svelte';
|
|
10
|
+
import type { MapStore } from '../../../core/stores/map.store.svelte';
|
|
11
|
+
import type { CoverageDataStore } from '../stores/coverage.data.svelte';
|
|
12
|
+
import type { CoverageDisplayStore } from '../stores/coverage.display.svelte';
|
|
13
|
+
import { gridToGeoJSON, getGridBounds } from '../logic/geometry';
|
|
14
|
+
import type mapboxgl from 'mapbox-gl';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
dataStore: CoverageDataStore;
|
|
18
|
+
displayStore: CoverageDisplayStore;
|
|
19
|
+
autoFitBounds?: boolean; // Automatically fit map to coverage bounds
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { dataStore, displayStore, autoFitBounds = true }: Props = $props();
|
|
23
|
+
|
|
24
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
25
|
+
const sourceId = 'coverage-source';
|
|
26
|
+
const layerId = 'coverage-layer';
|
|
27
|
+
const componentId = Math.random().toString(36).substring(7);
|
|
28
|
+
|
|
29
|
+
let layerInitialized = $state(false);
|
|
30
|
+
|
|
31
|
+
console.log(`[CoverageLayer-${componentId}] Component created, mapStore:`, mapStore);
|
|
32
|
+
|
|
33
|
+
// Main effect: manage layer lifecycle - runs once (avoid reactive dependencies!)
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const map = mapStore.map;
|
|
36
|
+
if (!map) return;
|
|
37
|
+
|
|
38
|
+
// Initialize layers when style is loaded
|
|
39
|
+
const initializeLayers = () => {
|
|
40
|
+
// Always check if layer already exists (style might have reloaded)
|
|
41
|
+
if (map.getLayer(layerId)) {
|
|
42
|
+
console.log(`[CoverageLayer-${componentId}] Layer already exists, skipping initialization`);
|
|
43
|
+
layerInitialized = true;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`[CoverageLayer-${componentId}] Initializing layer...`);
|
|
48
|
+
|
|
49
|
+
// Add source if it doesn't exist
|
|
50
|
+
if (!map.getSource(sourceId)) {
|
|
51
|
+
map.addSource(sourceId, {
|
|
52
|
+
type: 'geojson',
|
|
53
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
54
|
+
});
|
|
55
|
+
console.log(`[CoverageLayer-${componentId}] Source added:`, sourceId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add layer - use static defaults
|
|
59
|
+
map.addLayer({
|
|
60
|
+
id: layerId,
|
|
61
|
+
type: 'fill',
|
|
62
|
+
source: sourceId,
|
|
63
|
+
paint: {
|
|
64
|
+
'fill-color': ['get', 'color'],
|
|
65
|
+
'fill-opacity': 0.7 // Will be updated by separate effect
|
|
66
|
+
},
|
|
67
|
+
layout: {
|
|
68
|
+
'visibility': 'visible' // Will be updated by separate effect
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
console.log(`[CoverageLayer-${componentId}] Layer added:`, layerId);
|
|
72
|
+
|
|
73
|
+
layerInitialized = true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Listen to style.load event - fires when style finishes loading (and on style changes)
|
|
77
|
+
const handleStyleLoad = () => {
|
|
78
|
+
console.log(`[CoverageLayer-${componentId}] Style loaded, reinitializing layers...`);
|
|
79
|
+
layerInitialized = false; // Reset flag so layers are recreated
|
|
80
|
+
initializeLayers();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Initialize immediately if style is already loaded
|
|
84
|
+
if (map.isStyleLoaded()) {
|
|
85
|
+
initializeLayers();
|
|
86
|
+
} else {
|
|
87
|
+
map.once('style.load', initializeLayers);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Re-initialize layers when style changes/reloads
|
|
91
|
+
map.on('style.load', handleStyleLoad);
|
|
92
|
+
|
|
93
|
+
// Cleanup on unmount only
|
|
94
|
+
return () => {
|
|
95
|
+
console.log(`[CoverageLayer-${componentId}] Component unmounting, cleaning up...`);
|
|
96
|
+
map.off('style.load', handleStyleLoad);
|
|
97
|
+
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
|
98
|
+
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
99
|
+
layerInitialized = false;
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Update layer visibility and opacity dynamically
|
|
104
|
+
$effect(() => {
|
|
105
|
+
const map = mapStore.map;
|
|
106
|
+
if (!map || !layerInitialized) return;
|
|
107
|
+
|
|
108
|
+
const opacity = displayStore.opacity;
|
|
109
|
+
const visible = displayStore.visible;
|
|
110
|
+
|
|
111
|
+
if (map.getLayer(layerId)) {
|
|
112
|
+
map.setPaintProperty(layerId, 'fill-opacity', opacity);
|
|
113
|
+
map.setLayoutProperty(layerId, 'visibility', visible ? 'visible' : 'none');
|
|
114
|
+
console.log(`[CoverageLayer-${componentId}] Updated opacity:`, opacity, 'visible:', visible);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// React to data changes - separate effect
|
|
119
|
+
$effect(() => {
|
|
120
|
+
const map = mapStore.map;
|
|
121
|
+
if (!map || !layerInitialized) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Explicitly read dependencies for reactivity
|
|
126
|
+
const hasResults = dataStore.hasResults;
|
|
127
|
+
const colorScheme = displayStore.colorScheme;
|
|
128
|
+
const minSignal = displayStore.minSignalThreshold;
|
|
129
|
+
const maxSignal = displayStore.maxSignalThreshold;
|
|
130
|
+
|
|
131
|
+
console.log(`[CoverageLayer-${componentId}] Data/settings changed, hasResults:`, hasResults);
|
|
132
|
+
|
|
133
|
+
if (hasResults) {
|
|
134
|
+
updateLayer(map);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function updateLayer(map: mapboxgl.Map) {
|
|
139
|
+
if (!dataStore.result) {
|
|
140
|
+
console.log(`[CoverageLayer-${componentId}] No coverage data to render`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const grid = dataStore.result.grid;
|
|
145
|
+
const thresholds = dataStore.result.config.signalThresholds;
|
|
146
|
+
|
|
147
|
+
console.log(`[CoverageLayer-${componentId}] Grid structure:`, {
|
|
148
|
+
rows: grid.cells.length,
|
|
149
|
+
cols: grid.cells[0]?.length,
|
|
150
|
+
cellSizeMeters: grid.cellSizeMeters,
|
|
151
|
+
bounds: grid.bounds,
|
|
152
|
+
firstCell: grid.cells[0]?.[0]
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Convert grid to GeoJSON
|
|
156
|
+
const geojson = gridToGeoJSON(
|
|
157
|
+
grid,
|
|
158
|
+
displayStore.colorScheme,
|
|
159
|
+
displayStore.minSignalThreshold,
|
|
160
|
+
displayStore.maxSignalThreshold,
|
|
161
|
+
thresholds
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
console.log(`[CoverageLayer-${componentId}] GeoJSON created:`, {
|
|
165
|
+
features: geojson.features.length,
|
|
166
|
+
firstFeature: geojson.features[0],
|
|
167
|
+
firstFeatureCoords: geojson.features[0]?.geometry.coordinates,
|
|
168
|
+
firstFeatureColor: geojson.features[0]?.properties?.color
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Check if layer exists
|
|
172
|
+
console.log(`[CoverageLayer-${componentId}] Layer exists?`, map.getLayer(layerId) !== undefined);
|
|
173
|
+
console.log(`[CoverageLayer-${componentId}] Source exists?`, map.getSource(sourceId) !== undefined);
|
|
174
|
+
|
|
175
|
+
// Update source
|
|
176
|
+
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
177
|
+
if (source) {
|
|
178
|
+
source.setData(geojson);
|
|
179
|
+
console.log(`[CoverageLayer-${componentId}] Source data updated successfully`);
|
|
180
|
+
|
|
181
|
+
// Force a repaint
|
|
182
|
+
map.triggerRepaint();
|
|
183
|
+
} else {
|
|
184
|
+
console.error('[CoverageLayer] Source not found:', sourceId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Auto-fit bounds on first render
|
|
188
|
+
if (autoFitBounds && geojson.features.length > 0) {
|
|
189
|
+
const bounds = getGridBounds(grid);
|
|
190
|
+
console.log(`[CoverageLayer-${componentId}] Fitting bounds:`, bounds);
|
|
191
|
+
map.fitBounds(bounds, {
|
|
192
|
+
padding: 50,
|
|
193
|
+
maxZoom: 14,
|
|
194
|
+
duration: 1000
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
</script>
|