@smartnet360/svelte-components 0.0.94 → 0.0.95
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-v3/demo/DemoMap.svelte +1 -1
- package/dist/map-v3/demo/demo-cells.d.ts +2 -1
- package/dist/map-v3/demo/demo-cells.js +105 -18
- package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte +1 -80
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +17 -36
- package/dist/map-v3/features/cells/logic/site-distance.d.ts +2 -4
- package/dist/map-v3/features/cells/logic/site-distance.js +5 -13
- package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +2 -8
- package/dist/map-v3/features/cells/stores/cell.data.svelte.js +4 -16
- package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +0 -5
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +0 -16
- package/dist/map-v3/features/cells/stores/site.distance.svelte.d.ts +3 -14
- package/dist/map-v3/features/cells/stores/site.distance.svelte.js +13 -66
- package/dist/map-v3/features/sites/components/SiteSettingsPanel.svelte +1 -0
- package/dist/map-v3/features/sites/layers/SitesLayer.svelte +2 -1
- package/dist/map-v3/features/sites/stores/site.data.svelte.js +4 -1
- package/dist/map-v3/features/sites/types.d.ts +1 -0
- package/package.json +1 -1
- package/dist/map-v3/features/cells/logic/grid-density.d.ts +0 -51
- package/dist/map-v3/features/cells/logic/grid-density.js +0 -138
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
let { accessToken }: Props = $props();
|
|
31
31
|
|
|
32
32
|
// Initialize stores
|
|
33
|
+
const cellData = createCellDataStore();
|
|
33
34
|
const cellRegistry = createCellRegistry('demo-map');
|
|
34
35
|
const cellDisplay = new CellDisplayStore();
|
|
35
|
-
const cellData = createCellDataStore(cellDisplay);
|
|
36
36
|
|
|
37
37
|
const siteData = createSiteDataStore(cellData);
|
|
38
38
|
const siteRegistry = createSiteRegistry('demo-map');
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Cell } from '../features/cells/types';
|
|
10
10
|
/**
|
|
11
|
-
* Generate demo cells
|
|
11
|
+
* Generate demo cells with varied density patterns in circular distribution
|
|
12
|
+
* Creates density zones radiating from center with random placement
|
|
12
13
|
*/
|
|
13
14
|
export declare const demoCells: Cell[];
|
|
@@ -8,11 +8,66 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const BASE_LAT = 47.4979;
|
|
10
10
|
const BASE_LNG = 19.0402;
|
|
11
|
-
//
|
|
12
|
-
const NUM_SITES =
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
11
|
+
// Generate sites in a circular pattern with varying density
|
|
12
|
+
const NUM_SITES = 2000;
|
|
13
|
+
const RADIUS_KM = 15; // 15km radius circle
|
|
14
|
+
const RADIUS_DEGREES = RADIUS_KM / 111; // Approximate conversion
|
|
15
|
+
// Density zones (distance from center)
|
|
16
|
+
const DENSITY_ZONES = [
|
|
17
|
+
{ maxRadius: 0.3, minSpacing: 0.0008, maxSpacing: 0.0015, name: 'Very Dense Core' }, // 0-3km: 80-150m spacing
|
|
18
|
+
{ maxRadius: 0.5, minSpacing: 0.0015, maxSpacing: 0.003, name: 'Dense Inner' }, // 3-5km: 150-300m spacing
|
|
19
|
+
{ maxRadius: 0.7, minSpacing: 0.003, maxSpacing: 0.006, name: 'Medium' }, // 5-7km: 300-600m spacing
|
|
20
|
+
{ maxRadius: 0.85, minSpacing: 0.006, maxSpacing: 0.012, name: 'Sparse Suburban' }, // 7-12km: 600m-1.2km spacing
|
|
21
|
+
{ maxRadius: 1.0, minSpacing: 0.012, maxSpacing: 0.025, name: 'Very Sparse Rural' } // 12-15km: 1.2-2.5km spacing
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Get density zone for a given normalized radius
|
|
25
|
+
*/
|
|
26
|
+
function getDensityZone(normalizedRadius) {
|
|
27
|
+
for (const zone of DENSITY_ZONES) {
|
|
28
|
+
if (normalizedRadius <= zone.maxRadius) {
|
|
29
|
+
return zone;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return DENSITY_ZONES[DENSITY_ZONES.length - 1];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generate random point within circle using polar coordinates
|
|
36
|
+
*/
|
|
37
|
+
function generateRandomPointInCircle() {
|
|
38
|
+
// Use square root for uniform distribution in circle
|
|
39
|
+
const r = Math.sqrt(Math.random()) * RADIUS_DEGREES;
|
|
40
|
+
const theta = Math.random() * 2 * Math.PI;
|
|
41
|
+
const lat = BASE_LAT + r * Math.cos(theta);
|
|
42
|
+
const lng = BASE_LNG + r * Math.sin(theta);
|
|
43
|
+
const normalizedRadius = r / RADIUS_DEGREES;
|
|
44
|
+
return { lat, lng, normalizedRadius };
|
|
45
|
+
}
|
|
46
|
+
// Cluster configuration for varied density
|
|
47
|
+
// (kept for backward compatibility but not used with circular generation)
|
|
48
|
+
const CLUSTERS = [
|
|
49
|
+
// Dense urban cluster (top-left) - very tight spacing
|
|
50
|
+
{ startRow: 0, endRow: 3, startCol: 0, endCol: 3, spacing: 0.3 },
|
|
51
|
+
// Medium density cluster (center) - normal spacing
|
|
52
|
+
{ startRow: 3, endRow: 7, startCol: 3, endCol: 7, spacing: 1.0 },
|
|
53
|
+
// Sparse rural cluster (bottom-right) - wide spacing
|
|
54
|
+
{ startRow: 7, endRow: 10, startCol: 7, endCol: 10, spacing: 2.5 },
|
|
55
|
+
// Random outliers scattered around
|
|
56
|
+
{ startRow: 0, endRow: 10, startCol: 0, endCol: 10, spacing: 1.5 }
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Add random jitter to coordinates for natural variation
|
|
60
|
+
*/
|
|
61
|
+
function addJitter(value, maxJitter) {
|
|
62
|
+
return value + (Math.random() - 0.5) * 2 * maxJitter;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Determine if site should be skipped (for creating gaps)
|
|
66
|
+
*/
|
|
67
|
+
function shouldSkipSite(row, col) {
|
|
68
|
+
// Skip some sites randomly to create density variation (20% skip rate)
|
|
69
|
+
return Math.random() < 0.2;
|
|
70
|
+
}
|
|
16
71
|
// Standard beamwidth for sectors
|
|
17
72
|
const BEAMWIDTH = 65;
|
|
18
73
|
// Cell tech-band definitions with proper fband format
|
|
@@ -50,20 +105,51 @@ const STATUSES = [
|
|
|
50
105
|
'On_Air'
|
|
51
106
|
];
|
|
52
107
|
/**
|
|
53
|
-
* Generate demo cells
|
|
108
|
+
* Generate demo cells with varied density patterns in circular distribution
|
|
109
|
+
* Creates density zones radiating from center with random placement
|
|
54
110
|
*/
|
|
55
111
|
export const demoCells = [];
|
|
56
112
|
let cellCounter = 1;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
113
|
+
let actualSiteIndex = 0;
|
|
114
|
+
// Track used positions to maintain minimum spacing
|
|
115
|
+
const usedPositions = [];
|
|
116
|
+
/**
|
|
117
|
+
* Check if position is too close to existing sites
|
|
118
|
+
*/
|
|
119
|
+
function isTooClose(lat, lng, minSpacing) {
|
|
120
|
+
for (const pos of usedPositions) {
|
|
121
|
+
const distance = Math.sqrt(Math.pow(lat - pos.lat, 2) + Math.pow(lng - pos.lng, 2));
|
|
122
|
+
const requiredSpacing = (minSpacing + pos.minSpacing) / 2;
|
|
123
|
+
if (distance < requiredSpacing) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Generate sites in a circular pattern with density-based placement
|
|
130
|
+
for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; attempt++) {
|
|
131
|
+
// Generate random point in circle
|
|
132
|
+
const { lat, lng, normalizedRadius } = generateRandomPointInCircle();
|
|
133
|
+
// Get density zone for this radius
|
|
134
|
+
const zone = getDensityZone(normalizedRadius);
|
|
135
|
+
// Random spacing within zone range
|
|
136
|
+
const minSpacing = zone.minSpacing + Math.random() * (zone.maxSpacing - zone.minSpacing);
|
|
137
|
+
// Check if too close to existing sites
|
|
138
|
+
if (isTooClose(lat, lng, minSpacing)) {
|
|
139
|
+
continue; // Try another position
|
|
140
|
+
}
|
|
141
|
+
// Add random jitter for natural variation
|
|
142
|
+
const jitterAmount = minSpacing * 0.3; // 30% of spacing
|
|
143
|
+
const siteLat = addJitter(lat, jitterAmount);
|
|
144
|
+
const siteLng = addJitter(lng, jitterAmount);
|
|
145
|
+
// Record position
|
|
146
|
+
usedPositions.push({ lat: siteLat, lng: siteLng, minSpacing });
|
|
147
|
+
const siteId = `DEMO-SITE-${String(actualSiteIndex + 1).padStart(4, '0')}`;
|
|
148
|
+
actualSiteIndex++;
|
|
149
|
+
// Generate 3 sectors per site (with some random 1 or 2 sector sites)
|
|
150
|
+
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) => {
|
|
67
153
|
// Generate 12 tech-bands per sector
|
|
68
154
|
TECH_BANDS.forEach((techBand, techIndex) => {
|
|
69
155
|
const cellId = `CELL-${String(cellCounter).padStart(4, '0')}`;
|
|
@@ -117,10 +203,11 @@ for (let siteIndex = 0; siteIndex < NUM_SITES; siteIndex++) {
|
|
|
117
203
|
// Other
|
|
118
204
|
other: {
|
|
119
205
|
demoCell: true,
|
|
120
|
-
siteNumber:
|
|
206
|
+
siteNumber: actualSiteIndex,
|
|
121
207
|
sector: sectorIndex + 1,
|
|
122
208
|
techBandKey: `${techBand.tech}_${techBand.band}`,
|
|
123
|
-
|
|
209
|
+
radius: normalizedRadius,
|
|
210
|
+
densityZone: zone.name
|
|
124
211
|
},
|
|
125
212
|
customSubgroup: `Sector-${sectorIndex + 1}`
|
|
126
213
|
});
|
|
@@ -75,10 +75,9 @@
|
|
|
75
75
|
class="form-select form-select-sm"
|
|
76
76
|
bind:value={displayStore.layerGrouping}
|
|
77
77
|
>
|
|
78
|
-
<option value="frequency">
|
|
78
|
+
<option value="frequency">Frequency Priority</option>
|
|
79
79
|
<option value="technology">Technology Priority</option>
|
|
80
80
|
<option value="balanced">Balanced</option>
|
|
81
|
-
<option value="ltePriority">LTE Priority</option>
|
|
82
81
|
</select>
|
|
83
82
|
</div>
|
|
84
83
|
</div>
|
|
@@ -115,84 +114,6 @@
|
|
|
115
114
|
</select>
|
|
116
115
|
</div>
|
|
117
116
|
</div>
|
|
118
|
-
|
|
119
|
-
<!-- Neighbor Count -->
|
|
120
|
-
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
121
|
-
<div class="col-4 text-secondary small">Neighbors</div>
|
|
122
|
-
<div class="col-3 text-end">
|
|
123
|
-
<span class="badge bg-white text-muted border">{displayStore.autoSizeNeighborCount}</span>
|
|
124
|
-
</div>
|
|
125
|
-
<div class="col-5">
|
|
126
|
-
<input
|
|
127
|
-
type="range"
|
|
128
|
-
class="form-range w-100"
|
|
129
|
-
min="1"
|
|
130
|
-
max="10"
|
|
131
|
-
step="1"
|
|
132
|
-
bind:value={displayStore.autoSizeNeighborCount}
|
|
133
|
-
title="Number of nearest neighbors to average for density calculation"
|
|
134
|
-
/>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<!-- Size Multiplier -->
|
|
139
|
-
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
140
|
-
<div class="col-4 text-secondary small">Size Scale</div>
|
|
141
|
-
<div class="col-3 text-end">
|
|
142
|
-
<span class="badge bg-white text-muted border">{displayStore.autoSizeMultiplier.toFixed(1)}x</span>
|
|
143
|
-
</div>
|
|
144
|
-
<div class="col-5">
|
|
145
|
-
<input
|
|
146
|
-
type="range"
|
|
147
|
-
class="form-range w-100"
|
|
148
|
-
min="0.3"
|
|
149
|
-
max="2.0"
|
|
150
|
-
step="0.1"
|
|
151
|
-
bind:value={displayStore.autoSizeMultiplier}
|
|
152
|
-
title="Scale all auto-sized cells up or down"
|
|
153
|
-
/>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<!-- Grid-Based Outlier Capping -->
|
|
158
|
-
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
159
|
-
<div class="col-4 text-secondary small">Grid Capping</div>
|
|
160
|
-
<div class="col-8">
|
|
161
|
-
<div class="form-check form-switch">
|
|
162
|
-
<input
|
|
163
|
-
class="form-check-input"
|
|
164
|
-
type="checkbox"
|
|
165
|
-
id="gridCappingToggle"
|
|
166
|
-
bind:checked={displayStore.useGridCapping}
|
|
167
|
-
title="Remove outliers using grid-based percentile capping"
|
|
168
|
-
/>
|
|
169
|
-
<label class="form-check-label small" for="gridCappingToggle">
|
|
170
|
-
Remove outliers
|
|
171
|
-
</label>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{#if displayStore.useGridCapping}
|
|
177
|
-
<!-- Grid Cap Percentile -->
|
|
178
|
-
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
179
|
-
<div class="col-4 text-secondary small">Cap Percentile</div>
|
|
180
|
-
<div class="col-3 text-end">
|
|
181
|
-
<span class="badge bg-white text-muted border">{displayStore.gridCapPercentile}th</span>
|
|
182
|
-
</div>
|
|
183
|
-
<div class="col-5">
|
|
184
|
-
<input
|
|
185
|
-
type="range"
|
|
186
|
-
class="form-range w-100"
|
|
187
|
-
min="70"
|
|
188
|
-
max="95"
|
|
189
|
-
step="5"
|
|
190
|
-
bind:value={displayStore.gridCapPercentile}
|
|
191
|
-
title="Percentile threshold for capping outliers within each grid"
|
|
192
|
-
/>
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
{/if}
|
|
196
117
|
{/if}
|
|
197
118
|
|
|
198
119
|
<div class="border-top my-3"></div>
|
|
@@ -102,13 +102,8 @@
|
|
|
102
102
|
|
|
103
103
|
// Events for updating
|
|
104
104
|
map.on('style.load', addLayers);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Auto-size uses fixed meter-based sizes that don't change with zoom
|
|
108
|
-
if (!displayStore.useAutoSize) {
|
|
109
|
-
map.on('moveend', updateLayer);
|
|
110
|
-
map.on('zoomend', updateLayer);
|
|
111
|
-
}
|
|
105
|
+
map.on('moveend', updateLayer);
|
|
106
|
+
map.on('zoomend', updateLayer);
|
|
112
107
|
|
|
113
108
|
// Cleanup
|
|
114
109
|
return () => {
|
|
@@ -134,11 +129,6 @@
|
|
|
134
129
|
const _layerGrouping = displayStore.layerGrouping;
|
|
135
130
|
const _useAutoSize = displayStore.useAutoSize;
|
|
136
131
|
const _autoSizeMode = displayStore.autoSizeMode;
|
|
137
|
-
const _autoSizeNeighborCount = displayStore.autoSizeNeighborCount;
|
|
138
|
-
const _autoSizeMultiplier = displayStore.autoSizeMultiplier;
|
|
139
|
-
const _useGridCapping = displayStore.useGridCapping;
|
|
140
|
-
const _gridCapPercentile = displayStore.gridCapPercentile;
|
|
141
|
-
const _gridSizeKm = displayStore.gridSizeKm;
|
|
142
132
|
|
|
143
133
|
updateLayer();
|
|
144
134
|
});
|
|
@@ -154,22 +144,20 @@
|
|
|
154
144
|
}
|
|
155
145
|
|
|
156
146
|
function renderCells(map: mapboxgl.Map) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const centerLat = useAutoSize ? 0 : map.getCenter().lat;
|
|
147
|
+
const bounds = map.getBounds();
|
|
148
|
+
if (!bounds) return;
|
|
149
|
+
|
|
150
|
+
const zoom = map.getZoom();
|
|
151
|
+
const centerLat = map.getCenter().lat;
|
|
163
152
|
|
|
164
|
-
console.log(`[CellsLayer] Rendering ${
|
|
153
|
+
console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
|
|
165
154
|
|
|
166
|
-
// 1. Calculate base radius
|
|
167
|
-
const baseRadiusMeters =
|
|
168
|
-
|
|
169
|
-
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px at zoom ${zoom.toFixed(2)}`);
|
|
170
|
-
}
|
|
155
|
+
// 1. Calculate base radius
|
|
156
|
+
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
157
|
+
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
171
158
|
|
|
172
|
-
// 2. Group cells
|
|
159
|
+
// 2. Group cells (Level 1=Tech, Level 2=Band for now hardcoded)
|
|
160
|
+
// In real app, this comes from a store
|
|
173
161
|
const groups = groupCells(dataStore.filteredCells, displayStore.level1, displayStore.level2);
|
|
174
162
|
console.log(`[CellsLayer] Groups: ${groups.size}`);
|
|
175
163
|
|
|
@@ -185,17 +173,13 @@
|
|
|
185
173
|
if (!style.visible) continue;
|
|
186
174
|
|
|
187
175
|
for (const cell of cells) {
|
|
188
|
-
// 4. BBox Filter (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (inView) {
|
|
176
|
+
// 4. BBox Filter (Simple point check)
|
|
177
|
+
if (bounds.contains([cell.longitude, cell.latitude])) {
|
|
192
178
|
// 5. Z-Index Lookup
|
|
193
179
|
const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
|
|
194
180
|
const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
|
|
195
181
|
|
|
196
|
-
// 6. Calculate radius
|
|
197
|
-
// Auto-size: Fixed meter-based size from site density
|
|
198
|
-
// Manual: Pixel-based size that scales with zoom
|
|
182
|
+
// 6. Calculate radius with z-index scaling
|
|
199
183
|
const MAX_Z = 35;
|
|
200
184
|
let radiusMeters: number;
|
|
201
185
|
|
|
@@ -204,13 +188,10 @@
|
|
|
204
188
|
const siteDistance = dataStore.siteDistanceStore.getDistance(cell.siteId, 500);
|
|
205
189
|
const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
|
|
206
190
|
|
|
207
|
-
// Apply user's multiplier to scale the result
|
|
208
|
-
const adjustedAutoRadius = autoRadius * displayStore.autoSizeMultiplier;
|
|
209
|
-
|
|
210
191
|
// Scale based on z-index for stacking visibility
|
|
211
192
|
// Lower z-index (background) = larger, higher z-index (foreground) = smaller
|
|
212
193
|
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08; // 8% per layer
|
|
213
|
-
radiusMeters =
|
|
194
|
+
radiusMeters = autoRadius * scaleFactor;
|
|
214
195
|
} else {
|
|
215
196
|
// Manual mode: base from pixel size, then scale by z-index
|
|
216
197
|
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
@@ -26,11 +26,9 @@ export declare function groupBySite(cells: Cell[]): Map<string, Cell[]>;
|
|
|
26
26
|
export declare function extractSiteLocations(cells: Cell[]): Map<string, [number, number]>;
|
|
27
27
|
/**
|
|
28
28
|
* Compute nearest neighbor distance for a single site
|
|
29
|
-
* Can average multiple neighbors for better density estimation
|
|
30
29
|
* @param siteId Site to compute distance for
|
|
31
30
|
* @param siteLocation Location of the site
|
|
32
31
|
* @param allSiteLocations Map of all site locations
|
|
33
|
-
* @
|
|
34
|
-
* @returns Average distance to N nearest neighbors in meters, or Infinity if no neighbors
|
|
32
|
+
* @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
|
|
35
33
|
*/
|
|
36
|
-
export declare function computeNearestNeighbor(siteId: string, siteLocation: [number, number], allSiteLocations: Map<string, [number, number]
|
|
34
|
+
export declare function computeNearestNeighbor(siteId: string, siteLocation: [number, number], allSiteLocations: Map<string, [number, number]>): number;
|
|
@@ -54,26 +54,18 @@ export function extractSiteLocations(cells) {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Compute nearest neighbor distance for a single site
|
|
57
|
-
* Can average multiple neighbors for better density estimation
|
|
58
57
|
* @param siteId Site to compute distance for
|
|
59
58
|
* @param siteLocation Location of the site
|
|
60
59
|
* @param allSiteLocations Map of all site locations
|
|
61
|
-
* @
|
|
62
|
-
* @returns Average distance to N nearest neighbors in meters, or Infinity if no neighbors
|
|
60
|
+
* @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
|
|
63
61
|
*/
|
|
64
|
-
export function computeNearestNeighbor(siteId, siteLocation, allSiteLocations
|
|
65
|
-
|
|
62
|
+
export function computeNearestNeighbor(siteId, siteLocation, allSiteLocations) {
|
|
63
|
+
let minDist = Infinity;
|
|
66
64
|
for (const [otherId, otherLoc] of allSiteLocations) {
|
|
67
65
|
if (siteId === otherId)
|
|
68
66
|
continue;
|
|
69
67
|
const dist = haversineDistance(siteLocation, otherLoc);
|
|
70
|
-
|
|
68
|
+
minDist = Math.min(minDist, dist);
|
|
71
69
|
}
|
|
72
|
-
|
|
73
|
-
return Infinity;
|
|
74
|
-
// Sort distances and take N closest
|
|
75
|
-
distances.sort((a, b) => a - b);
|
|
76
|
-
const closestN = distances.slice(0, Math.min(neighborCount, distances.length));
|
|
77
|
-
// Return average of closest neighbors
|
|
78
|
-
return closestN.reduce((sum, d) => sum + d, 0) / closestN.length;
|
|
70
|
+
return minDist;
|
|
79
71
|
}
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import type { Cell } from '../types';
|
|
2
2
|
import { SiteDistanceStore } from './site.distance.svelte';
|
|
3
|
-
import type { CellDisplayStore } from './cell.display.svelte';
|
|
4
3
|
export declare class CellDataStore {
|
|
5
4
|
rawCells: Cell[];
|
|
6
5
|
filterOnAir: boolean;
|
|
7
6
|
siteDistanceStore: SiteDistanceStore;
|
|
8
|
-
|
|
9
|
-
constructor(displayStore?: CellDisplayStore);
|
|
7
|
+
constructor();
|
|
10
8
|
setCells(cells: Cell[]): void;
|
|
11
9
|
get filteredCells(): Cell[];
|
|
12
10
|
}
|
|
13
|
-
|
|
14
|
-
* Factory function to create a new CellDataStore
|
|
15
|
-
* @param displayStore Optional display store for auto-size settings
|
|
16
|
-
*/
|
|
17
|
-
export declare function createCellDataStore(displayStore?: CellDisplayStore): CellDataStore;
|
|
11
|
+
export declare function createCellDataStore(): CellDataStore;
|
|
@@ -4,21 +4,13 @@ export class CellDataStore {
|
|
|
4
4
|
filterOnAir = $state(false);
|
|
5
5
|
// Internal site distance store for auto-sizing
|
|
6
6
|
siteDistanceStore;
|
|
7
|
-
|
|
8
|
-
displayStore;
|
|
9
|
-
constructor(displayStore) {
|
|
7
|
+
constructor() {
|
|
10
8
|
this.siteDistanceStore = new SiteDistanceStore();
|
|
11
|
-
this.displayStore = displayStore;
|
|
12
9
|
}
|
|
13
10
|
setCells(cells) {
|
|
14
11
|
this.rawCells = cells;
|
|
15
12
|
// Automatically update site distances when cells are loaded
|
|
16
|
-
|
|
17
|
-
const neighborCount = this.displayStore?.autoSizeNeighborCount ?? 5;
|
|
18
|
-
const useGridCapping = this.displayStore?.useGridCapping ?? true;
|
|
19
|
-
const gridCapPercentile = this.displayStore?.gridCapPercentile ?? 80;
|
|
20
|
-
const gridSizeKm = this.displayStore?.gridSizeKm ?? 1;
|
|
21
|
-
this.siteDistanceStore.updateDistances(cells, neighborCount, useGridCapping, gridCapPercentile, gridSizeKm);
|
|
13
|
+
this.siteDistanceStore.updateDistances(cells);
|
|
22
14
|
}
|
|
23
15
|
get filteredCells() {
|
|
24
16
|
if (!this.filterOnAir)
|
|
@@ -26,10 +18,6 @@ export class CellDataStore {
|
|
|
26
18
|
return this.rawCells.filter(c => c.status === 'On_Air');
|
|
27
19
|
}
|
|
28
20
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* @param displayStore Optional display store for auto-size settings
|
|
32
|
-
*/
|
|
33
|
-
export function createCellDataStore(displayStore) {
|
|
34
|
-
return new CellDataStore(displayStore);
|
|
21
|
+
export function createCellDataStore() {
|
|
22
|
+
return new CellDataStore();
|
|
35
23
|
}
|
|
@@ -9,11 +9,6 @@ export declare class CellDisplayStore {
|
|
|
9
9
|
layerGrouping: LayerGroupingPreset;
|
|
10
10
|
useAutoSize: boolean;
|
|
11
11
|
autoSizeMode: AutoSizeMode;
|
|
12
|
-
autoSizeNeighborCount: number;
|
|
13
|
-
autoSizeMultiplier: number;
|
|
14
|
-
useGridCapping: boolean;
|
|
15
|
-
gridCapPercentile: number;
|
|
16
|
-
gridSizeKm: number;
|
|
17
12
|
level1: CellGroupingField;
|
|
18
13
|
level2: CellGroupingField;
|
|
19
14
|
currentZIndex: Record<string, number>;
|
|
@@ -11,12 +11,6 @@ export class CellDisplayStore {
|
|
|
11
11
|
// Auto-size settings
|
|
12
12
|
useAutoSize = $state(false);
|
|
13
13
|
autoSizeMode = $state('logarithmic');
|
|
14
|
-
autoSizeNeighborCount = $state(5); // Number of neighbors to average for density
|
|
15
|
-
autoSizeMultiplier = $state(1.0); // Scale factor for auto-size results (0.3 - 2.0)
|
|
16
|
-
// Grid-based outlier capping
|
|
17
|
-
useGridCapping = $state(true); // Enable grid-based outlier removal
|
|
18
|
-
gridCapPercentile = $state(80); // Percentile for capping (70-95)
|
|
19
|
-
gridSizeKm = $state(1); // Grid size in kilometers
|
|
20
14
|
// Grouping
|
|
21
15
|
level1 = $state('tech');
|
|
22
16
|
level2 = $state('fband');
|
|
@@ -46,11 +40,6 @@ export class CellDisplayStore {
|
|
|
46
40
|
this.layerGrouping = parsed.layerGrouping ?? 'frequency';
|
|
47
41
|
this.useAutoSize = parsed.useAutoSize ?? false;
|
|
48
42
|
this.autoSizeMode = parsed.autoSizeMode ?? 'logarithmic';
|
|
49
|
-
this.autoSizeNeighborCount = parsed.autoSizeNeighborCount ?? 5;
|
|
50
|
-
this.autoSizeMultiplier = parsed.autoSizeMultiplier ?? 1.0;
|
|
51
|
-
this.useGridCapping = parsed.useGridCapping ?? true;
|
|
52
|
-
this.gridCapPercentile = parsed.gridCapPercentile ?? 80;
|
|
53
|
-
this.gridSizeKm = parsed.gridSizeKm ?? 1;
|
|
54
43
|
this.level1 = parsed.level1 ?? 'tech';
|
|
55
44
|
this.level2 = parsed.level2 ?? 'fband';
|
|
56
45
|
this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
|
|
@@ -75,11 +64,6 @@ export class CellDisplayStore {
|
|
|
75
64
|
layerGrouping: this.layerGrouping,
|
|
76
65
|
useAutoSize: this.useAutoSize,
|
|
77
66
|
autoSizeMode: this.autoSizeMode,
|
|
78
|
-
autoSizeNeighborCount: this.autoSizeNeighborCount,
|
|
79
|
-
autoSizeMultiplier: this.autoSizeMultiplier,
|
|
80
|
-
useGridCapping: this.useGridCapping,
|
|
81
|
-
gridCapPercentile: this.gridCapPercentile,
|
|
82
|
-
gridSizeKm: this.gridSizeKm,
|
|
83
67
|
level1: this.level1,
|
|
84
68
|
level2: this.level2,
|
|
85
69
|
labelPixelDistance: this.labelPixelDistance,
|
|
@@ -8,28 +8,17 @@ import type { Cell } from '../types';
|
|
|
8
8
|
export declare class SiteDistanceStore {
|
|
9
9
|
key: string;
|
|
10
10
|
distances: Map<string, number>;
|
|
11
|
-
cappedSizes: Map<string, number>;
|
|
12
11
|
computedSites: Set<string>;
|
|
13
12
|
lastComputeTime: number;
|
|
14
13
|
computedCount: number;
|
|
15
14
|
constructor();
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* @param cells Array of cells to compute distances for
|
|
20
|
-
* @param neighborCount Number of neighbors to average for density calculation
|
|
21
|
-
* @param useGridCapping Whether to apply grid-based outlier capping
|
|
22
|
-
* @param gridCapPercentile Percentile for grid capping (70-95)
|
|
23
|
-
* @param gridSizeKm Grid size in kilometers
|
|
16
|
+
* Update distances for all sites in current dataset
|
|
17
|
+
* Always recomputes to ensure accuracy
|
|
24
18
|
*/
|
|
25
|
-
updateDistances(cells: Cell[]
|
|
26
|
-
/**
|
|
27
|
-
* Update existing sites if needed (when no new sites but data might have changed)
|
|
28
|
-
*/
|
|
29
|
-
private updateExistingSitesIfNeeded;
|
|
19
|
+
updateDistances(cells: Cell[]): void;
|
|
30
20
|
/**
|
|
31
21
|
* Get distance for a site, with fallback
|
|
32
|
-
* Returns capped size if grid capping was applied, otherwise original
|
|
33
22
|
*/
|
|
34
23
|
getDistance(siteId: string, fallback?: number): number;
|
|
35
24
|
/**
|
|
@@ -6,13 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { browser } from '$app/environment';
|
|
8
8
|
import { extractSiteLocations, computeNearestNeighbor, haversineDistance } from '../logic/site-distance';
|
|
9
|
-
import { computeGridBasedSizeCaps, getGridCappingStats } from '../logic/grid-density';
|
|
10
9
|
export class SiteDistanceStore {
|
|
11
10
|
key = 'map-v3-site-distances';
|
|
12
|
-
// Cached nearest neighbor distances
|
|
11
|
+
// Cached nearest neighbor distances
|
|
13
12
|
distances = $state(new Map());
|
|
14
|
-
// Grid-capped sizes (after outlier removal)
|
|
15
|
-
cappedSizes = $state(new Map());
|
|
16
13
|
// Track which sites have been computed
|
|
17
14
|
computedSites = $state(new Set());
|
|
18
15
|
// Performance tracking
|
|
@@ -24,91 +21,41 @@ export class SiteDistanceStore {
|
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
23
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @param cells Array of cells to compute distances for
|
|
30
|
-
* @param neighborCount Number of neighbors to average for density calculation
|
|
31
|
-
* @param useGridCapping Whether to apply grid-based outlier capping
|
|
32
|
-
* @param gridCapPercentile Percentile for grid capping (70-95)
|
|
33
|
-
* @param gridSizeKm Grid size in kilometers
|
|
24
|
+
* Update distances for all sites in current dataset
|
|
25
|
+
* Always recomputes to ensure accuracy
|
|
34
26
|
*/
|
|
35
|
-
updateDistances(cells
|
|
27
|
+
updateDistances(cells) {
|
|
36
28
|
if (!cells || cells.length === 0)
|
|
37
29
|
return;
|
|
38
30
|
const startTime = performance.now();
|
|
39
31
|
const siteLocations = extractSiteLocations(cells);
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
console.log(`[SiteDistance] Computing distances for ${newSites.length} new sites (${neighborCount} neighbors)`);
|
|
49
|
-
// Compute distances for NEW sites
|
|
50
|
-
for (const siteId of newSites) {
|
|
51
|
-
const location = siteLocations.get(siteId);
|
|
52
|
-
const nearestDist = computeNearestNeighbor(siteId, location, siteLocations, neighborCount);
|
|
32
|
+
// Clear existing data and recompute all sites
|
|
33
|
+
this.distances.clear();
|
|
34
|
+
this.computedSites.clear();
|
|
35
|
+
console.log(`[SiteDistance] Computing distances for ${siteLocations.size} sites`);
|
|
36
|
+
// Compute distances for ALL sites
|
|
37
|
+
for (const [siteId, location] of siteLocations) {
|
|
38
|
+
const nearestDist = computeNearestNeighbor(siteId, location, siteLocations);
|
|
53
39
|
this.distances.set(siteId, nearestDist);
|
|
54
40
|
this.computedSites.add(siteId);
|
|
55
41
|
}
|
|
56
|
-
// Update EXISTING sites if a new site is closer
|
|
57
|
-
for (const [existingId, existingLoc] of siteLocations) {
|
|
58
|
-
if (newSites.includes(existingId))
|
|
59
|
-
continue; // Skip new sites
|
|
60
|
-
// Recalculate with new sites present
|
|
61
|
-
const updatedDist = computeNearestNeighbor(existingId, existingLoc, siteLocations, neighborCount);
|
|
62
|
-
this.distances.set(existingId, updatedDist);
|
|
63
|
-
}
|
|
64
|
-
// Apply grid-based capping if enabled
|
|
65
|
-
if (useGridCapping) {
|
|
66
|
-
this.cappedSizes = computeGridBasedSizeCaps(cells, this.distances, gridCapPercentile, gridSizeKm);
|
|
67
|
-
const stats = getGridCappingStats(this.distances, this.cappedSizes);
|
|
68
|
-
console.log(`[SiteDistance] Grid capping: ${stats.cappedSites}/${stats.totalSites} sites capped (${stats.cappedPercentage.toFixed(1)}%), avg reduction: ${stats.avgReduction.toFixed(1)}m`);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
// No capping - capped sizes = original sizes
|
|
72
|
-
this.cappedSizes = new Map(this.distances);
|
|
73
|
-
}
|
|
74
42
|
const endTime = performance.now();
|
|
75
43
|
this.lastComputeTime = endTime - startTime;
|
|
76
44
|
this.computedCount = this.distances.size;
|
|
77
|
-
console.log(`[SiteDistance] Computed ${
|
|
45
|
+
console.log(`[SiteDistance] Computed ${this.computedCount} sites in ${this.lastComputeTime.toFixed(1)}ms`);
|
|
78
46
|
this.persist();
|
|
79
47
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Update existing sites if needed (when no new sites but data might have changed)
|
|
82
|
-
*/
|
|
83
|
-
updateExistingSitesIfNeeded(siteLocations) {
|
|
84
|
-
// Check if all computed sites still exist in current data
|
|
85
|
-
const currentSiteIds = new Set(siteLocations.keys());
|
|
86
|
-
let removed = 0;
|
|
87
|
-
for (const computedId of this.computedSites) {
|
|
88
|
-
if (!currentSiteIds.has(computedId)) {
|
|
89
|
-
this.distances.delete(computedId);
|
|
90
|
-
this.computedSites.delete(computedId);
|
|
91
|
-
removed++;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (removed > 0) {
|
|
95
|
-
console.log(`[SiteDistance] Removed ${removed} stale sites from cache`);
|
|
96
|
-
this.persist();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
48
|
/**
|
|
100
49
|
* Get distance for a site, with fallback
|
|
101
|
-
* Returns capped size if grid capping was applied, otherwise original
|
|
102
50
|
*/
|
|
103
51
|
getDistance(siteId, fallback = 500) {
|
|
104
|
-
return this.
|
|
52
|
+
return this.distances.get(siteId) ?? fallback;
|
|
105
53
|
}
|
|
106
54
|
/**
|
|
107
55
|
* Clear all cached distances (useful for testing or data refresh)
|
|
108
56
|
*/
|
|
109
57
|
clear() {
|
|
110
58
|
this.distances.clear();
|
|
111
|
-
this.cappedSizes.clear();
|
|
112
59
|
this.computedSites.clear();
|
|
113
60
|
this.lastComputeTime = 0;
|
|
114
61
|
this.computedCount = 0;
|
|
@@ -10,6 +10,8 @@ export class SiteDataStore {
|
|
|
10
10
|
if (!cell.siteId)
|
|
11
11
|
continue;
|
|
12
12
|
if (!siteMap.has(cell.siteId)) {
|
|
13
|
+
// Get distance to nearest site from cell data store
|
|
14
|
+
const nearestDistance = this.cellDataStore.siteDistanceStore.getDistance(cell.siteId, Infinity);
|
|
13
15
|
siteMap.set(cell.siteId, {
|
|
14
16
|
siteId: cell.siteId,
|
|
15
17
|
siteName: cell.cellName, // Fallback to cell name if site name not available
|
|
@@ -20,7 +22,8 @@ export class SiteDataStore {
|
|
|
20
22
|
provider: 'Cetin',
|
|
21
23
|
level1: 'Cetin', // Need to check where provider comes from in V3,
|
|
22
24
|
level2: 'Unknown',
|
|
23
|
-
cellCount: 1
|
|
25
|
+
cellCount: 1,
|
|
26
|
+
nearestSiteDistance: nearestDistance !== Infinity ? Math.round(nearestDistance) : undefined
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
else {
|
package/package.json
CHANGED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Grid-Based Density Calculation
|
|
3
|
-
*
|
|
4
|
-
* Divides map into fixed-size grids and computes density-based size caps
|
|
5
|
-
* Used to remove outliers and create visual uniformity within local areas
|
|
6
|
-
*/
|
|
7
|
-
import type { Cell } from '../types';
|
|
8
|
-
/**
|
|
9
|
-
* Convert geographic coordinates to grid key
|
|
10
|
-
* @param lon Longitude
|
|
11
|
-
* @param lat Latitude
|
|
12
|
-
* @param gridSizeKm Size of grid cell in kilometers (default: 1km)
|
|
13
|
-
* @returns Grid key string "x_y"
|
|
14
|
-
*/
|
|
15
|
-
export declare function getGridKey(lon: number, lat: number, gridSizeKm?: number): string;
|
|
16
|
-
/**
|
|
17
|
-
* Group cells by grid
|
|
18
|
-
* @param cells Array of cells
|
|
19
|
-
* @param gridSizeKm Grid size in kilometers
|
|
20
|
-
* @returns Map of gridKey -> cells in that grid
|
|
21
|
-
*/
|
|
22
|
-
export declare function groupCellsByGrid(cells: Cell[], gridSizeKm?: number): Map<string, Cell[]>;
|
|
23
|
-
/**
|
|
24
|
-
* Calculate percentile value from array
|
|
25
|
-
* @param values Sorted array of numbers
|
|
26
|
-
* @param percentile Percentile to calculate (0-100, e.g., 80 for 80th percentile)
|
|
27
|
-
* @returns Value at the given percentile
|
|
28
|
-
*/
|
|
29
|
-
export declare function calculatePercentile(values: number[], percentile: number): number;
|
|
30
|
-
/**
|
|
31
|
-
* Compute grid-based size caps using percentile
|
|
32
|
-
* @param cells Array of cells
|
|
33
|
-
* @param sizeMap Map of siteId -> calculated size
|
|
34
|
-
* @param percentile Percentile threshold (0-100, e.g., 80)
|
|
35
|
-
* @param gridSizeKm Grid size in kilometers
|
|
36
|
-
* @param minCellsForCapping Minimum cells in grid to apply capping (default: 3)
|
|
37
|
-
* @returns Map of siteId -> capped size
|
|
38
|
-
*/
|
|
39
|
-
export declare function computeGridBasedSizeCaps(cells: Cell[], sizeMap: Map<string, number>, percentile: number, gridSizeKm?: number, minCellsForCapping?: number): Map<string, number>;
|
|
40
|
-
/**
|
|
41
|
-
* Get statistics about grid-based capping
|
|
42
|
-
* @param originalSizes Map of original sizes
|
|
43
|
-
* @param cappedSizes Map of capped sizes
|
|
44
|
-
* @returns Object with statistics
|
|
45
|
-
*/
|
|
46
|
-
export declare function getGridCappingStats(originalSizes: Map<string, number>, cappedSizes: Map<string, number>): {
|
|
47
|
-
totalSites: number;
|
|
48
|
-
cappedSites: number;
|
|
49
|
-
cappedPercentage: number;
|
|
50
|
-
avgReduction: number;
|
|
51
|
-
};
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Grid-Based Density Calculation
|
|
3
|
-
*
|
|
4
|
-
* Divides map into fixed-size grids and computes density-based size caps
|
|
5
|
-
* Used to remove outliers and create visual uniformity within local areas
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Convert geographic coordinates to grid key
|
|
9
|
-
* @param lon Longitude
|
|
10
|
-
* @param lat Latitude
|
|
11
|
-
* @param gridSizeKm Size of grid cell in kilometers (default: 1km)
|
|
12
|
-
* @returns Grid key string "x_y"
|
|
13
|
-
*/
|
|
14
|
-
export function getGridKey(lon, lat, gridSizeKm = 1) {
|
|
15
|
-
// Approximate: 1 degree ≈ 111km at equator
|
|
16
|
-
// This is rough but sufficient for grouping
|
|
17
|
-
const degreesPerGrid = gridSizeKm / 111;
|
|
18
|
-
const gridX = Math.floor(lon / degreesPerGrid);
|
|
19
|
-
const gridY = Math.floor(lat / degreesPerGrid);
|
|
20
|
-
return `${gridX}_${gridY}`;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Group cells by grid
|
|
24
|
-
* @param cells Array of cells
|
|
25
|
-
* @param gridSizeKm Grid size in kilometers
|
|
26
|
-
* @returns Map of gridKey -> cells in that grid
|
|
27
|
-
*/
|
|
28
|
-
export function groupCellsByGrid(cells, gridSizeKm = 1) {
|
|
29
|
-
const grids = new Map();
|
|
30
|
-
for (const cell of cells) {
|
|
31
|
-
const gridKey = getGridKey(cell.longitude, cell.latitude, gridSizeKm);
|
|
32
|
-
if (!grids.has(gridKey)) {
|
|
33
|
-
grids.set(gridKey, []);
|
|
34
|
-
}
|
|
35
|
-
grids.get(gridKey).push(cell);
|
|
36
|
-
}
|
|
37
|
-
return grids;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Calculate percentile value from array
|
|
41
|
-
* @param values Sorted array of numbers
|
|
42
|
-
* @param percentile Percentile to calculate (0-100, e.g., 80 for 80th percentile)
|
|
43
|
-
* @returns Value at the given percentile
|
|
44
|
-
*/
|
|
45
|
-
export function calculatePercentile(values, percentile) {
|
|
46
|
-
if (values.length === 0)
|
|
47
|
-
return Infinity;
|
|
48
|
-
if (values.length === 1)
|
|
49
|
-
return values[0];
|
|
50
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
51
|
-
const index = (percentile / 100) * (sorted.length - 1);
|
|
52
|
-
const lower = Math.floor(index);
|
|
53
|
-
const upper = Math.ceil(index);
|
|
54
|
-
const weight = index % 1;
|
|
55
|
-
if (lower === upper) {
|
|
56
|
-
return sorted[lower];
|
|
57
|
-
}
|
|
58
|
-
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Compute grid-based size caps using percentile
|
|
62
|
-
* @param cells Array of cells
|
|
63
|
-
* @param sizeMap Map of siteId -> calculated size
|
|
64
|
-
* @param percentile Percentile threshold (0-100, e.g., 80)
|
|
65
|
-
* @param gridSizeKm Grid size in kilometers
|
|
66
|
-
* @param minCellsForCapping Minimum cells in grid to apply capping (default: 3)
|
|
67
|
-
* @returns Map of siteId -> capped size
|
|
68
|
-
*/
|
|
69
|
-
export function computeGridBasedSizeCaps(cells, sizeMap, percentile, gridSizeKm = 1, minCellsForCapping = 3) {
|
|
70
|
-
// Group cells by grid
|
|
71
|
-
const grids = groupCellsByGrid(cells, gridSizeKm);
|
|
72
|
-
// Calculate percentile cap for each grid
|
|
73
|
-
const gridCaps = new Map();
|
|
74
|
-
for (const [gridKey, gridCells] of grids) {
|
|
75
|
-
// Get sizes for all sites in this grid
|
|
76
|
-
const siteSizes = new Map();
|
|
77
|
-
for (const cell of gridCells) {
|
|
78
|
-
const size = sizeMap.get(cell.siteId);
|
|
79
|
-
if (size !== undefined) {
|
|
80
|
-
siteSizes.set(cell.siteId, size);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const uniqueSizes = Array.from(siteSizes.values());
|
|
84
|
-
// Only apply capping if grid has enough cells
|
|
85
|
-
if (uniqueSizes.length >= minCellsForCapping) {
|
|
86
|
-
const cap = calculatePercentile(uniqueSizes, percentile);
|
|
87
|
-
gridCaps.set(gridKey, cap);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
// For small grids, use max size (no capping)
|
|
91
|
-
gridCaps.set(gridKey, Math.max(...uniqueSizes, 0));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Apply caps to each site
|
|
95
|
-
const cappedSizes = new Map();
|
|
96
|
-
const sitesProcessed = new Set();
|
|
97
|
-
for (const cell of cells) {
|
|
98
|
-
// Only process each site once
|
|
99
|
-
if (sitesProcessed.has(cell.siteId))
|
|
100
|
-
continue;
|
|
101
|
-
sitesProcessed.add(cell.siteId);
|
|
102
|
-
const originalSize = sizeMap.get(cell.siteId);
|
|
103
|
-
if (originalSize === undefined)
|
|
104
|
-
continue;
|
|
105
|
-
const gridKey = getGridKey(cell.longitude, cell.latitude, gridSizeKm);
|
|
106
|
-
const cap = gridCaps.get(gridKey) ?? originalSize;
|
|
107
|
-
// Apply cap (take minimum)
|
|
108
|
-
cappedSizes.set(cell.siteId, Math.min(originalSize, cap));
|
|
109
|
-
}
|
|
110
|
-
return cappedSizes;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get statistics about grid-based capping
|
|
114
|
-
* @param originalSizes Map of original sizes
|
|
115
|
-
* @param cappedSizes Map of capped sizes
|
|
116
|
-
* @returns Object with statistics
|
|
117
|
-
*/
|
|
118
|
-
export function getGridCappingStats(originalSizes, cappedSizes) {
|
|
119
|
-
let totalSites = 0;
|
|
120
|
-
let cappedSites = 0;
|
|
121
|
-
let totalReduction = 0;
|
|
122
|
-
for (const [siteId, originalSize] of originalSizes) {
|
|
123
|
-
const cappedSize = cappedSizes.get(siteId);
|
|
124
|
-
if (cappedSize === undefined)
|
|
125
|
-
continue;
|
|
126
|
-
totalSites++;
|
|
127
|
-
if (cappedSize < originalSize) {
|
|
128
|
-
cappedSites++;
|
|
129
|
-
totalReduction += originalSize - cappedSize;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
totalSites,
|
|
134
|
-
cappedSites,
|
|
135
|
-
cappedPercentage: totalSites > 0 ? (cappedSites / totalSites) * 100 : 0,
|
|
136
|
-
avgReduction: cappedSites > 0 ? totalReduction / cappedSites : 0
|
|
137
|
-
};
|
|
138
|
-
}
|