@smartnet360/svelte-components 0.0.75 → 0.0.76

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.
@@ -14,7 +14,7 @@
14
14
  import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
15
15
  import { useMapbox } from '../../../core/hooks/useMapbox';
16
16
  import { cellsToGeoJSON } from '../utils/cellGeoJSON';
17
- import { CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from '../constants/zIndex';
17
+ import { CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX, Z_INDEX_BY_BAND } from '../constants/zIndex';
18
18
 
19
19
  interface Props {
20
20
  /** Cell store context */
@@ -24,28 +24,34 @@
24
24
  }
25
25
 
26
26
  let { store, namespace }: Props = $props();
27
-
28
- const FILL_LAYER_ID = `${namespace}-cells-fill`;
29
- const LINE_LAYER_ID = `${namespace}-cells-line`;
27
+
30
28
  const SOURCE_ID = `${namespace}-cells`;
31
-
29
+
32
30
  // Get map from mapbox hook
33
31
  const mapStore = useMapbox();
34
-
32
+
35
33
  let map = $state<MapboxMap | null>(null);
36
34
  let mounted = $state(false);
37
35
  let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
38
36
  let viewportVersion = $state(0); // Increment this to force $effect re-run on viewport changes
39
-
37
+
38
+ // Get all unique z-index values and sort them
39
+ const sortedZIndexes = [...new Set(Object.values(Z_INDEX_BY_BAND))].sort((a, b) => a - b);
40
+
41
+ // Create layer IDs for each z-index
42
+ const fillLayerIds = sortedZIndexes.map((zIndex) => `${namespace}-cells-fill-${zIndex}`);
43
+ const lineLayerIds = sortedZIndexes.map((zIndex) => `${namespace}-cells-line-${zIndex}`);
44
+ const allLayerIds = [...fillLayerIds, ...lineLayerIds];
45
+
40
46
  // Viewport change handler (pan/zoom/move) with debouncing
41
47
  function handleMoveEnd() {
42
48
  if (!map) return;
43
-
49
+
44
50
  // Clear any existing timer
45
51
  if (viewportUpdateTimer) {
46
52
  clearTimeout(viewportUpdateTimer);
47
53
  }
48
-
54
+
49
55
  // Debounce: wait 200ms after last move event before re-rendering
50
56
  viewportUpdateTimer = setTimeout(() => {
51
57
  if (!map) return;
@@ -57,86 +63,85 @@
57
63
  viewportVersion++;
58
64
  }, 200);
59
65
  }
60
-
66
+
61
67
  /**
62
68
  * Initialize or reinitialize the cell layers
63
69
  * Called on mount and when map style changes
64
70
  */
65
71
  function initializeLayer() {
66
72
  if (!map) return;
67
-
73
+
68
74
  console.log('CellsLayer: initializeLayer called');
69
-
75
+
70
76
  // Set initial zoom
71
77
  store.setCurrentZoom(map.getZoom());
72
-
78
+
73
79
  // Add moveend listener if not already added
74
80
  // Note: We need to be careful not to duplicate listeners
75
81
  map.off('moveend', handleMoveEnd); // Remove if exists
76
- map.on('moveend', handleMoveEnd); // Add it back
77
-
82
+ map.on('moveend', handleMoveEnd); // Add it back
83
+
78
84
  // Mark as mounted to trigger $effect
79
85
  mounted = true;
80
-
86
+
81
87
  // Force $effect to re-run by incrementing viewportVersion
82
88
  // This is critical after style.load when layers need to be recreated
83
89
  // but mounted/map haven't changed (they're already true/set)
84
90
  viewportVersion++;
85
91
  }
86
-
92
+
87
93
  onMount(() => {
88
94
  console.log('CellsLayer: onMount, waiting for map...');
89
-
95
+
90
96
  // Subscribe to map store
91
97
  const unsubscribe = mapStore.subscribe((mapInstance) => {
92
98
  if (mapInstance && !map) {
93
99
  console.log('CellsLayer: Map available, initializing...');
94
100
  map = mapInstance;
95
-
101
+
96
102
  // Initial layer setup
97
103
  initializeLayer();
98
-
104
+
99
105
  // Re-initialize layer when map style changes
100
106
  // Mapbox removes all custom layers/sources when setStyle() is called
101
107
  map.on('style.load', initializeLayer);
102
108
  }
103
109
  });
104
-
110
+
105
111
  return () => {
106
112
  unsubscribe();
107
113
  };
108
114
  });
109
-
115
+
110
116
  onDestroy(() => {
111
117
  if (!map) return;
112
-
118
+
113
119
  // Clean up timer
114
120
  if (viewportUpdateTimer) {
115
121
  clearTimeout(viewportUpdateTimer);
116
122
  }
117
-
123
+
118
124
  // Remove style.load listener
119
125
  map.off('style.load', initializeLayer);
120
-
126
+
121
127
  map.off('moveend', handleMoveEnd);
122
-
128
+
123
129
  // Clean up layers and source
124
- if (map.getLayer(LINE_LAYER_ID)) {
125
- map.removeLayer(LINE_LAYER_ID);
126
- }
127
- if (map.getLayer(FILL_LAYER_ID)) {
128
- map.removeLayer(FILL_LAYER_ID);
129
- }
130
+ allLayerIds.forEach((id) => {
131
+ if (map.getLayer(id)) {
132
+ map.removeLayer(id);
133
+ }
134
+ });
130
135
  if (map.getSource(SOURCE_ID)) {
131
136
  map.removeSource(SOURCE_ID);
132
137
  }
133
138
  });
134
-
139
+
135
140
  // Reactive: Update GeoJSON when cells/zoom/filters/settings change
136
141
  $effect(() => {
137
142
  // Track viewportVersion to force re-run on pan (even without zoom change)
138
143
  viewportVersion;
139
-
144
+
140
145
  console.log('CellsLayer $effect triggered:', {
141
146
  mounted,
142
147
  hasMap: !!map,
@@ -146,35 +151,35 @@
146
151
  baseRadius: store.baseRadius,
147
152
  viewportVersion
148
153
  });
149
-
150
- if (!mounted || !map || !store.showCells) {
154
+
155
+ if (!mounted || !map) {
156
+ return;
157
+ }
158
+
159
+ if (!store.showCells) {
151
160
  // Remove layers if showCells is false
152
- if (mounted && map) {
153
- console.log('CellsLayer: Removing layers (showCells=false or not mounted)');
154
- if (map.getLayer(LINE_LAYER_ID)) {
155
- map.removeLayer(LINE_LAYER_ID);
156
- }
157
- if (map.getLayer(FILL_LAYER_ID)) {
158
- map.removeLayer(FILL_LAYER_ID);
159
- }
160
- if (map.getSource(SOURCE_ID)) {
161
- map.removeSource(SOURCE_ID);
161
+ allLayerIds.forEach((id) => {
162
+ if (map.getLayer(id)) {
163
+ map.removeLayer(id);
162
164
  }
165
+ });
166
+ if (map.getSource(SOURCE_ID)) {
167
+ map.removeSource(SOURCE_ID);
163
168
  }
164
169
  return;
165
170
  }
166
-
171
+
167
172
  // Filter cells by viewport bounds (only render visible cells)
168
173
  const bounds = map.getBounds();
169
174
  if (!bounds) {
170
175
  console.warn('CellsLayer: Cannot get map bounds, skipping viewport filter');
171
176
  return;
172
177
  }
173
-
174
- const visibleCells = store.filteredCells.filter(cell =>
178
+
179
+ const visibleCells = store.filteredCells.filter((cell) =>
175
180
  bounds.contains([cell.longitude, cell.latitude])
176
181
  );
177
-
182
+
178
183
  console.log('CellsLayer: Viewport filtering:', {
179
184
  totalFiltered: store.filteredCells.length,
180
185
  visibleInViewport: visibleCells.length,
@@ -185,7 +190,7 @@
185
190
  west: bounds.getWest()
186
191
  }
187
192
  });
188
-
193
+
189
194
  // Generate GeoJSON from visible cells only
190
195
  const geoJSON = cellsToGeoJSON(
191
196
  visibleCells,
@@ -194,12 +199,12 @@
194
199
  store.groupColorMap,
195
200
  store.cellGroupMap // Pass lookup map for O(1) color lookup
196
201
  );
197
-
202
+
198
203
  console.log('CellsLayer: Generated GeoJSON:', {
199
204
  featureCount: geoJSON.features.length,
200
205
  firstFeature: geoJSON.features[0]
201
206
  });
202
-
207
+
203
208
  // Update or create source
204
209
  const source = map.getSource(SOURCE_ID);
205
210
  if (source && source.type === 'geojson') {
@@ -212,60 +217,76 @@
212
217
  data: geoJSON
213
218
  });
214
219
  }
215
-
216
- // Add fill layer if not exists
217
- if (!map.getLayer(FILL_LAYER_ID)) {
218
- console.log('CellsLayer: Creating fill layer');
219
- map.addLayer({
220
- id: FILL_LAYER_ID,
221
- type: 'fill',
222
- source: SOURCE_ID,
223
- paint: {
224
- 'fill-color': [
225
- 'coalesce',
226
- ['get', 'groupColor'],
227
- ['get', 'techBandColor'],
228
- '#888888' // Fallback
229
- ],
230
- 'fill-opacity': store.fillOpacity
231
- },
232
- metadata: {
233
- zIndex: CELL_FILL_Z_INDEX
234
- }
235
- });
236
- } else {
237
- // Update fill opacity
238
- console.log('CellsLayer: Updating fill opacity:', store.fillOpacity);
239
- map.setPaintProperty(FILL_LAYER_ID, 'fill-opacity', store.fillOpacity);
240
- }
241
-
242
- // Add line layer if not exists
243
- if (!map.getLayer(LINE_LAYER_ID)) {
244
- console.log('CellsLayer: Creating line layer');
245
- map.addLayer({
246
- id: LINE_LAYER_ID,
247
- type: 'line',
248
- source: SOURCE_ID,
249
- paint: {
250
- 'line-color': ['get', 'lineColor'],
251
- 'line-width': store.lineWidth,
252
- 'line-opacity': ['get', 'lineOpacity'],
253
- 'line-dasharray': [
254
- 'case',
255
- ['has', 'dashArray'],
256
- ['get', 'dashArray'],
257
- ['literal', [1, 0]] // Solid line default
258
- ]
259
- },
260
- metadata: {
261
- zIndex: CELL_LINE_Z_INDEX
262
- }
263
- });
264
- } else {
265
- // Update line width
266
- console.log('CellsLayer: Updating line width:', store.lineWidth);
267
- map.setPaintProperty(LINE_LAYER_ID, 'line-width', store.lineWidth);
220
+
221
+ // Get all unique bands for the current z-index
222
+ const bandsByZIndex = new Map<number, string[]>();
223
+ for (const [band, zIndex] of Object.entries(Z_INDEX_BY_BAND)) {
224
+ if (!bandsByZIndex.has(zIndex)) {
225
+ bandsByZIndex.set(zIndex, []);
226
+ }
227
+ bandsByZIndex.get(zIndex)?.push(band);
268
228
  }
229
+
230
+ // Add/update layers for each z-index level
231
+ sortedZIndexes.forEach((zIndex) => {
232
+ const fillLayerId = `${namespace}-cells-fill-${zIndex}`;
233
+ const lineLayerId = `${namespace}-cells-line-${zIndex}`;
234
+ const bands = bandsByZIndex.get(zIndex) || [];
235
+
236
+ // Add fill layer if not exists
237
+ if (!map.getLayer(fillLayerId)) {
238
+ console.log(`CellsLayer: Creating fill layer for z-index ${zIndex}`);
239
+ map.addLayer({
240
+ id: fillLayerId,
241
+ type: 'fill',
242
+ source: SOURCE_ID,
243
+ filter: ['in', ['get', 'techBandKey'], ['literal', bands]],
244
+ paint: {
245
+ 'fill-color': [
246
+ 'coalesce',
247
+ ['get', 'groupColor'],
248
+ ['get', 'techBandColor'],
249
+ '#888888' // Fallback
250
+ ],
251
+ 'fill-opacity': store.fillOpacity
252
+ },
253
+ metadata: {
254
+ zIndex: CELL_FILL_Z_INDEX + zIndex
255
+ }
256
+ });
257
+ } else {
258
+ // Update fill opacity
259
+ map.setPaintProperty(fillLayerId, 'fill-opacity', store.fillOpacity);
260
+ }
261
+
262
+ // Add line layer if not exists
263
+ if (!map.getLayer(lineLayerId)) {
264
+ console.log(`CellsLayer: Creating line layer for z-index ${zIndex}`);
265
+ map.addLayer({
266
+ id: lineLayerId,
267
+ type: 'line',
268
+ source: SOURCE_ID,
269
+ filter: ['in', ['get', 'techBandKey'], ['literal', bands]],
270
+ paint: {
271
+ 'line-color': ['get', 'lineColor'],
272
+ 'line-width': store.lineWidth,
273
+ 'line-opacity': ['get', 'lineOpacity'],
274
+ 'line-dasharray': [
275
+ 'case',
276
+ ['has', 'dashArray'],
277
+ ['get', 'dashArray'],
278
+ ['literal', [1, 0]] // Solid line default
279
+ ]
280
+ },
281
+ metadata: {
282
+ zIndex: CELL_LINE_Z_INDEX + zIndex
283
+ }
284
+ });
285
+ } else {
286
+ // Update line width
287
+ map.setPaintProperty(lineLayerId, 'line-width', store.lineWidth);
288
+ }
289
+ });
269
290
  });
270
291
  </script>
271
292
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { TECHNOLOGY_BAND_COLORS } from '../constants/colors';
7
7
  import { DEFAULT_STATUS_STYLES } from '../constants/statusStyles';
8
+ import { Z_INDEX_BY_BAND } from '../constants/zIndex';
8
9
  import { calculateRadius } from './zoomScaling';
9
10
  import { createArcPolygon } from './arcGeometry';
10
11
  /**
@@ -61,6 +62,7 @@ export function cellsToGeoJSON(cells, currentZoom, baseRadius = 500, groupColorM
61
62
  techBandKey,
62
63
  techBandColor: defaultColor, // Default color from constants
63
64
  groupColor: groupColor, // Custom color (if set)
65
+ zIndex: Z_INDEX_BY_BAND[techBandKey] || 0, // Z-index for layer ordering
64
66
  // Line (border) styling from status
65
67
  lineColor: statusStyle.lineColor,
66
68
  lineWidth: statusStyle.lineWidth,
@@ -1,25 +1,12 @@
1
1
  /**
2
2
  * Z-Index ordering for repeater tech-band combinations
3
3
  *
4
+ * This uses the same Z_INDEX_BY_BAND constant as cells for consistency.
4
5
  * Higher frequency bands are rendered on top when repeaters overlap.
5
- * This follows the same pattern as cells.
6
6
  */
7
+ import type { TechnologyBandKey } from '../../cells/types';
7
8
  /**
8
- * Z-index ordering for tech:fband combinations
9
+ * Shared z-index ordering by technology-band key
9
10
  * Higher number = rendered on top
10
- *
11
- * Organized by frequency (low to high):
12
- * - Lower frequencies (700-900) at bottom
13
- * - Mid frequencies (1800-2100) in middle
14
- * - Higher frequencies (2600-3500) on top
15
- */
16
- export declare const REPEATER_TECH_BAND_Z_ORDER: Record<string, number>;
17
- /**
18
- * Get z-order index for a tech:fband combination
19
- * Used to set the sort key for layering overlapping repeaters
20
- *
21
- * @param tech - Technology (2G, 4G, 5G)
22
- * @param fband - Frequency band (e.g., "LTE1800", "GSM900")
23
- * @returns Z-order index (defaults to 0 if not found)
24
11
  */
25
- export declare function getRepeaterZOrder(tech: string, fband: string): number;
12
+ export declare const Z_INDEX_BY_BAND: Record<TechnologyBandKey, number>;
@@ -1,43 +1,26 @@
1
1
  /**
2
2
  * Z-Index ordering for repeater tech-band combinations
3
3
  *
4
+ * This uses the same Z_INDEX_BY_BAND constant as cells for consistency.
4
5
  * Higher frequency bands are rendered on top when repeaters overlap.
5
- * This follows the same pattern as cells.
6
6
  */
7
7
  /**
8
- * Z-index ordering for tech:fband combinations
8
+ * Shared z-index ordering by technology-band key
9
9
  * Higher number = rendered on top
10
- *
11
- * Organized by frequency (low to high):
12
- * - Lower frequencies (700-900) at bottom
13
- * - Mid frequencies (1800-2100) in middle
14
- * - Higher frequencies (2600-3500) on top
15
10
  */
16
- export const REPEATER_TECH_BAND_Z_ORDER = {
17
- // 2G bands (lowest)
18
- '2G:GSM900': 1,
19
- '2G:GSM1800': 2,
20
- // 4G bands (medium)
21
- '4G:LTE700': 3,
22
- '4G:LTE800': 4,
23
- '4G:LTE900': 5,
24
- '4G:LTE1800': 6,
25
- '4G:LTE2100': 7,
26
- '4G:LTE2600': 8,
27
- // 5G bands (highest)
28
- '5G:5G-700': 9,
29
- '5G:5G-2100': 10,
30
- '5G:5G-3500': 11,
11
+ export const Z_INDEX_BY_BAND = {
12
+ // 2G bands
13
+ '2G_900': 5,
14
+ '2G_1800': 4,
15
+ // 4G bands
16
+ '4G_700': 2,
17
+ '4G_800': 3,
18
+ '4G_900': 4,
19
+ '4G_1800': 8,
20
+ '4G_2100': 9,
21
+ '4G_2600': 11,
22
+ // 5G bands
23
+ '5G_700': 9,
24
+ '5G_2100': 10,
25
+ '5G_3500': 12
31
26
  };
32
- /**
33
- * Get z-order index for a tech:fband combination
34
- * Used to set the sort key for layering overlapping repeaters
35
- *
36
- * @param tech - Technology (2G, 4G, 5G)
37
- * @param fband - Frequency band (e.g., "LTE1800", "GSM900")
38
- * @returns Z-order index (defaults to 0 if not found)
39
- */
40
- export function getRepeaterZOrder(tech, fband) {
41
- const key = `${tech}:${fband}`;
42
- return REPEATER_TECH_BAND_Z_ORDER[key] ?? 0;
43
- }
@@ -13,4 +13,4 @@ export { repeatersToGeoJSON } from './utils/repeaterGeoJSON';
13
13
  export { buildRepeaterTree, getFilteredRepeaters } from './utils/repeaterTree';
14
14
  export { REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX } from './constants/zIndex';
15
15
  export { REPEATER_RADIUS_MULTIPLIER, getRepeaterRadiusMultiplier } from './constants/radiusMultipliers';
16
- export { REPEATER_TECH_BAND_Z_ORDER, getRepeaterZOrder } from './constants/techBandZOrder';
16
+ export { Z_INDEX_BY_BAND } from './constants/techBandZOrder';
@@ -16,4 +16,4 @@ export { buildRepeaterTree, getFilteredRepeaters } from './utils/repeaterTree';
16
16
  // Constants
17
17
  export { REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX } from './constants/zIndex';
18
18
  export { REPEATER_RADIUS_MULTIPLIER, getRepeaterRadiusMultiplier } from './constants/radiusMultipliers';
19
- export { REPEATER_TECH_BAND_Z_ORDER, getRepeaterZOrder } from './constants/techBandZOrder';
19
+ export { Z_INDEX_BY_BAND } from './constants/techBandZOrder';
@@ -17,6 +17,7 @@
17
17
  import { useMapbox } from '../../../core/hooks/useMapbox';
18
18
  import { repeatersToGeoJSON } from '../utils/repeaterGeoJSON';
19
19
  import { REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX } from '../constants/zIndex';
20
+ import { Z_INDEX_BY_BAND } from '../constants/techBandZOrder';
20
21
 
21
22
  interface Props {
22
23
  /** Repeater store context */
@@ -26,28 +27,34 @@
26
27
  }
27
28
 
28
29
  let { store, namespace }: Props = $props();
29
-
30
- const FILL_LAYER_ID = `${namespace}-repeaters-fill`;
31
- const LINE_LAYER_ID = `${namespace}-repeaters-line`;
30
+
32
31
  const SOURCE_ID = `${namespace}-repeaters`;
33
-
32
+
34
33
  // Get map from mapbox hook
35
34
  const mapStore = useMapbox();
36
-
35
+
37
36
  let map = $state<MapboxMap | null>(null);
38
37
  let mounted = $state(false);
39
38
  let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
40
39
  let viewportVersion = $state(0); // Increment to force $effect re-run on viewport changes
41
-
40
+
41
+ // Get all unique z-index values and sort them
42
+ const sortedZIndexes = [...new Set(Object.values(Z_INDEX_BY_BAND))].sort((a, b) => a - b);
43
+
44
+ // Create layer IDs for each z-index
45
+ const fillLayerIds = sortedZIndexes.map((zIndex) => `${namespace}-repeaters-fill-${zIndex}`);
46
+ const lineLayerIds = sortedZIndexes.map((zIndex) => `${namespace}-repeaters-line-${zIndex}`);
47
+ const allLayerIds = [...fillLayerIds, ...lineLayerIds];
48
+
42
49
  // Viewport change handler with debouncing
43
50
  function handleMoveEnd() {
44
51
  if (!map) return;
45
-
52
+
46
53
  // Clear any existing timer
47
54
  if (viewportUpdateTimer) {
48
55
  clearTimeout(viewportUpdateTimer);
49
56
  }
50
-
57
+
51
58
  // Debounce: wait 200ms after last move event before re-rendering
52
59
  viewportUpdateTimer = setTimeout(() => {
53
60
  if (!map) return;
@@ -59,73 +66,72 @@
59
66
  viewportVersion++;
60
67
  }, 200);
61
68
  }
62
-
69
+
63
70
  /**
64
71
  * Initialize or reinitialize the repeater layers
65
72
  * Called on mount and when map style changes
66
73
  */
67
74
  function initializeLayer() {
68
75
  if (!map) return;
69
-
76
+
70
77
  console.log('RepeatersLayer: initializeLayer called');
71
-
78
+
72
79
  // Set initial zoom
73
80
  store.setCurrentZoom(map.getZoom());
74
-
81
+
75
82
  // Add moveend listener (remove first to avoid duplicates)
76
83
  map.off('moveend', handleMoveEnd);
77
84
  map.on('moveend', handleMoveEnd);
78
-
85
+
79
86
  // Mark as mounted to trigger $effect
80
87
  mounted = true;
81
-
88
+
82
89
  // Force $effect to re-run
83
90
  viewportVersion++;
84
91
  }
85
-
92
+
86
93
  onMount(() => {
87
94
  console.log('RepeatersLayer: onMount, waiting for map...');
88
-
95
+
89
96
  // Subscribe to map store
90
97
  const unsubscribe = mapStore.subscribe((mapInstance) => {
91
98
  if (mapInstance && !map) {
92
99
  console.log('RepeatersLayer: Map available, initializing...');
93
100
  map = mapInstance;
94
-
101
+
95
102
  // Initial layer setup
96
103
  initializeLayer();
97
-
104
+
98
105
  // Re-initialize layer when map style changes
99
106
  // Mapbox removes all custom layers/sources when setStyle() is called
100
107
  map.on('style.load', initializeLayer);
101
108
  }
102
109
  });
103
-
110
+
104
111
  return () => {
105
112
  unsubscribe();
106
113
  };
107
114
  });
108
-
115
+
109
116
  onDestroy(() => {
110
117
  if (!map) return;
111
-
118
+
112
119
  // Clean up timer
113
120
  if (viewportUpdateTimer) {
114
121
  clearTimeout(viewportUpdateTimer);
115
122
  }
116
-
123
+
117
124
  // Remove style.load listener
118
125
  map.off('style.load', initializeLayer);
119
-
126
+
120
127
  map.off('moveend', handleMoveEnd);
121
-
128
+
122
129
  // Clean up layers and source
123
- if (map.getLayer(LINE_LAYER_ID)) {
124
- map.removeLayer(LINE_LAYER_ID);
125
- }
126
- if (map.getLayer(FILL_LAYER_ID)) {
127
- map.removeLayer(FILL_LAYER_ID);
128
- }
130
+ allLayerIds.forEach((id) => {
131
+ if (map.getLayer(id)) {
132
+ map.removeLayer(id);
133
+ }
134
+ });
129
135
  if (map.getSource(SOURCE_ID)) {
130
136
  map.removeSource(SOURCE_ID);
131
137
  }
@@ -135,7 +141,7 @@
135
141
  $effect(() => {
136
142
  // Track viewportVersion to force re-run on pan
137
143
  viewportVersion;
138
-
144
+
139
145
  console.log('RepeatersLayer $effect triggered:', {
140
146
  mounted,
141
147
  hasMap: !!map,
@@ -145,40 +151,40 @@
145
151
  baseRadius: store.baseRadius,
146
152
  viewportVersion
147
153
  });
148
-
149
- if (!mounted || !map || !store.showRepeaters) {
154
+
155
+ if (!mounted || !map) {
156
+ return;
157
+ }
158
+
159
+ if (!store.showRepeaters) {
150
160
  // Remove layers if showRepeaters is false
151
- if (mounted && map) {
152
- console.log('RepeatersLayer: Removing layers (showRepeaters=false or not mounted)');
153
- if (map.getLayer(LINE_LAYER_ID)) {
154
- map.removeLayer(LINE_LAYER_ID);
155
- }
156
- if (map.getLayer(FILL_LAYER_ID)) {
157
- map.removeLayer(FILL_LAYER_ID);
158
- }
159
- if (map.getSource(SOURCE_ID)) {
160
- map.removeSource(SOURCE_ID);
161
+ allLayerIds.forEach((id) => {
162
+ if (map.getLayer(id)) {
163
+ map.removeLayer(id);
161
164
  }
165
+ });
166
+ if (map.getSource(SOURCE_ID)) {
167
+ map.removeSource(SOURCE_ID);
162
168
  }
163
169
  return;
164
170
  }
165
-
171
+
166
172
  // Filter repeaters by viewport bounds (only render visible repeaters)
167
173
  const bounds = map.getBounds();
168
174
  if (!bounds) {
169
175
  console.warn('RepeatersLayer: Cannot get map bounds, skipping viewport filter');
170
176
  return;
171
177
  }
172
-
173
- const visibleRepeaters = store.filteredRepeaters.filter(repeater =>
178
+
179
+ const visibleRepeaters = store.filteredRepeaters.filter((repeater) =>
174
180
  bounds.contains([repeater.longitude, repeater.latitude])
175
181
  );
176
-
182
+
177
183
  console.log('RepeatersLayer: Viewport filtering:', {
178
184
  totalFiltered: store.filteredRepeaters.length,
179
185
  visibleInViewport: visibleRepeaters.length
180
186
  });
181
-
187
+
182
188
  // Generate GeoJSON from visible repeaters only
183
189
  const geoJSON = repeatersToGeoJSON(
184
190
  visibleRepeaters,
@@ -186,12 +192,12 @@
186
192
  store.baseRadius,
187
193
  store.techBandColorMap
188
194
  );
189
-
195
+
190
196
  console.log('RepeatersLayer: Generated GeoJSON:', {
191
197
  featureCount: geoJSON.features.length,
192
198
  firstFeature: geoJSON.features[0]
193
199
  });
194
-
200
+
195
201
  // Update or create source
196
202
  const source = map.getSource(SOURCE_ID);
197
203
  if (source && source.type === 'geojson') {
@@ -204,55 +210,65 @@
204
210
  data: geoJSON
205
211
  });
206
212
  }
207
-
208
- // Add fill layer if not exists
209
- if (!map.getLayer(FILL_LAYER_ID)) {
210
- console.log('RepeatersLayer: Creating fill layer');
211
- map.addLayer({
212
- id: FILL_LAYER_ID,
213
- type: 'fill',
214
- source: SOURCE_ID,
215
- layout: {
216
- 'fill-sort-key': ['get', 'sortKey'] // Use sortKey for z-ordering
217
- },
218
- paint: {
219
- 'fill-color': ['get', 'techBandColor'],
220
- 'fill-opacity': store.fillOpacity
221
- },
222
- metadata: {
223
- zIndex: REPEATER_FILL_Z_INDEX
224
- }
225
- });
226
- } else {
227
- // Update fill opacity
228
- console.log('RepeatersLayer: Updating fill opacity:', store.fillOpacity);
229
- map.setPaintProperty(FILL_LAYER_ID, 'fill-opacity', store.fillOpacity);
230
- }
231
-
232
- // Add line layer if not exists
233
- if (!map.getLayer(LINE_LAYER_ID)) {
234
- console.log('RepeatersLayer: Creating line layer');
235
- map.addLayer({
236
- id: LINE_LAYER_ID,
237
- type: 'line',
238
- source: SOURCE_ID,
239
- layout: {
240
- 'line-sort-key': ['get', 'sortKey'] // Use sortKey for z-ordering
241
- },
242
- paint: {
243
- 'line-color': ['get', 'lineColor'],
244
- 'line-width': store.lineWidth,
245
- 'line-opacity': ['get', 'lineOpacity']
246
- },
247
- metadata: {
248
- zIndex: REPEATER_LINE_Z_INDEX
249
- }
250
- });
251
- } else {
252
- // Update line width
253
- console.log('RepeatersLayer: Updating line width:', store.lineWidth);
254
- map.setPaintProperty(LINE_LAYER_ID, 'line-width', store.lineWidth);
213
+
214
+ // Get all unique bands for the current z-index
215
+ const bandsByZIndex = new Map<number, string[]>();
216
+ for (const [band, zIndex] of Object.entries(Z_INDEX_BY_BAND)) {
217
+ if (!bandsByZIndex.has(zIndex)) {
218
+ bandsByZIndex.set(zIndex, []);
219
+ }
220
+ bandsByZIndex.get(zIndex)?.push(band);
255
221
  }
222
+
223
+ // Add/update layers for each z-index level
224
+ sortedZIndexes.forEach((zIndex) => {
225
+ const fillLayerId = `${namespace}-repeaters-fill-${zIndex}`;
226
+ const lineLayerId = `${namespace}-repeaters-line-${zIndex}`;
227
+ const bands = bandsByZIndex.get(zIndex) || [];
228
+
229
+ // Add fill layer if not exists
230
+ if (!map.getLayer(fillLayerId)) {
231
+ console.log(`RepeatersLayer: Creating fill layer for z-index ${zIndex}`);
232
+ map.addLayer({
233
+ id: fillLayerId,
234
+ type: 'fill',
235
+ source: SOURCE_ID,
236
+ filter: ['in', ['get', 'techBandKey'], ['literal', bands]],
237
+ paint: {
238
+ 'fill-color': ['get', 'techBandColor'],
239
+ 'fill-opacity': store.fillOpacity
240
+ },
241
+ metadata: {
242
+ zIndex: REPEATER_FILL_Z_INDEX + zIndex
243
+ }
244
+ });
245
+ } else {
246
+ // Update fill opacity
247
+ map.setPaintProperty(fillLayerId, 'fill-opacity', store.fillOpacity);
248
+ }
249
+
250
+ // Add line layer if not exists
251
+ if (!map.getLayer(lineLayerId)) {
252
+ console.log(`RepeatersLayer: Creating line layer for z-index ${zIndex}`);
253
+ map.addLayer({
254
+ id: lineLayerId,
255
+ type: 'line',
256
+ source: SOURCE_ID,
257
+ filter: ['in', ['get', 'techBandKey'], ['literal', bands]],
258
+ paint: {
259
+ 'line-color': ['get', 'lineColor'],
260
+ 'line-width': store.lineWidth,
261
+ 'line-opacity': ['get', 'lineOpacity']
262
+ },
263
+ metadata: {
264
+ zIndex: REPEATER_LINE_Z_INDEX + zIndex
265
+ }
266
+ });
267
+ } else {
268
+ // Update line width
269
+ map.setPaintProperty(lineLayerId, 'line-width', store.lineWidth);
270
+ }
271
+ });
256
272
  });
257
273
  </script>
258
274
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { createArcPolygon } from '../../cells/utils/arcGeometry';
10
10
  import { getRepeaterRadiusMultiplier } from '../constants/radiusMultipliers';
11
- import { getRepeaterZOrder } from '../constants/techBandZOrder';
11
+ import { Z_INDEX_BY_BAND } from '../constants/techBandZOrder';
12
12
  /** Fixed beamwidth for all repeaters */
13
13
  const REPEATER_BEAMWIDTH = 30;
14
14
  /** Reference zoom level where base radius is used as-is */
@@ -26,6 +26,18 @@ const REFERENCE_ZOOM = 12;
26
26
  function getZoomFactor(currentZoom) {
27
27
  return Math.pow(2, (REFERENCE_ZOOM - currentZoom) / 2);
28
28
  }
29
+ /**
30
+ * Parse repeater tech and fband into a TechnologyBandKey
31
+ * Repeaters use format like tech="4G", fband="LTE1800" or "1800"
32
+ * We need to convert to "4G_1800" format
33
+ */
34
+ function parseTechBand(tech, fband) {
35
+ // Extract numeric band from fband (handles "LTE1800", "1800", "GSM900", "900", etc.)
36
+ const bandMatch = fband.match(/(\d+)/);
37
+ const band = bandMatch ? bandMatch[1] : fband;
38
+ // Build TechnologyBandKey
39
+ return `${tech}_${band}`;
40
+ }
29
41
  /**
30
42
  * Convert repeaters to GeoJSON FeatureCollection with arc geometries
31
43
  *
@@ -37,16 +49,18 @@ function getZoomFactor(currentZoom) {
37
49
  */
38
50
  export function repeatersToGeoJSON(repeaters, currentZoom, baseRadius = 500, techBandColorMap) {
39
51
  const features = repeaters.map((repeater) => {
40
- // Get color for this tech:fband combination
41
- const key = `${repeater.tech}:${repeater.fband}`;
42
- const color = techBandColorMap.get(key) || '#888888'; // Fallback gray
52
+ // Get color for this tech:fband combination (old format for color map)
53
+ const colorKey = `${repeater.tech}:${repeater.fband}`;
54
+ const color = techBandColorMap.get(colorKey) || '#888888'; // Fallback gray
55
+ // Parse to TechnologyBandKey for z-index lookup
56
+ const techBandKey = parseTechBand(repeater.tech, repeater.fband);
43
57
  // Calculate zoom-reactive radius with inverse scaling (like cells)
44
58
  const zoomFactor = getZoomFactor(currentZoom);
45
59
  // Apply tech-band specific radius multiplier
46
60
  const techBandMultiplier = getRepeaterRadiusMultiplier(repeater.tech, repeater.fband);
47
61
  const radius = baseRadius * zoomFactor * techBandMultiplier;
48
- // Get z-order for layering (higher frequency = higher z-order = on top)
49
- const zOrder = getRepeaterZOrder(repeater.tech, repeater.fband);
62
+ // Get z-order for layering using shared constant
63
+ const zIndex = Z_INDEX_BY_BAND[techBandKey] || 0;
50
64
  // Create arc polygon geometry with fixed 30° beamwidth
51
65
  const center = [repeater.longitude, repeater.latitude];
52
66
  const arc = createArcPolygon(center, repeater.azimuth, REPEATER_BEAMWIDTH, radius);
@@ -73,13 +87,13 @@ export function repeatersToGeoJSON(repeaters, currentZoom, baseRadius = 500, tec
73
87
  featureGroup: repeater.featureGroup || '',
74
88
  status: repeater.status || 'active',
75
89
  // Styling properties for Mapbox
76
- techBandKey: key,
90
+ techBandKey: techBandKey, // TechnologyBandKey format for consistency
77
91
  techBandColor: color,
78
92
  lineColor: color,
79
93
  lineOpacity: 0.8,
80
- // Z-order for layering (higher frequency on top)
81
- zOrder: zOrder,
82
- sortKey: zOrder // Used by Mapbox for layer ordering
94
+ // Z-index for layering (higher frequency on top)
95
+ zIndex: zIndex,
96
+ sortKey: zIndex // Used by Mapbox for layer ordering
83
97
  }
84
98
  };
85
99
  });
@@ -8,5 +8,5 @@ export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreB
8
8
  export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
9
9
  export { type Site, type SiteStoreValue, type SiteStoreContext, createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSelectionControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
10
10
  export { type Cell, type CellStatus, type CellStatusStyle, type CellGroupingField, type CellGroupingLabels, type CellTreeConfig, type TechnologyBandKey, type CellStoreValue, type CellStoreContext, createCellStoreContext, CellsLayer, CellLabelsLayer, CellFilterControl, CellStyleControl, cellsToGeoJSON, buildCellTree, getFilteredCells, calculateRadius, getZoomFactor, createArcPolygon, DEFAULT_CELL_TREE_CONFIG, TECHNOLOGY_BAND_COLORS, DEFAULT_STATUS_STYLES, RADIUS_MULTIPLIER } from './features/cells';
11
- export { type Repeater, type RepeaterTreeNode, type RepeaterStoreValue, type RepeaterStoreContext, createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, getRepeaterZOrder, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER, REPEATER_TECH_BAND_Z_ORDER } from './features/repeaters';
11
+ export { type Repeater, type RepeaterTreeNode, type RepeaterStoreValue, type RepeaterStoreContext, createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER } from './features/repeaters';
12
12
  export { DemoMap, demoSites, demoCells, demoRepeaters } from './demo';
@@ -23,7 +23,7 @@ export { createCellStoreContext, CellsLayer, CellLabelsLayer, CellFilterControl,
23
23
  // ============================================================================
24
24
  // REPEATER FEATURE
25
25
  // ============================================================================
26
- export { createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, getRepeaterZOrder, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER, REPEATER_TECH_BAND_Z_ORDER } from './features/repeaters';
26
+ export { createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER } from './features/repeaters';
27
27
  // ============================================================================
28
28
  // DEMO
29
29
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -32,13 +32,14 @@
32
32
  }
33
33
  },
34
34
  "peerDependencies": {
35
+ "@turf/turf": "^7.2.0",
35
36
  "bootstrap": "^5.2.3",
37
+ "deck.gl": "^9.1.0",
38
+ "@deck.gl/mapbox": "^9.2.2",
36
39
  "dexie": "^4.0.11",
40
+ "mapbox-gl": "^3.0.0",
37
41
  "plotly.js-dist-min": "^3.1.0",
38
- "svelte": "^5.0.0",
39
- "@turf/turf": "^7.2.0",
40
- "deck.gl": "^9.1.0",
41
- "mapbox-gl": "^3.0.0"
42
+ "svelte": "^5.0.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@eslint/compat": "^1.2.5",
@@ -68,6 +69,5 @@
68
69
  },
69
70
  "keywords": [
70
71
  "svelte"
71
- ],
72
- "dependencies": {}
72
+ ]
73
73
  }