@smartnet360/svelte-components 0.0.71 → 0.0.72

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.
Files changed (30) hide show
  1. package/dist/map-v2/demo/DemoMap.svelte +29 -1
  2. package/dist/map-v2/demo/demo-repeaters.d.ts +13 -0
  3. package/dist/map-v2/demo/demo-repeaters.js +74 -0
  4. package/dist/map-v2/features/repeaters/constants/radiusMultipliers.d.ts +26 -0
  5. package/dist/map-v2/features/repeaters/constants/radiusMultipliers.js +40 -0
  6. package/dist/map-v2/features/repeaters/constants/techBandZOrder.d.ts +25 -0
  7. package/dist/map-v2/features/repeaters/constants/techBandZOrder.js +43 -0
  8. package/dist/map-v2/features/repeaters/constants/zIndex.d.ts +11 -0
  9. package/dist/map-v2/features/repeaters/constants/zIndex.js +11 -0
  10. package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +172 -0
  11. package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte.d.ts +18 -0
  12. package/dist/map-v2/features/repeaters/index.d.ts +13 -0
  13. package/dist/map-v2/features/repeaters/index.js +16 -0
  14. package/dist/map-v2/features/repeaters/layers/RepeaterLabelsLayer.svelte +301 -0
  15. package/dist/map-v2/features/repeaters/layers/RepeaterLabelsLayer.svelte.d.ts +10 -0
  16. package/dist/map-v2/features/repeaters/layers/RepeatersLayer.svelte +259 -0
  17. package/dist/map-v2/features/repeaters/layers/RepeatersLayer.svelte.d.ts +10 -0
  18. package/dist/map-v2/features/repeaters/stores/repeaterStoreContext.svelte.d.ts +69 -0
  19. package/dist/map-v2/features/repeaters/stores/repeaterStoreContext.svelte.js +222 -0
  20. package/dist/map-v2/features/repeaters/types.d.ts +59 -0
  21. package/dist/map-v2/features/repeaters/types.js +4 -0
  22. package/dist/map-v2/features/repeaters/utils/repeaterGeoJSON.d.ts +20 -0
  23. package/dist/map-v2/features/repeaters/utils/repeaterGeoJSON.js +90 -0
  24. package/dist/map-v2/features/repeaters/utils/repeaterTree.d.ts +23 -0
  25. package/dist/map-v2/features/repeaters/utils/repeaterTree.js +111 -0
  26. package/dist/map-v2/shared/controls/FeatureSettingsControl.svelte +27 -21
  27. package/dist/map-v2/shared/controls/FeatureSettingsControl.svelte.d.ts +3 -0
  28. package/dist/map-v2/shared/controls/panels/RepeaterSettingsPanel.svelte +280 -5
  29. package/dist/map-v2/shared/controls/panels/RepeaterSettingsPanel.svelte.d.ts +17 -16
  30. package/package.json +1 -1
@@ -0,0 +1,301 @@
1
+ <script lang="ts">
2
+ /**
3
+ * RepeaterLabelsLayer - Renders repeater labels positioned along azimuth
4
+ *
5
+ * Features:
6
+ * - Projects labels along azimuth from repeater location
7
+ * - Configurable label offset (% of arc radius)
8
+ * - Primary + optional secondary field display
9
+ * - Zoom-reactive positioning
10
+ * - Viewport filtering for performance
11
+ */
12
+
13
+ import { onMount, onDestroy } from 'svelte';
14
+ import { useMapbox } from '../../../core/hooks/useMapbox';
15
+ import { waitForStyleLoad, generateLayerId, generateSourceId } from '../../../shared/utils/mapboxHelpers';
16
+ import type { RepeaterStoreContext } from '../stores/repeaterStoreContext.svelte';
17
+ import type { Repeater } from '../types';
18
+ import * as turf from '@turf/turf';
19
+
20
+ interface Props {
21
+ /** Repeater store context */
22
+ store: RepeaterStoreContext;
23
+ /** Unique namespace for layer/source IDs */
24
+ namespace: string;
25
+ }
26
+
27
+ let { store, namespace }: Props = $props();
28
+
29
+ const mapStore = useMapbox();
30
+ const sourceId = generateSourceId(namespace, 'repeater-labels');
31
+ const layerId = generateLayerId(namespace, 'repeater-labels');
32
+
33
+ let map = $state<mapboxgl.Map | null>(null);
34
+ let unsubscribe: (() => void) | null = null;
35
+ let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
36
+ let viewportVersion = $state(0); // Increment to force $effect re-run on viewport changes
37
+
38
+ // Viewport change handler (pan/zoom/move) with debouncing
39
+ function handleViewportChange() {
40
+ if (!map) return;
41
+
42
+ // Debounce viewport updates to avoid excessive re-renders
43
+ if (viewportUpdateTimer) {
44
+ clearTimeout(viewportUpdateTimer);
45
+ }
46
+
47
+ // Wait 150ms after last pan/zoom before updating
48
+ viewportUpdateTimer = setTimeout(() => {
49
+ viewportVersion++;
50
+ }, 150);
51
+ }
52
+
53
+ /**
54
+ * Get label text for a repeater field
55
+ */
56
+ function getRepeaterLabelText(repeater: Repeater, field: keyof Repeater | 'none'): string {
57
+ if (field === 'none') return '';
58
+
59
+ const value = repeater[field];
60
+ return value == null ? '' : String(value);
61
+ }
62
+
63
+ /**
64
+ * Build GeoJSON features for repeater labels
65
+ */
66
+ function buildLabelFeatures(): GeoJSON.FeatureCollection {
67
+ const features: GeoJSON.Feature[] = [];
68
+
69
+ console.log('RepeaterLabelsLayer: Building label features', {
70
+ showLabels: store.showLabels,
71
+ filteredRepeatersCount: store.filteredRepeaters.length,
72
+ hasMap: !!map
73
+ });
74
+
75
+ if (!store.showLabels || store.filteredRepeaters.length === 0 || !map) {
76
+ console.log('RepeaterLabelsLayer: Skipping labels -', {
77
+ showLabels: store.showLabels,
78
+ hasRepeaters: store.filteredRepeaters.length > 0,
79
+ hasMap: !!map
80
+ });
81
+ return { type: 'FeatureCollection', features: [] };
82
+ }
83
+
84
+ // Get current zoom level
85
+ const currentZoom = map.getZoom();
86
+
87
+ // Filter repeaters by viewport bounds
88
+ const bounds = map.getBounds();
89
+ if (!bounds) {
90
+ return { type: 'FeatureCollection', features: [] };
91
+ }
92
+
93
+ const visibleRepeaters = store.filteredRepeaters.filter(repeater =>
94
+ bounds.contains([repeater.longitude, repeater.latitude])
95
+ );
96
+
97
+ visibleRepeaters.forEach((repeater) => {
98
+ // Get label text
99
+ const primaryText = getRepeaterLabelText(repeater, store.primaryLabelField);
100
+ const secondaryText = getRepeaterLabelText(repeater, store.secondaryLabelField);
101
+
102
+ // Build combined label (single line with separator)
103
+ let labelText = primaryText;
104
+ if (secondaryText) {
105
+ labelText += ` | ${secondaryText}`;
106
+ }
107
+
108
+ if (!labelText.trim()) return; // Skip if no text
109
+
110
+ // Calculate zoom-aware radius
111
+ const zoomMultiplier = Math.pow(1.2, currentZoom - 12);
112
+ const repeaterRadius = store.baseRadius * zoomMultiplier;
113
+
114
+ // Position label at a percentage of the arc radius (configurable via labelOffset)
115
+ // labelOffset is treated as a percentage (e.g., 300 = 300% = beyond arc)
116
+ const labelDistance = (repeaterRadius * store.labelOffset) / 100;
117
+
118
+ // Project label position along azimuth from the repeater's location
119
+ const origin = turf.point([repeater.longitude, repeater.latitude]);
120
+ const labelPosition = turf.destination(
121
+ origin,
122
+ labelDistance / 1000, // Convert meters to kilometers
123
+ repeater.azimuth,
124
+ { units: 'kilometers' }
125
+ );
126
+
127
+ features.push({
128
+ type: 'Feature',
129
+ geometry: labelPosition.geometry,
130
+ properties: {
131
+ text: labelText,
132
+ repeaterId: repeater.repeaterId
133
+ }
134
+ });
135
+ });
136
+
137
+ console.log('RepeaterLabelsLayer: Built features', {
138
+ totalRepeaters: store.filteredRepeaters.length,
139
+ visibleInViewport: visibleRepeaters.length,
140
+ featuresGenerated: features.length
141
+ });
142
+
143
+ return { type: 'FeatureCollection', features };
144
+ }
145
+
146
+ /**
147
+ * Update label layer on map
148
+ */
149
+ function updateLabels() {
150
+ if (!map || !map.getSource(sourceId)) return;
151
+
152
+ const geojson = buildLabelFeatures();
153
+
154
+ console.log('RepeaterLabelsLayer: Updating labels', {
155
+ sourceExists: !!map.getSource(sourceId),
156
+ layerExists: !!map.getLayer(layerId),
157
+ featureCount: geojson.features.length,
158
+ currentZoom: map.getZoom(),
159
+ minLabelZoom: store.minLabelZoom,
160
+ labelSize: store.labelSize,
161
+ labelColor: store.labelColor
162
+ });
163
+
164
+ const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
165
+ source.setData(geojson);
166
+
167
+ // Also update paint/layout properties when labels are refreshed
168
+ if (map.getLayer(layerId)) {
169
+ map.setLayoutProperty(layerId, 'text-size', store.labelSize);
170
+ map.setPaintProperty(layerId, 'text-color', store.labelColor);
171
+ map.setPaintProperty(layerId, 'text-halo-color', store.labelHaloColor);
172
+ map.setPaintProperty(layerId, 'text-halo-width', store.labelHaloWidth);
173
+ map.setLayerZoomRange(layerId, store.minLabelZoom, 24);
174
+
175
+ console.log('RepeaterLabelsLayer: Updated layer properties');
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Initialize or reinitialize the label layer
181
+ * Called on mount and when map style changes
182
+ */
183
+ async function initializeLayer() {
184
+ if (!map) return;
185
+
186
+ // Wait for style to load
187
+ await waitForStyleLoad(map);
188
+
189
+ // Add source if missing
190
+ if (!map.getSource(sourceId)) {
191
+ map.addSource(sourceId, {
192
+ type: 'geojson',
193
+ data: { type: 'FeatureCollection', features: [] }
194
+ });
195
+ }
196
+
197
+ // Add label layer if missing
198
+ if (!map.getLayer(layerId)) {
199
+ map.addLayer({
200
+ id: layerId,
201
+ type: 'symbol',
202
+ source: sourceId,
203
+ minzoom: store.minLabelZoom,
204
+ layout: {
205
+ 'text-field': ['get', 'text'],
206
+ 'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
207
+ 'text-size': store.labelSize,
208
+ 'text-anchor': 'center',
209
+ 'text-allow-overlap': true,
210
+ 'text-ignore-placement': false,
211
+ 'text-max-width': 999,
212
+ 'text-justify': 'center'
213
+ },
214
+ paint: {
215
+ 'text-color': store.labelColor,
216
+ 'text-halo-color': store.labelHaloColor,
217
+ 'text-halo-width': store.labelHaloWidth
218
+ }
219
+ });
220
+ }
221
+
222
+ // Add event listeners (remove first to avoid duplicates)
223
+ map.off('zoom', updateLabels);
224
+ map.off('moveend', handleViewportChange);
225
+ map.off('zoomend', handleViewportChange);
226
+
227
+ map.on('zoom', updateLabels);
228
+ map.on('moveend', handleViewportChange);
229
+ map.on('zoomend', handleViewportChange);
230
+
231
+ // Initial render
232
+ updateLabels();
233
+ }
234
+
235
+ onMount(async () => {
236
+ unsubscribe = mapStore.subscribe(async (m) => {
237
+ if (!m) {
238
+ map = null;
239
+ return;
240
+ }
241
+
242
+ map = m;
243
+
244
+ // Initial layer setup
245
+ await initializeLayer();
246
+
247
+ // Re-initialize layer when map style changes
248
+ // Mapbox removes all custom layers/sources when setStyle() is called
249
+ map.on('style.load', initializeLayer);
250
+ });
251
+ });
252
+
253
+ // Watch for changes in store properties and update labels
254
+ $effect(() => {
255
+ // Track viewportVersion to force re-run on pan
256
+ viewportVersion;
257
+
258
+ // Dependencies that should trigger label refresh
259
+ store.filteredRepeaters;
260
+ store.showLabels;
261
+ store.primaryLabelField;
262
+ store.secondaryLabelField;
263
+ store.labelOffset;
264
+ store.baseRadius;
265
+
266
+ // Style properties
267
+ store.labelSize;
268
+ store.labelColor;
269
+ store.labelHaloColor;
270
+ store.labelHaloWidth;
271
+ store.minLabelZoom;
272
+
273
+ updateLabels();
274
+ });
275
+
276
+ onDestroy(() => {
277
+ unsubscribe?.();
278
+
279
+ // Clear any pending viewport update timer
280
+ if (viewportUpdateTimer) {
281
+ clearTimeout(viewportUpdateTimer);
282
+ }
283
+
284
+ if (map) {
285
+ // Remove style.load listener
286
+ map.off('style.load', initializeLayer);
287
+
288
+ // Remove all event listeners
289
+ map.off('zoom', updateLabels);
290
+ map.off('moveend', handleViewportChange);
291
+ map.off('zoomend', handleViewportChange);
292
+
293
+ if (map.getLayer(layerId)) {
294
+ map.removeLayer(layerId);
295
+ }
296
+ if (map.getSource(sourceId)) {
297
+ map.removeSource(sourceId);
298
+ }
299
+ }
300
+ });
301
+ </script>
@@ -0,0 +1,10 @@
1
+ import type { RepeaterStoreContext } from '../stores/repeaterStoreContext.svelte';
2
+ interface Props {
3
+ /** Repeater store context */
4
+ store: RepeaterStoreContext;
5
+ /** Unique namespace for layer/source IDs */
6
+ namespace: string;
7
+ }
8
+ declare const RepeaterLabelsLayer: import("svelte").Component<Props, {}, "">;
9
+ type RepeaterLabelsLayer = ReturnType<typeof RepeaterLabelsLayer>;
10
+ export default RepeaterLabelsLayer;
@@ -0,0 +1,259 @@
1
+ <script lang="ts">
2
+ /**
3
+ * RepeatersLayer - Renders repeater sectors as arc polygons
4
+ *
5
+ * Features:
6
+ * - Fixed 30° beamwidth for all repeaters
7
+ * - Mapbox fill layer (colored by tech:fband)
8
+ * - Mapbox line layer (borders)
9
+ * - Zoom-reactive: Regenerates arcs on zoom
10
+ * - Viewport filtering: Only renders visible repeaters
11
+ * - Style-change resilient: Reinitializes on map style load
12
+ */
13
+
14
+ import { onDestroy, onMount } from 'svelte';
15
+ import type { Map as MapboxMap } from 'mapbox-gl';
16
+ import type { RepeaterStoreContext } from '../stores/repeaterStoreContext.svelte';
17
+ import { useMapbox } from '../../../core/hooks/useMapbox';
18
+ import { repeatersToGeoJSON } from '../utils/repeaterGeoJSON';
19
+ import { REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX } from '../constants/zIndex';
20
+
21
+ interface Props {
22
+ /** Repeater store context */
23
+ store: RepeaterStoreContext;
24
+ /** Unique namespace for layer IDs */
25
+ namespace: string;
26
+ }
27
+
28
+ let { store, namespace }: Props = $props();
29
+
30
+ const FILL_LAYER_ID = `${namespace}-repeaters-fill`;
31
+ const LINE_LAYER_ID = `${namespace}-repeaters-line`;
32
+ const SOURCE_ID = `${namespace}-repeaters`;
33
+
34
+ // Get map from mapbox hook
35
+ const mapStore = useMapbox();
36
+
37
+ let map = $state<MapboxMap | null>(null);
38
+ let mounted = $state(false);
39
+ let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
40
+ let viewportVersion = $state(0); // Increment to force $effect re-run on viewport changes
41
+
42
+ // Viewport change handler with debouncing
43
+ function handleMoveEnd() {
44
+ if (!map) return;
45
+
46
+ // Clear any existing timer
47
+ if (viewportUpdateTimer) {
48
+ clearTimeout(viewportUpdateTimer);
49
+ }
50
+
51
+ // Debounce: wait 200ms after last move event before re-rendering
52
+ viewportUpdateTimer = setTimeout(() => {
53
+ if (!map) return;
54
+ const newZoom = map.getZoom();
55
+ console.log('RepeatersLayer: Viewport changed, updating to zoom:', newZoom);
56
+ // Update zoom
57
+ store.setCurrentZoom(newZoom);
58
+ // Force $effect to re-run
59
+ viewportVersion++;
60
+ }, 200);
61
+ }
62
+
63
+ /**
64
+ * Initialize or reinitialize the repeater layers
65
+ * Called on mount and when map style changes
66
+ */
67
+ function initializeLayer() {
68
+ if (!map) return;
69
+
70
+ console.log('RepeatersLayer: initializeLayer called');
71
+
72
+ // Set initial zoom
73
+ store.setCurrentZoom(map.getZoom());
74
+
75
+ // Add moveend listener (remove first to avoid duplicates)
76
+ map.off('moveend', handleMoveEnd);
77
+ map.on('moveend', handleMoveEnd);
78
+
79
+ // Mark as mounted to trigger $effect
80
+ mounted = true;
81
+
82
+ // Force $effect to re-run
83
+ viewportVersion++;
84
+ }
85
+
86
+ onMount(() => {
87
+ console.log('RepeatersLayer: onMount, waiting for map...');
88
+
89
+ // Subscribe to map store
90
+ const unsubscribe = mapStore.subscribe((mapInstance) => {
91
+ if (mapInstance && !map) {
92
+ console.log('RepeatersLayer: Map available, initializing...');
93
+ map = mapInstance;
94
+
95
+ // Initial layer setup
96
+ initializeLayer();
97
+
98
+ // Re-initialize layer when map style changes
99
+ // Mapbox removes all custom layers/sources when setStyle() is called
100
+ map.on('style.load', initializeLayer);
101
+ }
102
+ });
103
+
104
+ return () => {
105
+ unsubscribe();
106
+ };
107
+ });
108
+
109
+ onDestroy(() => {
110
+ if (!map) return;
111
+
112
+ // Clean up timer
113
+ if (viewportUpdateTimer) {
114
+ clearTimeout(viewportUpdateTimer);
115
+ }
116
+
117
+ // Remove style.load listener
118
+ map.off('style.load', initializeLayer);
119
+
120
+ map.off('moveend', handleMoveEnd);
121
+
122
+ // 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
+ }
129
+ if (map.getSource(SOURCE_ID)) {
130
+ map.removeSource(SOURCE_ID);
131
+ }
132
+ });
133
+
134
+ // Reactive: Update GeoJSON when repeaters/zoom/settings change
135
+ $effect(() => {
136
+ // Track viewportVersion to force re-run on pan
137
+ viewportVersion;
138
+
139
+ console.log('RepeatersLayer $effect triggered:', {
140
+ mounted,
141
+ hasMap: !!map,
142
+ showRepeaters: store.showRepeaters,
143
+ filteredRepeatersCount: store.filteredRepeaters.length,
144
+ currentZoom: store.currentZoom,
145
+ baseRadius: store.baseRadius,
146
+ viewportVersion
147
+ });
148
+
149
+ if (!mounted || !map || !store.showRepeaters) {
150
+ // 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
+ }
162
+ }
163
+ return;
164
+ }
165
+
166
+ // Filter repeaters by viewport bounds (only render visible repeaters)
167
+ const bounds = map.getBounds();
168
+ if (!bounds) {
169
+ console.warn('RepeatersLayer: Cannot get map bounds, skipping viewport filter');
170
+ return;
171
+ }
172
+
173
+ const visibleRepeaters = store.filteredRepeaters.filter(repeater =>
174
+ bounds.contains([repeater.longitude, repeater.latitude])
175
+ );
176
+
177
+ console.log('RepeatersLayer: Viewport filtering:', {
178
+ totalFiltered: store.filteredRepeaters.length,
179
+ visibleInViewport: visibleRepeaters.length
180
+ });
181
+
182
+ // Generate GeoJSON from visible repeaters only
183
+ const geoJSON = repeatersToGeoJSON(
184
+ visibleRepeaters,
185
+ store.currentZoom,
186
+ store.baseRadius,
187
+ store.techBandColorMap
188
+ );
189
+
190
+ console.log('RepeatersLayer: Generated GeoJSON:', {
191
+ featureCount: geoJSON.features.length,
192
+ firstFeature: geoJSON.features[0]
193
+ });
194
+
195
+ // Update or create source
196
+ const source = map.getSource(SOURCE_ID);
197
+ if (source && source.type === 'geojson') {
198
+ console.log('RepeatersLayer: Updating existing source');
199
+ source.setData(geoJSON);
200
+ } else {
201
+ console.log('RepeatersLayer: Creating new source');
202
+ map.addSource(SOURCE_ID, {
203
+ type: 'geojson',
204
+ data: geoJSON
205
+ });
206
+ }
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);
255
+ }
256
+ });
257
+ </script>
258
+
259
+ <!-- This component doesn't render DOM, only Mapbox layers -->
@@ -0,0 +1,10 @@
1
+ import type { RepeaterStoreContext } from '../stores/repeaterStoreContext.svelte';
2
+ interface Props {
3
+ /** Repeater store context */
4
+ store: RepeaterStoreContext;
5
+ /** Unique namespace for layer IDs */
6
+ namespace: string;
7
+ }
8
+ declare const RepeatersLayer: import("svelte").Component<Props, {}, "">;
9
+ type RepeatersLayer = ReturnType<typeof RepeatersLayer>;
10
+ export default RepeatersLayer;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Repeater Store Context - Reactive state for repeater feature
3
+ *
4
+ * Simplified store with fixed Tech → FBand tree structure
5
+ * Supports leaf-level coloring for each tech:fband combination
6
+ */
7
+ import type { Repeater } from '../types';
8
+ export interface RepeaterStoreValue {
9
+ repeaters: Repeater[];
10
+ visibleTechBands: Set<string>;
11
+ techBandColorMap: Map<string, string>;
12
+ showRepeaters: boolean;
13
+ baseRadius: number;
14
+ fillOpacity: number;
15
+ lineWidth: number;
16
+ showLabels: boolean;
17
+ primaryLabelField: keyof Repeater;
18
+ secondaryLabelField: keyof Repeater | 'none';
19
+ labelSize: number;
20
+ labelColor: string;
21
+ labelOffset: number;
22
+ labelHaloColor: string;
23
+ labelHaloWidth: number;
24
+ minLabelZoom: number;
25
+ currentZoom: number;
26
+ }
27
+ export interface RepeaterStoreContext {
28
+ readonly repeaters: Repeater[];
29
+ readonly filteredRepeaters: Repeater[];
30
+ readonly visibleTechBands: Set<string>;
31
+ readonly techBandColorMap: Map<string, string>;
32
+ readonly showRepeaters: boolean;
33
+ readonly baseRadius: number;
34
+ readonly fillOpacity: number;
35
+ readonly lineWidth: number;
36
+ readonly currentZoom: number;
37
+ readonly showLabels: boolean;
38
+ readonly primaryLabelField: keyof Repeater;
39
+ readonly secondaryLabelField: keyof Repeater | 'none';
40
+ readonly labelSize: number;
41
+ readonly labelColor: string;
42
+ readonly labelOffset: number;
43
+ readonly labelHaloColor: string;
44
+ readonly labelHaloWidth: number;
45
+ readonly minLabelZoom: number;
46
+ setRepeaters(repeaters: Repeater[]): void;
47
+ toggleTechBand(tech: string, fband: string): void;
48
+ isTechBandVisible(tech: string, fband: string): boolean;
49
+ getTechBandColor(tech: string, fband: string): string;
50
+ setTechBandColor(tech: string, fband: string, color: string): void;
51
+ setShowRepeaters(value: boolean): void;
52
+ setBaseRadius(value: number): void;
53
+ setFillOpacity(value: number): void;
54
+ setLineWidth(value: number): void;
55
+ setCurrentZoom(value: number): void;
56
+ setShowLabels(value: boolean): void;
57
+ setPrimaryLabelField(field: keyof Repeater): void;
58
+ setSecondaryLabelField(field: keyof Repeater | 'none'): void;
59
+ setLabelSize(value: number): void;
60
+ setLabelColor(value: string): void;
61
+ setLabelOffset(value: number): void;
62
+ setLabelHaloColor(value: string): void;
63
+ setLabelHaloWidth(value: number): void;
64
+ setMinLabelZoom(value: number): void;
65
+ }
66
+ /**
67
+ * Create a repeater store context with reactive state
68
+ */
69
+ export declare function createRepeaterStoreContext(repeaters: Repeater[]): RepeaterStoreContext;