@smartnet360/svelte-components 0.0.58 → 0.0.60

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.
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Demo Cell Data
3
3
  *
4
- * 1 site with 3 sectors (azimuths: 0°, 120°, 240°)
4
+ * 100 sites across San Francisco Bay Area
5
+ * Each site has 3 sectors (azimuths: 0°, 120°, 240°)
5
6
  * Each sector has 12 cells (all tech-band combinations)
6
- * Total: 36 cells
7
+ * Total: 100 sites × 3 sectors × 12 tech-bands = 3,600 cells
7
8
  */
8
9
  import type { Cell } from '../features/cells/types';
9
10
  /**
10
- * Generate demo cells: 3 sectors × 12 tech-bands = 36 cells
11
+ * Generate demo cells: 100 sites × 3 sectors × 12 tech-bands = 3,600 cells
11
12
  */
12
13
  export declare const demoCells: Cell[];
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * Demo Cell Data
3
3
  *
4
- * 1 site with 3 sectors (azimuths: 0°, 120°, 240°)
4
+ * 100 sites across San Francisco Bay Area
5
+ * Each site has 3 sectors (azimuths: 0°, 120°, 240°)
5
6
  * Each sector has 12 cells (all tech-band combinations)
6
- * Total: 36 cells
7
+ * Total: 100 sites × 3 sectors × 12 tech-bands = 3,600 cells
7
8
  */
8
- // Site location: San Francisco
9
- const SITE_LAT = 37.7749;
10
- const SITE_LNG = -122.4194;
11
- const SITE_ID = 'DEMO-SITE-001';
9
+ // Base location: San Francisco
10
+ const BASE_LAT = 37.7749;
11
+ const BASE_LNG = -122.4194;
12
+ // Grid parameters for distributing sites
13
+ const NUM_SITES = 1;
14
+ const GRID_SIZE = 10; // 10×10 grid
15
+ const LAT_SPACING = 0.01; // ~1.1 km spacing
16
+ const LNG_SPACING = 0.015; // ~1.1 km spacing (adjusted for longitude)
12
17
  // Standard beamwidth for sectors
13
18
  const BEAMWIDTH = 65;
14
19
  // Cell tech-band definitions with proper fband format
@@ -46,69 +51,82 @@ const STATUSES = [
46
51
  'On_Air'
47
52
  ];
48
53
  /**
49
- * Generate demo cells: 3 sectors × 12 tech-bands = 36 cells
54
+ * Generate demo cells: 100 sites × 3 sectors × 12 tech-bands = 3,600 cells
50
55
  */
51
56
  export const demoCells = [];
52
57
  let cellCounter = 1;
53
- AZIMUTHS.forEach((azimuth, sectorIndex) => {
54
- TECH_BANDS.forEach((techBand, techIndex) => {
55
- const cellId = `CELL-${String(cellCounter).padStart(3, '0')}`;
56
- const status = STATUSES[techIndex];
57
- demoCells.push({
58
- // Core properties
59
- id: cellId,
60
- txId: `TX-${String(cellCounter).padStart(3, '0')}`,
61
- cellID: cellId,
62
- cellID2G: techBand.tech === '2G' ? cellId : '',
63
- cellName: `Demo Cell ${techBand.tech} ${techBand.band} - Sector ${sectorIndex + 1}`,
64
- siteId: SITE_ID,
65
- tech: techBand.tech,
66
- fband: techBand.fband,
67
- frq: techBand.band,
68
- type: 'MACRO',
69
- status: status,
70
- onAirDate: '2024-01-15',
71
- // 2G specific
72
- bcch: techBand.tech === '2G' ? 100 + techIndex : 0,
73
- ctrlid: techBand.tech === '2G' ? `CTRL-${cellId}` : '',
74
- // 4G specific
75
- dlEarfn: techBand.tech === '4G' ? 6200 + techIndex * 100 : 0,
76
- // Physical properties
77
- antenna: 'DEMO-ANTENNA-MODEL',
78
- azimuth: azimuth,
79
- height: 30, // 30 meters antenna height
80
- electricalTilt: '3',
81
- beamwidth: BEAMWIDTH,
82
- latitude: SITE_LAT,
83
- longitude: SITE_LNG,
84
- dx: 0,
85
- dy: 0,
86
- siteLatitude: SITE_LAT,
87
- siteLongitude: SITE_LNG,
88
- // Planning
89
- comment: `Demo ${techBand.tech} ${techBand.band} cell at azimuth ${azimuth}°`,
90
- planner: 'Demo User',
91
- // Atoll properties
92
- atollETP: 43.0,
93
- atollPW: 20.0,
94
- atollRS: 500.0 + (techBand.band === '700' ? 200 : 0), // Lower freq = longer range
95
- atollBW: parseFloat(techBand.band) / 100, // Simplified bandwidth
96
- // Network properties
97
- cellId3: `${cellId}-3G`,
98
- nwtP1: 20,
99
- nwtP2: 40,
100
- pci1: (cellCounter % 504), // Physical Cell ID for LTE
101
- nwtRS: 450.0,
102
- nwtBW: 10.0,
103
- // Other
104
- other: {
105
- demoCell: true,
106
- sector: sectorIndex + 1,
107
- techBandKey: `${techBand.tech}_${techBand.band}`
108
- },
109
- customSubgroup: `Sector-${sectorIndex + 1}`
58
+ // Generate sites in a grid pattern
59
+ for (let siteIndex = 0; siteIndex < NUM_SITES; siteIndex++) {
60
+ const row = Math.floor(siteIndex / GRID_SIZE);
61
+ const col = siteIndex % GRID_SIZE;
62
+ // Calculate site position (centered grid around base location)
63
+ const siteLat = BASE_LAT + (row - GRID_SIZE / 2) * LAT_SPACING;
64
+ const siteLng = BASE_LNG + (col - GRID_SIZE / 2) * LNG_SPACING;
65
+ const siteId = `DEMO-SITE-${String(siteIndex + 1).padStart(3, '0')}`;
66
+ // Generate 3 sectors per site
67
+ AZIMUTHS.forEach((azimuth, sectorIndex) => {
68
+ // Generate 12 tech-bands per sector
69
+ TECH_BANDS.forEach((techBand, techIndex) => {
70
+ const cellId = `CELL-${String(cellCounter).padStart(4, '0')}`;
71
+ const status = STATUSES[techIndex];
72
+ demoCells.push({
73
+ // Core properties
74
+ id: cellId,
75
+ txId: `TX-${String(cellCounter).padStart(4, '0')}`,
76
+ cellID: cellId,
77
+ cellID2G: techBand.tech === '2G' ? cellId : '',
78
+ cellName: `${siteId} ${techBand.tech}${techBand.band} S${sectorIndex + 1}`,
79
+ siteId: siteId,
80
+ tech: techBand.tech,
81
+ fband: techBand.fband,
82
+ frq: techBand.band,
83
+ type: 'MACRO',
84
+ status: status,
85
+ onAirDate: '2024-01-15',
86
+ // 2G specific
87
+ bcch: techBand.tech === '2G' ? 100 + techIndex : 0,
88
+ ctrlid: techBand.tech === '2G' ? `CTRL-${cellId}` : '',
89
+ // 4G specific
90
+ dlEarfn: techBand.tech === '4G' ? 6200 + techIndex * 100 : 0,
91
+ // Physical properties
92
+ antenna: 'DEMO-ANTENNA-MODEL',
93
+ azimuth: azimuth,
94
+ height: 30, // 30 meters antenna height
95
+ electricalTilt: '3',
96
+ beamwidth: BEAMWIDTH,
97
+ latitude: siteLat,
98
+ longitude: siteLng,
99
+ dx: 0,
100
+ dy: 0,
101
+ siteLatitude: siteLat,
102
+ siteLongitude: siteLng,
103
+ // Planning
104
+ comment: `Demo ${techBand.tech} ${techBand.band} cell at azimuth ${azimuth}°`,
105
+ planner: 'Demo User',
106
+ // Atoll properties
107
+ atollETP: 43.0,
108
+ atollPW: 20.0,
109
+ atollRS: 500.0 + (techBand.band === '700' ? 200 : 0), // Lower freq = longer range
110
+ atollBW: parseFloat(techBand.band) / 100, // Simplified bandwidth
111
+ // Network properties
112
+ cellId3: `${cellId}-3G`,
113
+ nwtP1: 20,
114
+ nwtP2: 40,
115
+ pci1: (cellCounter % 504), // Physical Cell ID for LTE
116
+ nwtRS: 450.0,
117
+ nwtBW: 10.0,
118
+ // Other
119
+ other: {
120
+ demoCell: true,
121
+ siteNumber: siteIndex + 1,
122
+ sector: sectorIndex + 1,
123
+ techBandKey: `${techBand.tech}_${techBand.band}`,
124
+ gridPosition: { row, col }
125
+ },
126
+ customSubgroup: `Sector-${sectorIndex + 1}`
127
+ });
128
+ cellCounter++;
110
129
  });
111
- cellCounter++;
112
130
  });
113
- });
114
- console.log(`Generated ${demoCells.length} demo cells across ${AZIMUTHS.length} sectors`);
131
+ }
132
+ console.log(`Generated ${demoCells.length} demo cells across ${NUM_SITES} sites (${AZIMUTHS.length} sectors × ${TECH_BANDS.length} tech-bands per site)`);
@@ -56,10 +56,36 @@
56
56
  let level1 = $state<Exclude<CellGroupingField, 'none'>>(store.groupingConfig.level1);
57
57
  let level2 = $state<CellGroupingField>(store.groupingConfig.level2);
58
58
 
59
+ // Track config for localStorage invalidation
60
+ const STORAGE_CONFIG_KEY = 'cellular-cell-filter:config';
61
+
62
+ /**
63
+ * Check if grouping config has changed and clear localStorage if needed
64
+ */
65
+ function validateStoredConfig() {
66
+ const storedConfigStr = localStorage.getItem(STORAGE_CONFIG_KEY);
67
+ const currentConfig = `${level1}:${level2}`;
68
+
69
+ if (storedConfigStr !== currentConfig) {
70
+ console.log('CellFilterControl: Config changed, clearing tree state', {
71
+ stored: storedConfigStr,
72
+ current: currentConfig
73
+ });
74
+ // Clear tree state from localStorage
75
+ localStorage.removeItem('cellular-cell-filter:checked');
76
+ localStorage.removeItem('cellular-cell-filter:expanded');
77
+ // Save new config
78
+ localStorage.setItem(STORAGE_CONFIG_KEY, currentConfig);
79
+ }
80
+ }
81
+
59
82
  // Rebuild tree when grouping config or cells change
60
83
  function rebuildTree() {
61
84
  if (store.cells.length === 0) return;
62
85
 
86
+ // Validate and clear stale localStorage
87
+ validateStoredConfig();
88
+
63
89
  console.log('CellFilterControl: Building tree with config:', { level1, level2 });
64
90
 
65
91
  const treeNodes = buildCellTree(
@@ -34,12 +34,28 @@
34
34
 
35
35
  let map = $state<MapboxMap | null>(null);
36
36
  let mounted = $state(false);
37
+ let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
38
+ let viewportVersion = $state(0); // Increment this to force $effect re-run on viewport changes
37
39
 
38
- // Zoom change handler
39
- function handleZoomEnd() {
40
+ // Viewport change handler (pan/zoom/move) with debouncing
41
+ function handleMoveEnd() {
40
42
  if (!map) return;
41
- const newZoom = map.getZoom();
42
- store.setCurrentZoom(newZoom);
43
+
44
+ // Clear any existing timer
45
+ if (viewportUpdateTimer) {
46
+ clearTimeout(viewportUpdateTimer);
47
+ }
48
+
49
+ // Debounce: wait 200ms after last move event before re-rendering
50
+ viewportUpdateTimer = setTimeout(() => {
51
+ if (!map) return;
52
+ const newZoom = map.getZoom();
53
+ console.log('CellsLayer: Viewport changed (zoom/pan), updating to zoom:', newZoom);
54
+ // Update zoom if changed
55
+ store.setCurrentZoom(newZoom);
56
+ // Force $effect to re-run by incrementing version (triggers on pan without zoom change)
57
+ viewportVersion++;
58
+ }, 200);
43
59
  }
44
60
 
45
61
  onMount(() => {
@@ -55,8 +71,8 @@
55
71
  // Set initial zoom
56
72
  store.setCurrentZoom(mapInstance.getZoom());
57
73
 
58
- // Listen to zoom changes
59
- mapInstance.on('zoomend', handleZoomEnd);
74
+ // Listen to viewport changes (pan + zoom, debounced)
75
+ mapInstance.on('moveend', handleMoveEnd);
60
76
  }
61
77
  });
62
78
 
@@ -68,7 +84,12 @@
68
84
  onDestroy(() => {
69
85
  if (!map) return;
70
86
 
71
- map.off('zoomend', handleZoomEnd);
87
+ // Clean up timer
88
+ if (viewportUpdateTimer) {
89
+ clearTimeout(viewportUpdateTimer);
90
+ }
91
+
92
+ map.off('moveend', handleMoveEnd);
72
93
 
73
94
  // Clean up layers and source
74
95
  if (map.getLayer(LINE_LAYER_ID)) {
@@ -84,13 +105,17 @@
84
105
 
85
106
  // Reactive: Update GeoJSON when cells/zoom/filters/settings change
86
107
  $effect(() => {
108
+ // Track viewportVersion to force re-run on pan (even without zoom change)
109
+ viewportVersion;
110
+
87
111
  console.log('CellsLayer $effect triggered:', {
88
112
  mounted,
89
113
  hasMap: !!map,
90
114
  showCells: store.showCells,
91
115
  filteredCellsCount: store.filteredCells.length,
92
116
  currentZoom: store.currentZoom,
93
- baseRadius: store.baseRadius
117
+ baseRadius: store.baseRadius,
118
+ viewportVersion
94
119
  });
95
120
 
96
121
  if (!mounted || !map || !store.showCells) {
@@ -110,9 +135,31 @@
110
135
  return;
111
136
  }
112
137
 
113
- // Generate GeoJSON from filtered cells
138
+ // Filter cells by viewport bounds (only render visible cells)
139
+ const bounds = map.getBounds();
140
+ if (!bounds) {
141
+ console.warn('CellsLayer: Cannot get map bounds, skipping viewport filter');
142
+ return;
143
+ }
144
+
145
+ const visibleCells = store.filteredCells.filter(cell =>
146
+ bounds.contains([cell.longitude, cell.latitude])
147
+ );
148
+
149
+ console.log('CellsLayer: Viewport filtering:', {
150
+ totalFiltered: store.filteredCells.length,
151
+ visibleInViewport: visibleCells.length,
152
+ bounds: {
153
+ north: bounds.getNorth(),
154
+ south: bounds.getSouth(),
155
+ east: bounds.getEast(),
156
+ west: bounds.getWest()
157
+ }
158
+ });
159
+
160
+ // Generate GeoJSON from visible cells only
114
161
  const geoJSON = cellsToGeoJSON(
115
- store.filteredCells,
162
+ visibleCells,
116
163
  store.currentZoom,
117
164
  store.baseRadius,
118
165
  store.groupColorMap
@@ -71,12 +71,22 @@ function generateNodeId(field, value, parentId) {
71
71
  */
72
72
  export function buildCellTree(cells, config, colorMap) {
73
73
  const { level1, level2 } = config;
74
+ console.log('=== buildCellTree START ===');
75
+ console.log('Config:', { level1, level2 });
76
+ console.log('Total cells:', cells.length);
74
77
  // If level2 is 'none', create flat 2-level tree
75
78
  if (level2 === 'none') {
76
- return buildFlatTree(cells, level1, colorMap);
79
+ console.log('Building flat tree (2-level)');
80
+ const tree = buildFlatTree(cells, level1, colorMap);
81
+ console.log('Flat tree built, root children:', tree.children?.length);
82
+ return tree;
77
83
  }
78
84
  // Otherwise, create nested 3-level tree
79
- return buildNestedTree(cells, level1, level2, colorMap);
85
+ console.log('Building nested tree (3-level)');
86
+ const tree = buildNestedTree(cells, level1, level2, colorMap);
87
+ console.log('Nested tree built, root children:', tree.children?.length);
88
+ console.log('=== buildCellTree END ===');
89
+ return tree;
80
90
  }
81
91
  /**
82
92
  * Build flat 2-level tree (Root → Level1 groups)
@@ -97,6 +107,8 @@ function buildFlatTree(cells, level1, colorMap) {
97
107
  const children = sortedGroups.map(([value, groupCells]) => {
98
108
  const nodeId = generateNodeId(level1, value);
99
109
  const color = colorMap?.get(nodeId);
110
+ const cellIds = groupCells.map((c) => c.id);
111
+ console.log(` Flat tree node: id="${nodeId}", cells=${cellIds.length}, cellIds sample:`, cellIds.slice(0, 3));
100
112
  return {
101
113
  id: nodeId,
102
114
  label: getGroupLabel(level1, value, groupCells.length),
@@ -106,7 +118,7 @@ function buildFlatTree(cells, level1, colorMap) {
106
118
  type: 'leafGroup',
107
119
  groupingField: level1,
108
120
  groupingValue: value,
109
- cellIds: groupCells.map((c) => c.id),
121
+ cellIds,
110
122
  color
111
123
  }
112
124
  };
@@ -149,6 +161,8 @@ function buildNestedTree(cells, level1, level2, colorMap) {
149
161
  const level2Children = sortedLevel2.map(([value2, groupCells]) => {
150
162
  const nodeId = generateNodeId(level2, value2, parentId);
151
163
  const color = colorMap?.get(nodeId);
164
+ const cellIds = groupCells.map((c) => c.id);
165
+ console.log(` Nested tree leaf: id="${nodeId}", cells=${cellIds.length}, cellIds sample:`, cellIds.slice(0, 3));
152
166
  return {
153
167
  id: nodeId,
154
168
  label: getGroupLabel(level2, value2, groupCells.length),
@@ -160,13 +174,14 @@ function buildNestedTree(cells, level1, level2, colorMap) {
160
174
  groupingValue: value2,
161
175
  parentGroupingField: level1,
162
176
  parentGroupingValue: value1,
163
- cellIds: groupCells.map((c) => c.id),
177
+ cellIds,
164
178
  color
165
179
  }
166
180
  };
167
181
  });
168
182
  // Calculate total cells in this level1 group
169
183
  const totalCells = Array.from(level2Groups.values()).flat().length;
184
+ console.log(` Nested tree parent: id="${parentId}", totalCells=${totalCells}, children=${level2Children.length}`);
170
185
  return {
171
186
  id: parentId,
172
187
  label: getGroupLabel(level1, value1, totalCells),
@@ -197,30 +212,72 @@ function buildNestedTree(cells, level1, level2, colorMap) {
197
212
  * @returns Filtered array of cells
198
213
  */
199
214
  export function getFilteredCells(checkedPaths, allCells) {
215
+ console.log('=== getFilteredCells START ===');
216
+ console.log('Checked paths:', Array.from(checkedPaths));
217
+ console.log('Total cells:', allCells.length);
200
218
  // If root is checked (or no specific filters), return all cells
201
219
  if (checkedPaths.has('root') && checkedPaths.size === 1) {
220
+ console.log('Root only checked, returning all cells');
202
221
  return allCells;
203
222
  }
223
+ // Sample first 3 cells for detailed logging
224
+ const sampleSize = 3;
225
+ let processedCount = 0;
204
226
  // Filter cells that match any of the checked paths
205
227
  const filtered = allCells.filter((cell) => {
228
+ const shouldLog = processedCount < sampleSize;
229
+ if (shouldLog) {
230
+ console.log(`\n--- Sample Cell ${processedCount + 1}: ${cell.id} ---`);
231
+ console.log(` Properties: tech="${cell.tech}", frq="${cell.frq}", status="${cell.status}"`);
232
+ }
206
233
  // Check if this cell belongs to any checked path
207
- return Array.from(checkedPaths).some((path) => {
234
+ const matched = Array.from(checkedPaths).some((path) => {
208
235
  // Match cell properties against path segments
209
- if (path === 'root')
236
+ if (path === 'root') {
237
+ if (shouldLog)
238
+ console.log(` Path "root": MATCH`);
210
239
  return true;
211
- // Parse path and check if cell matches
212
- const segments = path.split(':');
213
- // Simple string matching against cell properties
214
- return segments.every((segment) => {
215
- return (cell.tech === segment ||
240
+ }
241
+ // Parse path and filter out "root" prefix and deduplicate consecutive segments
242
+ const allSegments = path.split(':');
243
+ // Remove "root" and deduplicate consecutive segments
244
+ const segments = [];
245
+ let prevSegment = '';
246
+ for (const segment of allSegments) {
247
+ if (segment !== 'root' && segment !== prevSegment) {
248
+ segments.push(segment);
249
+ prevSegment = segment;
250
+ }
251
+ }
252
+ if (shouldLog) {
253
+ console.log(` Checking path "${path}" (raw: [${allSegments.join(', ')}], filtered: [${segments.join(', ')}])`);
254
+ }
255
+ // Match filtered segments against cell properties
256
+ const segmentMatches = segments.every((segment) => {
257
+ const matches = (cell.tech === segment ||
216
258
  cell.frq === segment ||
217
259
  cell.status === segment ||
218
260
  cell.siteId === segment ||
219
261
  cell.customSubgroup === segment ||
220
262
  cell.type === segment ||
221
263
  cell.planner === segment);
264
+ if (shouldLog) {
265
+ console.log(` Segment "${segment}": ${matches ? '✓ MATCH' : '✗ NO MATCH'}`);
266
+ }
267
+ return matches;
222
268
  });
269
+ if (shouldLog) {
270
+ console.log(` Path "${path}": ${segmentMatches ? '✓ MATCHED' : '✗ NOT MATCHED'}`);
271
+ }
272
+ return segmentMatches;
223
273
  });
274
+ if (shouldLog) {
275
+ console.log(` Final result: ${matched ? '✓ INCLUDE' : '✗ EXCLUDE'}`);
276
+ }
277
+ processedCount++;
278
+ return matched;
224
279
  });
280
+ console.log(`\nFiltered result: ${filtered.length} of ${allCells.length} cells`);
281
+ console.log('=== getFilteredCells END ===\n');
225
282
  return filtered;
226
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",