@smartnet360/svelte-components 0.0.92 → 0.0.94
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/features/cells/components/CellSettingsPanel.svelte +78 -0
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +36 -17
- package/dist/map-v3/features/cells/logic/grid-density.d.ts +51 -0
- package/dist/map-v3/features/cells/logic/grid-density.js +138 -0
- package/dist/map-v3/features/cells/logic/site-distance.d.ts +4 -2
- package/dist/map-v3/features/cells/logic/site-distance.js +13 -5
- package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +8 -2
- package/dist/map-v3/features/cells/stores/cell.data.svelte.js +16 -4
- package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +5 -0
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +16 -0
- package/dist/map-v3/features/cells/stores/site.distance.svelte.d.ts +8 -1
- package/dist/map-v3/features/cells/stores/site.distance.svelte.js +28 -14
- package/package.json +1 -1
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
let { accessToken }: Props = $props();
|
|
31
31
|
|
|
32
32
|
// Initialize stores
|
|
33
|
-
const cellData = createCellDataStore();
|
|
34
33
|
const cellRegistry = createCellRegistry('demo-map');
|
|
35
34
|
const cellDisplay = new CellDisplayStore();
|
|
35
|
+
const cellData = createCellDataStore(cellDisplay);
|
|
36
36
|
|
|
37
37
|
const siteData = createSiteDataStore(cellData);
|
|
38
38
|
const siteRegistry = createSiteRegistry('demo-map');
|
|
@@ -115,6 +115,84 @@
|
|
|
115
115
|
</select>
|
|
116
116
|
</div>
|
|
117
117
|
</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}
|
|
118
196
|
{/if}
|
|
119
197
|
|
|
120
198
|
<div class="border-top my-3"></div>
|
|
@@ -102,8 +102,13 @@
|
|
|
102
102
|
|
|
103
103
|
// Events for updating
|
|
104
104
|
map.on('style.load', addLayers);
|
|
105
|
-
|
|
106
|
-
map
|
|
105
|
+
|
|
106
|
+
// Only listen to map events when NOT using auto-size
|
|
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
|
+
}
|
|
107
112
|
|
|
108
113
|
// Cleanup
|
|
109
114
|
return () => {
|
|
@@ -129,6 +134,11 @@
|
|
|
129
134
|
const _layerGrouping = displayStore.layerGrouping;
|
|
130
135
|
const _useAutoSize = displayStore.useAutoSize;
|
|
131
136
|
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;
|
|
132
142
|
|
|
133
143
|
updateLayer();
|
|
134
144
|
});
|
|
@@ -144,20 +154,22 @@
|
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
function renderCells(map: mapboxgl.Map) {
|
|
147
|
-
const
|
|
148
|
-
if (!bounds) return;
|
|
149
|
-
|
|
150
|
-
const zoom = map.getZoom();
|
|
151
|
-
const centerLat = map.getCenter().lat;
|
|
157
|
+
const useAutoSize = displayStore.useAutoSize;
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
// Only need bounds checking and zoom calculations for manual mode
|
|
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})`);
|
|
154
165
|
|
|
155
|
-
// 1. Calculate base radius
|
|
156
|
-
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
157
|
-
|
|
166
|
+
// 1. Calculate base radius (only for manual mode)
|
|
167
|
+
const baseRadiusMeters = useAutoSize ? 0 : calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
|
|
168
|
+
if (!useAutoSize) {
|
|
169
|
+
console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px at zoom ${zoom.toFixed(2)}`);
|
|
170
|
+
}
|
|
158
171
|
|
|
159
|
-
// 2. Group cells
|
|
160
|
-
// In real app, this comes from a store
|
|
172
|
+
// 2. Group cells
|
|
161
173
|
const groups = groupCells(dataStore.filteredCells, displayStore.level1, displayStore.level2);
|
|
162
174
|
console.log(`[CellsLayer] Groups: ${groups.size}`);
|
|
163
175
|
|
|
@@ -173,13 +185,17 @@
|
|
|
173
185
|
if (!style.visible) continue;
|
|
174
186
|
|
|
175
187
|
for (const cell of cells) {
|
|
176
|
-
// 4. BBox Filter (
|
|
177
|
-
|
|
188
|
+
// 4. BBox Filter (skip for auto-size - Mapbox handles culling efficiently)
|
|
189
|
+
const inView = useAutoSize ? true : (bounds?.contains([cell.longitude, cell.latitude]) ?? false);
|
|
190
|
+
|
|
191
|
+
if (inView) {
|
|
178
192
|
// 5. Z-Index Lookup
|
|
179
193
|
const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
|
|
180
194
|
const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
|
|
181
195
|
|
|
182
|
-
// 6. Calculate radius
|
|
196
|
+
// 6. Calculate radius
|
|
197
|
+
// Auto-size: Fixed meter-based size from site density
|
|
198
|
+
// Manual: Pixel-based size that scales with zoom
|
|
183
199
|
const MAX_Z = 35;
|
|
184
200
|
let radiusMeters: number;
|
|
185
201
|
|
|
@@ -188,10 +204,13 @@
|
|
|
188
204
|
const siteDistance = dataStore.siteDistanceStore.getDistance(cell.siteId, 500);
|
|
189
205
|
const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
|
|
190
206
|
|
|
207
|
+
// Apply user's multiplier to scale the result
|
|
208
|
+
const adjustedAutoRadius = autoRadius * displayStore.autoSizeMultiplier;
|
|
209
|
+
|
|
191
210
|
// Scale based on z-index for stacking visibility
|
|
192
211
|
// Lower z-index (background) = larger, higher z-index (foreground) = smaller
|
|
193
212
|
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08; // 8% per layer
|
|
194
|
-
radiusMeters =
|
|
213
|
+
radiusMeters = adjustedAutoRadius * scaleFactor;
|
|
195
214
|
} else {
|
|
196
215
|
// Manual mode: base from pixel size, then scale by z-index
|
|
197
216
|
const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
}
|
|
@@ -26,9 +26,11 @@ 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
|
|
29
30
|
* @param siteId Site to compute distance for
|
|
30
31
|
* @param siteLocation Location of the site
|
|
31
32
|
* @param allSiteLocations Map of all site locations
|
|
32
|
-
* @
|
|
33
|
+
* @param neighborCount Number of closest neighbors to average (default: 5)
|
|
34
|
+
* @returns Average distance to N nearest neighbors in meters, or Infinity if no neighbors
|
|
33
35
|
*/
|
|
34
|
-
export declare function computeNearestNeighbor(siteId: string, siteLocation: [number, number], allSiteLocations: Map<string, [number, number]
|
|
36
|
+
export declare function computeNearestNeighbor(siteId: string, siteLocation: [number, number], allSiteLocations: Map<string, [number, number]>, neighborCount?: number): number;
|
|
@@ -54,18 +54,26 @@ 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
|
|
57
58
|
* @param siteId Site to compute distance for
|
|
58
59
|
* @param siteLocation Location of the site
|
|
59
60
|
* @param allSiteLocations Map of all site locations
|
|
60
|
-
* @
|
|
61
|
+
* @param neighborCount Number of closest neighbors to average (default: 5)
|
|
62
|
+
* @returns Average distance to N nearest neighbors in meters, or Infinity if no neighbors
|
|
61
63
|
*/
|
|
62
|
-
export function computeNearestNeighbor(siteId, siteLocation, allSiteLocations) {
|
|
63
|
-
|
|
64
|
+
export function computeNearestNeighbor(siteId, siteLocation, allSiteLocations, neighborCount = 5) {
|
|
65
|
+
const distances = [];
|
|
64
66
|
for (const [otherId, otherLoc] of allSiteLocations) {
|
|
65
67
|
if (siteId === otherId)
|
|
66
68
|
continue;
|
|
67
69
|
const dist = haversineDistance(siteLocation, otherLoc);
|
|
68
|
-
|
|
70
|
+
distances.push(dist);
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
if (distances.length === 0)
|
|
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;
|
|
71
79
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Cell } from '../types';
|
|
2
2
|
import { SiteDistanceStore } from './site.distance.svelte';
|
|
3
|
+
import type { CellDisplayStore } from './cell.display.svelte';
|
|
3
4
|
export declare class CellDataStore {
|
|
4
5
|
rawCells: Cell[];
|
|
5
6
|
filterOnAir: boolean;
|
|
6
7
|
siteDistanceStore: SiteDistanceStore;
|
|
7
|
-
|
|
8
|
+
displayStore?: CellDisplayStore;
|
|
9
|
+
constructor(displayStore?: CellDisplayStore);
|
|
8
10
|
setCells(cells: Cell[]): void;
|
|
9
11
|
get filteredCells(): Cell[];
|
|
10
12
|
}
|
|
11
|
-
|
|
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;
|
|
@@ -4,13 +4,21 @@ export class CellDataStore {
|
|
|
4
4
|
filterOnAir = $state(false);
|
|
5
5
|
// Internal site distance store for auto-sizing
|
|
6
6
|
siteDistanceStore;
|
|
7
|
-
|
|
7
|
+
// Reference to display store for auto-size settings
|
|
8
|
+
displayStore;
|
|
9
|
+
constructor(displayStore) {
|
|
8
10
|
this.siteDistanceStore = new SiteDistanceStore();
|
|
11
|
+
this.displayStore = displayStore;
|
|
9
12
|
}
|
|
10
13
|
setCells(cells) {
|
|
11
14
|
this.rawCells = cells;
|
|
12
15
|
// Automatically update site distances when cells are loaded
|
|
13
|
-
|
|
16
|
+
// Pass auto-size and grid capping settings from displayStore
|
|
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);
|
|
14
22
|
}
|
|
15
23
|
get filteredCells() {
|
|
16
24
|
if (!this.filterOnAir)
|
|
@@ -18,6 +26,10 @@ export class CellDataStore {
|
|
|
18
26
|
return this.rawCells.filter(c => c.status === 'On_Air');
|
|
19
27
|
}
|
|
20
28
|
}
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Factory function to create a new CellDataStore
|
|
31
|
+
* @param displayStore Optional display store for auto-size settings
|
|
32
|
+
*/
|
|
33
|
+
export function createCellDataStore(displayStore) {
|
|
34
|
+
return new CellDataStore(displayStore);
|
|
23
35
|
}
|
|
@@ -9,6 +9,11 @@ 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;
|
|
12
17
|
level1: CellGroupingField;
|
|
13
18
|
level2: CellGroupingField;
|
|
14
19
|
currentZIndex: Record<string, number>;
|
|
@@ -11,6 +11,12 @@ 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
|
|
14
20
|
// Grouping
|
|
15
21
|
level1 = $state('tech');
|
|
16
22
|
level2 = $state('fband');
|
|
@@ -40,6 +46,11 @@ export class CellDisplayStore {
|
|
|
40
46
|
this.layerGrouping = parsed.layerGrouping ?? 'frequency';
|
|
41
47
|
this.useAutoSize = parsed.useAutoSize ?? false;
|
|
42
48
|
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;
|
|
43
54
|
this.level1 = parsed.level1 ?? 'tech';
|
|
44
55
|
this.level2 = parsed.level2 ?? 'fband';
|
|
45
56
|
this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
|
|
@@ -64,6 +75,11 @@ export class CellDisplayStore {
|
|
|
64
75
|
layerGrouping: this.layerGrouping,
|
|
65
76
|
useAutoSize: this.useAutoSize,
|
|
66
77
|
autoSizeMode: this.autoSizeMode,
|
|
78
|
+
autoSizeNeighborCount: this.autoSizeNeighborCount,
|
|
79
|
+
autoSizeMultiplier: this.autoSizeMultiplier,
|
|
80
|
+
useGridCapping: this.useGridCapping,
|
|
81
|
+
gridCapPercentile: this.gridCapPercentile,
|
|
82
|
+
gridSizeKm: this.gridSizeKm,
|
|
67
83
|
level1: this.level1,
|
|
68
84
|
level2: this.level2,
|
|
69
85
|
labelPixelDistance: this.labelPixelDistance,
|
|
@@ -8,6 +8,7 @@ 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>;
|
|
11
12
|
computedSites: Set<string>;
|
|
12
13
|
lastComputeTime: number;
|
|
13
14
|
computedCount: number;
|
|
@@ -15,14 +16,20 @@ export declare class SiteDistanceStore {
|
|
|
15
16
|
/**
|
|
16
17
|
* Incrementally update distances for new sites
|
|
17
18
|
* Only computes distances for sites not in cache
|
|
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
|
|
18
24
|
*/
|
|
19
|
-
updateDistances(cells: Cell[]): void;
|
|
25
|
+
updateDistances(cells: Cell[], neighborCount?: number, useGridCapping?: boolean, gridCapPercentile?: number, gridSizeKm?: number): void;
|
|
20
26
|
/**
|
|
21
27
|
* Update existing sites if needed (when no new sites but data might have changed)
|
|
22
28
|
*/
|
|
23
29
|
private updateExistingSitesIfNeeded;
|
|
24
30
|
/**
|
|
25
31
|
* Get distance for a site, with fallback
|
|
32
|
+
* Returns capped size if grid capping was applied, otherwise original
|
|
26
33
|
*/
|
|
27
34
|
getDistance(siteId: string, fallback?: number): number;
|
|
28
35
|
/**
|
|
@@ -6,10 +6,13 @@
|
|
|
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';
|
|
9
10
|
export class SiteDistanceStore {
|
|
10
11
|
key = 'map-v3-site-distances';
|
|
11
|
-
// Cached nearest neighbor distances
|
|
12
|
+
// Cached nearest neighbor distances (original calculated sizes)
|
|
12
13
|
distances = $state(new Map());
|
|
14
|
+
// Grid-capped sizes (after outlier removal)
|
|
15
|
+
cappedSizes = $state(new Map());
|
|
13
16
|
// Track which sites have been computed
|
|
14
17
|
computedSites = $state(new Set());
|
|
15
18
|
// Performance tracking
|
|
@@ -23,8 +26,13 @@ export class SiteDistanceStore {
|
|
|
23
26
|
/**
|
|
24
27
|
* Incrementally update distances for new sites
|
|
25
28
|
* Only computes distances for sites not in cache
|
|
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
|
|
26
34
|
*/
|
|
27
|
-
updateDistances(cells) {
|
|
35
|
+
updateDistances(cells, neighborCount = 5, useGridCapping = true, gridCapPercentile = 80, gridSizeKm = 1) {
|
|
28
36
|
if (!cells || cells.length === 0)
|
|
29
37
|
return;
|
|
30
38
|
const startTime = performance.now();
|
|
@@ -37,11 +45,11 @@ export class SiteDistanceStore {
|
|
|
37
45
|
this.updateExistingSitesIfNeeded(siteLocations);
|
|
38
46
|
return;
|
|
39
47
|
}
|
|
40
|
-
console.log(`[SiteDistance] Computing distances for ${newSites.length} new sites`);
|
|
48
|
+
console.log(`[SiteDistance] Computing distances for ${newSites.length} new sites (${neighborCount} neighbors)`);
|
|
41
49
|
// Compute distances for NEW sites
|
|
42
50
|
for (const siteId of newSites) {
|
|
43
51
|
const location = siteLocations.get(siteId);
|
|
44
|
-
const nearestDist = computeNearestNeighbor(siteId, location, siteLocations);
|
|
52
|
+
const nearestDist = computeNearestNeighbor(siteId, location, siteLocations, neighborCount);
|
|
45
53
|
this.distances.set(siteId, nearestDist);
|
|
46
54
|
this.computedSites.add(siteId);
|
|
47
55
|
}
|
|
@@ -49,15 +57,19 @@ export class SiteDistanceStore {
|
|
|
49
57
|
for (const [existingId, existingLoc] of siteLocations) {
|
|
50
58
|
if (newSites.includes(existingId))
|
|
51
59
|
continue; // Skip new sites
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
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);
|
|
61
73
|
}
|
|
62
74
|
const endTime = performance.now();
|
|
63
75
|
this.lastComputeTime = endTime - startTime;
|
|
@@ -86,15 +98,17 @@ export class SiteDistanceStore {
|
|
|
86
98
|
}
|
|
87
99
|
/**
|
|
88
100
|
* Get distance for a site, with fallback
|
|
101
|
+
* Returns capped size if grid capping was applied, otherwise original
|
|
89
102
|
*/
|
|
90
103
|
getDistance(siteId, fallback = 500) {
|
|
91
|
-
return this.distances.get(siteId) ?? fallback;
|
|
104
|
+
return this.cappedSizes.get(siteId) ?? this.distances.get(siteId) ?? fallback;
|
|
92
105
|
}
|
|
93
106
|
/**
|
|
94
107
|
* Clear all cached distances (useful for testing or data refresh)
|
|
95
108
|
*/
|
|
96
109
|
clear() {
|
|
97
110
|
this.distances.clear();
|
|
111
|
+
this.cappedSizes.clear();
|
|
98
112
|
this.computedSites.clear();
|
|
99
113
|
this.lastComputeTime = 0;
|
|
100
114
|
this.computedCount = 0;
|