@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.
@@ -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: 100 sites × 3 sectors × 12 tech-bands = 3,600 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
- // Grid parameters for distributing sites
12
- const NUM_SITES = 1700;
13
- const GRID_SIZE = 10; // 10×10 grid
14
- const LAT_SPACING = 0.01; // ~1.1 km spacing
15
- const LNG_SPACING = 0.015; // ~1.1 km spacing (adjusted for longitude)
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: 100 sites × 3 sectors × 12 tech-bands = 3,600 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
- // Generate sites in a grid pattern
58
- for (let siteIndex = 0; siteIndex < NUM_SITES; siteIndex++) {
59
- const row = Math.floor(siteIndex / GRID_SIZE);
60
- const col = siteIndex % GRID_SIZE;
61
- // Calculate site position (centered grid around base location)
62
- const siteLat = BASE_LAT + (row - GRID_SIZE / 2) * LAT_SPACING;
63
- const siteLng = BASE_LNG + (col - GRID_SIZE / 2) * LNG_SPACING;
64
- const siteId = `DEMO-SITE-${String(siteIndex + 1).padStart(3, '0')}`;
65
- // Generate 3 sectors per site
66
- AZIMUTHS.forEach((azimuth, sectorIndex) => {
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: siteIndex + 1,
206
+ siteNumber: actualSiteIndex,
121
207
  sector: sectorIndex + 1,
122
208
  techBandKey: `${techBand.tech}_${techBand.band}`,
123
- gridPosition: { row, col }
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">Classic (frequency-priority)</option>
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
- <!-- 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 -->
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 Scale</div>
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.autoSizeMultiplier.toFixed(1)}x</span>
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.autoSizeMultiplier}
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 map events when NOT using auto-size
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 _autoSizeNeighborCount = displayStore.autoSizeNeighborCount;
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 useAutoSize = displayStore.useAutoSize;
152
+ const zoom = map.getZoom();
153
+ const centerLat = map.getCenter().lat;
158
154
 
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})`);
155
+ console.log(`[CellsLayer] Rendering.. Zoom: ${zoom.toFixed(2)}, Cells: ${dataStore.filteredCells.length}`);
165
156
 
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)}`);
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 (skip for auto-size - Mapbox handles culling efficiently)
189
- const inView = useAutoSize ? true : (bounds?.contains([cell.longitude, cell.latitude]) ?? false);
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
- if (inView) {
192
- // 5. Z-Index Lookup
193
- const zIndexKey = `${cell.tech}_${cell.frq}` as TechnologyBandKey;
194
- const zIndex = displayStore.currentZIndex[zIndexKey] ?? 10;
195
-
196
- // 6. Calculate radius
197
- // Auto-size: Fixed meter-based size from site density
198
- // Manual: Pixel-based size that scales with zoom
199
- const MAX_Z = 35;
200
- let radiusMeters: number;
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
- if (displayStore.useAutoSize) {
203
- // Auto-size mode: get target radius for this site
204
- const siteDistance = dataStore.siteDistanceStore.getDistance(cell.siteId, 500);
205
- const autoRadius = calculateAutoRadius(siteDistance, displayStore.autoSizeMode);
206
-
207
- // Apply user's multiplier to scale the result
208
- const adjustedAutoRadius = autoRadius * displayStore.autoSizeMultiplier;
209
-
210
- // Scale based on z-index for stacking visibility
211
- // Lower z-index (background) = larger, higher z-index (foreground) = smaller
212
- const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08; // 8% per layer
213
- radiusMeters = adjustedAutoRadius * scaleFactor;
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
- // 7. Apply beamwidth boost from displayStore preset
221
- const beamwidthBoost = displayStore.currentBeamwidthBoost[zIndexKey] || 0;
222
- const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
215
+ // 7. Apply beamwidth boost from displayStore preset
216
+ const beamwidthBoost = displayStore.currentBeamwidthBoost[zIndexKey] || 0;
217
+ const adjustedBeamwidth = cell.beamwidth + beamwidthBoost;
223
218
 
224
- // 8. Generate Arc
225
- const feature = generateCellArc(cell, radiusMeters, zIndex, style.color, adjustedBeamwidth);
226
- features.push(feature);
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
- * @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
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]>, neighborCount?: 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
- * @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
60
+ * @returns Distance to nearest neighbor in meters, or Infinity if no neighbors
63
61
  */
64
- export function computeNearestNeighbor(siteId, siteLocation, allSiteLocations, neighborCount = 5) {
65
- const distances = [];
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
- distances.push(dist);
68
+ minDist = Math.min(minDist, dist);
71
69
  }
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;
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
- displayStore?: CellDisplayStore;
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
- // Reference to display store for auto-size settings
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
- // 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);
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
- * 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);
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
- autoSizeNeighborCount: number;
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
- 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
+ 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.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;
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
- autoSizeNeighborCount: this.autoSizeNeighborCount,
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
- * Incrementally update distances for new sites
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
16
+ * Update distances for all sites in current dataset
17
+ * Always recomputes to ensure accuracy
24
18
  */
25
- updateDistances(cells: Cell[], neighborCount?: number, useGridCapping?: boolean, gridCapPercentile?: number, gridSizeKm?: number): void;
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 (original calculated sizes)
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
- * Incrementally update distances for new sites
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
24
+ * Update distances for all sites in current dataset
25
+ * Always recomputes to ensure accuracy
34
26
  */
35
- updateDistances(cells, neighborCount = 5, useGridCapping = true, gridCapPercentile = 80, gridSizeKm = 1) {
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
- // Find NEW sites (not in cache)
41
- const newSites = [...siteLocations.keys()].filter((id) => !this.computedSites.has(id));
42
- if (newSites.length === 0) {
43
- // No new sites, but check if we need to update existing sites
44
- // (in case a new site is closer than previously computed)
45
- this.updateExistingSitesIfNeeded(siteLocations);
46
- return;
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 ${newSites.length} new sites in ${this.lastComputeTime.toFixed(1)}ms (total: ${this.computedCount})`);
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.cappedSizes.get(siteId) ?? this.distances.get(siteId) ?? fallback;
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
  */
@@ -139,6 +139,7 @@
139
139
  <option value="siteName">Name</option>
140
140
  <option value="provider">Provider</option>
141
141
  <option value="cellCount">Cell Count</option>
142
+ <option value="nearestSiteDistance">Nearest Site Distance</option>
142
143
  </select>
143
144
  </div>
144
145
  </div>
@@ -116,7 +116,8 @@
116
116
  properties: {
117
117
  siteId: site.siteId,
118
118
  name: site.siteName,
119
- color: color
119
+ color: color,
120
+ nearestSiteDistance: site.nearestSiteDistance
120
121
  }
121
122
  });
122
123
  }
@@ -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 {
@@ -9,4 +9,5 @@ export interface Site {
9
9
  level1: string;
10
10
  level2: string;
11
11
  cellCount: number;
12
+ nearestSiteDistance?: number;
12
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.94",
3
+ "version": "0.0.96",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -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
- }