@smartnet360/svelte-components 0.0.94 → 0.0.96
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 +8 -67
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +51 -57
- package/dist/map-v3/features/cells/logic/geometry.js +23 -0
- 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 +1 -5
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +3 -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/cells/types.d.ts +1 -1
- 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>
|
|
@@ -112,87 +111,29 @@
|
|
|
112
111
|
<option value="logarithmic">Logarithmic (smooth)</option>
|
|
113
112
|
<option value="percentage">Proportional (40%)</option>
|
|
114
113
|
<option value="tiered">Tiered (4 levels)</option>
|
|
114
|
+
<option value="hybrid">Hybrid (stepped proportional)</option>
|
|
115
115
|
</select>
|
|
116
116
|
</div>
|
|
117
117
|
</div>
|
|
118
|
-
|
|
119
|
-
<!--
|
|
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 -->
|
|
118
|
+
|
|
119
|
+
<!-- Auto Size Base Multiplier -->
|
|
139
120
|
<div class="row align-items-center g-2 mb-3 ps-3">
|
|
140
|
-
<div class="col-4 text-secondary small">Size
|
|
121
|
+
<div class="col-4 text-secondary small">Base Size</div>
|
|
141
122
|
<div class="col-3 text-end">
|
|
142
|
-
<span class="badge bg-white text-muted border">{displayStore.
|
|
123
|
+
<span class="badge bg-white text-muted border">{displayStore.autoSizeBase.toFixed(1)}x</span>
|
|
143
124
|
</div>
|
|
144
125
|
<div class="col-5">
|
|
145
126
|
<input
|
|
127
|
+
id="cell-autosize-base-slider"
|
|
146
128
|
type="range"
|
|
147
129
|
class="form-range w-100"
|
|
148
130
|
min="0.3"
|
|
149
131
|
max="2.0"
|
|
150
132
|
step="0.1"
|
|
151
|
-
bind:value={displayStore.
|
|
152
|
-
title="Scale all auto-sized cells up or down"
|
|
133
|
+
bind:value={displayStore.autoSizeBase}
|
|
153
134
|
/>
|
|
154
135
|
</div>
|
|
155
136
|
</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
137
|
{/if}
|
|
197
138
|
|
|
198
139
|
<div class="border-top my-3"></div>
|
|
@@ -100,11 +100,10 @@
|
|
|
100
100
|
// Initial setup
|
|
101
101
|
addLayers();
|
|
102
102
|
|
|
103
|
-
// Events for updating
|
|
103
|
+
// Events for updating - conditional based on auto-size
|
|
104
104
|
map.on('style.load', addLayers);
|
|
105
105
|
|
|
106
|
-
// Only listen to
|
|
107
|
-
// Auto-size uses fixed meter-based sizes that don't change with zoom
|
|
106
|
+
// Only listen to zoom/move events if NOT using auto-size
|
|
108
107
|
if (!displayStore.useAutoSize) {
|
|
109
108
|
map.on('moveend', updateLayer);
|
|
110
109
|
map.on('zoomend', updateLayer);
|
|
@@ -134,11 +133,7 @@
|
|
|
134
133
|
const _layerGrouping = displayStore.layerGrouping;
|
|
135
134
|
const _useAutoSize = displayStore.useAutoSize;
|
|
136
135
|
const _autoSizeMode = displayStore.autoSizeMode;
|
|
137
|
-
const
|
|
138
|
-
const _autoSizeMultiplier = displayStore.autoSizeMultiplier;
|
|
139
|
-
const _useGridCapping = displayStore.useGridCapping;
|
|
140
|
-
const _gridCapPercentile = displayStore.gridCapPercentile;
|
|
141
|
-
const _gridSizeKm = displayStore.gridSizeKm;
|
|
136
|
+
const _autoSizeBase = displayStore.autoSizeBase;
|
|
142
137
|
|
|
143
138
|
updateLayer();
|
|
144
139
|
});
|
|
@@ -154,22 +149,20 @@
|
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
function renderCells(map: mapboxgl.Map) {
|
|
157
|
-
const
|
|
152
|
+
const zoom = map.getZoom();
|
|
153
|
+
const centerLat = map.getCenter().lat;
|
|
158
154
|
|
|
159
|
-
|
|
160
|
-
const bounds = useAutoSize ? null : map.getBounds();
|
|
161
|
-
const zoom = useAutoSize ? 0 : map.getZoom();
|
|
162
|
-
const centerLat = useAutoSize ? 0 : map.getCenter().lat;
|
|
163
|
-
|
|
164
|
-
console.log(`[CellsLayer] Rendering ${dataStore.filteredCells.length} cells (auto-size: ${useAutoSize})`);
|
|
155
|
+
console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
|
|
165
156
|
|
|
166
|
-
// 1. Calculate base radius (only
|
|
167
|
-
const baseRadiusMeters =
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
// 1. Calculate base radius (only used in manual mode)
|
|
158
|
+
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
159
|
+
|
|
160
|
+
if (!displayStore.useAutoSize) {
|
|
161
|
+
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
|
|
170
162
|
}
|
|
171
163
|
|
|
172
|
-
// 2. Group cells
|
|
164
|
+
// 2. Group cells (Level 1=Tech, Level 2=Band for now hardcoded)
|
|
165
|
+
// In real app, this comes from a store
|
|
173
166
|
const groups = groupCells(dataStore.filteredCells, displayStore.level1, displayStore.level2);
|
|
174
167
|
console.log(`[CellsLayer] Groups: ${groups.size}`);
|
|
175
168
|
|
|
@@ -185,50 +178,51 @@
|
|
|
185
178
|
if (!style.visible) continue;
|
|
186
179
|
|
|
187
180
|
for (const cell of cells) {
|
|
188
|
-
// 4. BBox Filter
|
|
189
|
-
|
|
181
|
+
// 4. BBox Filter - SKIP if auto-size is enabled
|
|
182
|
+
if (!displayStore.useAutoSize) {
|
|
183
|
+
const bounds = map.getBounds();
|
|
184
|
+
if (!bounds || !bounds.contains([cell.longitude, cell.latitude])) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
189
|
+
// 5. Z-Index Lookup
|
|
190
|
+
const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
|
|
191
|
+
const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
|
|
192
|
+
|
|
193
|
+
// 6. Calculate radius with z-index scaling
|
|
194
|
+
const MAX_Z = 35;
|
|
195
|
+
let radiusMeters: number;
|
|
196
|
+
|
|
197
|
+
if (displayStore.useAutoSize) {
|
|
198
|
+
// Auto-size mode: get target radius for this site
|
|
199
|
+
const siteDistance = dataStore.siteDistanceStore.getDistance(cell.siteId, 500);
|
|
200
|
+
const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} else {
|
|
215
|
-
// Manual mode: base from pixel size, then scale by z-index
|
|
216
|
-
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
217
|
-
radiusMeters = baseRadiusMeters * scaleFactor;
|
|
218
|
-
}
|
|
202
|
+
// Apply base size multiplier
|
|
203
|
+
const baseAdjusted = autoRadius * displayStore.autoSizeBase;
|
|
204
|
+
|
|
205
|
+
// Scale based on z-index for stacking visibility
|
|
206
|
+
// Lower z-index (background) = larger, higher z-index (foreground) = smaller
|
|
207
|
+
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08; // 8% per layer
|
|
208
|
+
radiusMeters = baseAdjusted * scaleFactor;
|
|
209
|
+
} else {
|
|
210
|
+
// Manual mode: base from pixel size, then scale by z-index
|
|
211
|
+
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
212
|
+
radiusMeters = baseRadiusMeters * scaleFactor;
|
|
213
|
+
}
|
|
219
214
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
// 7. Apply beamwidth boost from displayStore preset
|
|
216
|
+
const beamwidthBoost = displayStore.currentBeamwidthBoost[zIndexKey] || 0;
|
|
217
|
+
const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
|
|
223
218
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
219
|
+
// 8. Generate Arc
|
|
220
|
+
const feature = generateCellArc(cell, radiusMeters, zIndex, style.color, adjustedBeamwidth);
|
|
221
|
+
features.push(feature);
|
|
228
222
|
}
|
|
229
223
|
}
|
|
230
224
|
|
|
231
|
-
console.log(`[CellsLayer] Generated ${features.length} features in view`);
|
|
225
|
+
console.log(`[CellsLayer] Generated ${features.length} features ${displayStore.useAutoSize ? '(all cells)' : 'in view'}`);
|
|
232
226
|
|
|
233
227
|
// 8. Update Source
|
|
234
228
|
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
@@ -38,6 +38,29 @@ export function calculateAutoRadius(nearestSiteDistance, mode = 'logarithmic') {
|
|
|
38
38
|
return 120;
|
|
39
39
|
return 180;
|
|
40
40
|
}
|
|
41
|
+
case 'hybrid': {
|
|
42
|
+
// Stepped proportional - tiers with proportional scaling within each tier
|
|
43
|
+
if (nearestSiteDistance < 300) {
|
|
44
|
+
// Tier 1: 0-300m → 30-50m radius
|
|
45
|
+
const ratio = nearestSiteDistance / 300;
|
|
46
|
+
return 30 + (ratio * 20);
|
|
47
|
+
}
|
|
48
|
+
else if (nearestSiteDistance < 600) {
|
|
49
|
+
// Tier 2: 300-600m → 50-80m radius
|
|
50
|
+
const ratio = (nearestSiteDistance - 300) / 300;
|
|
51
|
+
return 50 + (ratio * 30);
|
|
52
|
+
}
|
|
53
|
+
else if (nearestSiteDistance < 1200) {
|
|
54
|
+
// Tier 3: 600-1200m → 80-120m radius
|
|
55
|
+
const ratio = (nearestSiteDistance - 600) / 600;
|
|
56
|
+
return 80 + (ratio * 40);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Tier 4: 1200m+ → 120-180m radius
|
|
60
|
+
const ratio = Math.min((nearestSiteDistance - 1200) / 1200, 1);
|
|
61
|
+
return 120 + (ratio * 60);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
41
64
|
default:
|
|
42
65
|
return 80;
|
|
43
66
|
}
|
|
@@ -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,7 @@ export declare class CellDisplayStore {
|
|
|
9
9
|
layerGrouping: LayerGroupingPreset;
|
|
10
10
|
useAutoSize: boolean;
|
|
11
11
|
autoSizeMode: AutoSizeMode;
|
|
12
|
-
|
|
13
|
-
autoSizeMultiplier: number;
|
|
14
|
-
useGridCapping: boolean;
|
|
15
|
-
gridCapPercentile: number;
|
|
16
|
-
gridSizeKm: number;
|
|
12
|
+
autoSizeBase: number;
|
|
17
13
|
level1: CellGroupingField;
|
|
18
14
|
level2: CellGroupingField;
|
|
19
15
|
currentZIndex: Record<string, number>;
|
|
@@ -11,12 +11,7 @@ export class CellDisplayStore {
|
|
|
11
11
|
// Auto-size settings
|
|
12
12
|
useAutoSize = $state(false);
|
|
13
13
|
autoSizeMode = $state('logarithmic');
|
|
14
|
-
|
|
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
|
|
14
|
+
autoSizeBase = $state(1.0);
|
|
20
15
|
// Grouping
|
|
21
16
|
level1 = $state('tech');
|
|
22
17
|
level2 = $state('fband');
|
|
@@ -46,11 +41,7 @@ export class CellDisplayStore {
|
|
|
46
41
|
this.layerGrouping = parsed.layerGrouping ?? 'frequency';
|
|
47
42
|
this.useAutoSize = parsed.useAutoSize ?? false;
|
|
48
43
|
this.autoSizeMode = parsed.autoSizeMode ?? 'logarithmic';
|
|
49
|
-
this.
|
|
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;
|
|
44
|
+
this.autoSizeBase = parsed.autoSizeBase ?? 1.0;
|
|
54
45
|
this.level1 = parsed.level1 ?? 'tech';
|
|
55
46
|
this.level2 = parsed.level2 ?? 'fband';
|
|
56
47
|
this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
|
|
@@ -75,11 +66,7 @@ export class CellDisplayStore {
|
|
|
75
66
|
layerGrouping: this.layerGrouping,
|
|
76
67
|
useAutoSize: this.useAutoSize,
|
|
77
68
|
autoSizeMode: this.autoSizeMode,
|
|
78
|
-
|
|
79
|
-
autoSizeMultiplier: this.autoSizeMultiplier,
|
|
80
|
-
useGridCapping: this.useGridCapping,
|
|
81
|
-
gridCapPercentile: this.gridCapPercentile,
|
|
82
|
-
gridSizeKm: this.gridSizeKm,
|
|
69
|
+
autoSizeBase: this.autoSizeBase,
|
|
83
70
|
level1: this.level1,
|
|
84
71
|
level2: this.level2,
|
|
85
72
|
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;
|
|
@@ -63,7 +63,7 @@ export type CellGroupingField = 'tech' | 'fband' | 'frq' | 'status' | 'siteId' |
|
|
|
63
63
|
/**
|
|
64
64
|
* Auto-size calculation modes
|
|
65
65
|
*/
|
|
66
|
-
export type AutoSizeMode = 'logarithmic' | 'percentage' | 'tiered';
|
|
66
|
+
export type AutoSizeMode = 'logarithmic' | 'percentage' | 'tiered' | 'hybrid';
|
|
67
67
|
/**
|
|
68
68
|
* Site distance data for auto-sizing
|
|
69
69
|
*/
|
|
@@ -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
|
-
}
|