@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.
@@ -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
- map.on('moveend', updateLayer);
106
- map.on('zoomend', updateLayer);
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 bounds = map.getBounds();
148
- if (!bounds) return;
149
-
150
- const zoom = map.getZoom();
151
- const centerLat = map.getCenter().lat;
157
+ const useAutoSize = displayStore.useAutoSize;
152
158
 
153
- console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
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
- console.log(`[CellsLayer] Base radius: ${baseRadiusMeters.toFixed(2)}m for target ${displayStore.targetPixelSize}px`);
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 (Level 1=Tech, Level 2=Band for now hardcoded)
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 (Simple point check)
177
- if (bounds.contains([cell.longitude, cell.latitude])) {
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 with z-index scaling
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 = autoRadius * scaleFactor;
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
- * @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
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]>): 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
- * @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
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
- let minDist = Infinity;
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
- minDist = Math.min(minDist, dist);
70
+ distances.push(dist);
69
71
  }
70
- return minDist;
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
- constructor();
8
+ displayStore?: CellDisplayStore;
9
+ constructor(displayStore?: CellDisplayStore);
8
10
  setCells(cells: Cell[]): void;
9
11
  get filteredCells(): Cell[];
10
12
  }
11
- export declare function createCellDataStore(): CellDataStore;
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
- constructor() {
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
- this.siteDistanceStore.updateDistances(cells);
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
- export function createCellDataStore() {
22
- return new CellDataStore();
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
- let currentMin = this.distances.get(existingId) || Infinity;
53
- for (const newId of newSites) {
54
- const newLoc = siteLocations.get(newId);
55
- const dist = haversineDistance(existingLoc, newLoc);
56
- currentMin = Math.min(currentMin, dist);
57
- }
58
- if (currentMin < (this.distances.get(existingId) || Infinity)) {
59
- this.distances.set(existingId, currentMin);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.92",
3
+ "version": "0.0.94",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",