@smartnet360/svelte-components 0.0.85 → 0.0.86

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 (115) hide show
  1. package/dist/apps/antenna-pattern/components/AntennaControls.svelte +1 -106
  2. package/dist/apps/antenna-pattern/components/AntennaDiagrams.svelte +0 -36
  3. package/dist/apps/antenna-pattern/components/AntennaSettingsModal.svelte +0 -2
  4. package/dist/apps/antenna-pattern/components/PlotlyRadarChart.svelte +0 -22
  5. package/dist/apps/antenna-pattern/components/chart-engines/PolarAreaChart.svelte +0 -2
  6. package/dist/apps/antenna-pattern/components/chart-engines/PolarBarChart.svelte +0 -2
  7. package/dist/apps/antenna-pattern/components/chart-engines/PolarLineChart.svelte +0 -2
  8. package/dist/apps/site-check/data-loader.js +0 -8
  9. package/dist/core/Charts/GlobalControls.svelte +0 -4
  10. package/dist/core/Desktop/Grid/ResizeHandle.svelte +0 -7
  11. package/dist/core/Desktop/Grid/resizeStore.js +0 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +2 -0
  14. package/dist/map-v2/demo/DemoMap.svelte +0 -2
  15. package/dist/map-v2/demo/demo-cells.js +0 -1
  16. package/dist/map-v2/features/cells/layers/CellsLayer.svelte +7 -26
  17. package/dist/map-v2/features/cells/utils/cellTree.js +0 -29
  18. package/dist/map-v2/features/repeaters/layers/RepeaterLabelsLayer.svelte +3 -27
  19. package/dist/map-v2/features/repeaters/layers/RepeatersLayer.svelte +8 -25
  20. package/dist/map-v2/features/repeaters/utils/repeaterTree.js +0 -6
  21. package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +0 -8
  22. package/dist/map-v2/features/sites/utils/siteTreeUtils.js +0 -6
  23. package/dist/map-v3/core/components/Map.svelte +89 -0
  24. package/dist/map-v3/core/components/Map.svelte.d.ts +13 -0
  25. package/dist/map-v3/core/controls/FeatureSettingsControl.svelte +103 -0
  26. package/dist/map-v3/core/controls/FeatureSettingsControl.svelte.d.ts +15 -0
  27. package/dist/map-v3/core/controls/MapStyleControl.svelte +271 -0
  28. package/dist/map-v3/core/controls/MapStyleControl.svelte.d.ts +28 -0
  29. package/dist/map-v3/core/index.d.ts +3 -0
  30. package/dist/map-v3/core/index.js +3 -0
  31. package/dist/map-v3/core/stores/map.store.svelte.d.ts +8 -0
  32. package/dist/map-v3/core/stores/map.store.svelte.js +29 -0
  33. package/dist/map-v3/core/stores/viewport.store.svelte.d.ts +38 -0
  34. package/dist/map-v3/core/stores/viewport.store.svelte.js +107 -0
  35. package/dist/map-v3/demo/DemoMap.svelte +104 -0
  36. package/dist/map-v3/demo/DemoMap.svelte.d.ts +6 -0
  37. package/dist/map-v3/demo/demo-cells.d.ts +13 -0
  38. package/dist/map-v3/demo/demo-cells.js +130 -0
  39. package/dist/map-v3/demo/demo-data.d.ts +8 -0
  40. package/dist/map-v3/demo/demo-data.js +104 -0
  41. package/dist/map-v3/demo/demo-repeaters.d.ts +13 -0
  42. package/dist/map-v3/demo/demo-repeaters.js +73 -0
  43. package/dist/map-v3/features/cells/components/CellFilterControl.svelte +208 -0
  44. package/dist/map-v3/features/cells/components/CellFilterControl.svelte.d.ts +12 -0
  45. package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte +229 -0
  46. package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte.d.ts +7 -0
  47. package/dist/map-v3/features/cells/constants.d.ts +18 -0
  48. package/dist/map-v3/features/cells/constants.js +37 -0
  49. package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte +230 -0
  50. package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte.d.ts +11 -0
  51. package/dist/map-v3/features/cells/layers/CellsLayer.svelte +194 -0
  52. package/dist/map-v3/features/cells/layers/CellsLayer.svelte.d.ts +11 -0
  53. package/dist/map-v3/features/cells/layers/index.d.ts +2 -0
  54. package/dist/map-v3/features/cells/layers/index.js +2 -0
  55. package/dist/map-v3/features/cells/logic/geometry.d.ts +12 -0
  56. package/dist/map-v3/features/cells/logic/geometry.js +35 -0
  57. package/dist/map-v3/features/cells/logic/grouping.d.ts +18 -0
  58. package/dist/map-v3/features/cells/logic/grouping.js +30 -0
  59. package/dist/map-v3/features/cells/logic/tree-adapter.d.ts +11 -0
  60. package/dist/map-v3/features/cells/logic/tree-adapter.js +53 -0
  61. package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +9 -0
  62. package/dist/map-v3/features/cells/stores/cell.data.svelte.js +16 -0
  63. package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +25 -0
  64. package/dist/map-v3/features/cells/stores/cell.display.svelte.js +67 -0
  65. package/dist/map-v3/features/cells/stores/cell.registry.svelte.d.ts +23 -0
  66. package/dist/map-v3/features/cells/stores/cell.registry.svelte.js +68 -0
  67. package/dist/map-v3/features/cells/types.d.ts +62 -0
  68. package/dist/map-v3/features/cells/types.js +6 -0
  69. package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +148 -0
  70. package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte.d.ts +12 -0
  71. package/dist/map-v3/features/repeaters/components/RepeaterSettingsPanel.svelte +209 -0
  72. package/dist/map-v3/features/repeaters/components/RepeaterSettingsPanel.svelte.d.ts +7 -0
  73. package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte +177 -0
  74. package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte.d.ts +11 -0
  75. package/dist/map-v3/features/repeaters/layers/RepeatersLayer.svelte +163 -0
  76. package/dist/map-v3/features/repeaters/layers/RepeatersLayer.svelte.d.ts +11 -0
  77. package/dist/map-v3/features/repeaters/logic/geometry.d.ts +3 -0
  78. package/dist/map-v3/features/repeaters/logic/geometry.js +23 -0
  79. package/dist/map-v3/features/repeaters/logic/grouping.d.ts +8 -0
  80. package/dist/map-v3/features/repeaters/logic/grouping.js +20 -0
  81. package/dist/map-v3/features/repeaters/logic/tree-adapter.d.ts +8 -0
  82. package/dist/map-v3/features/repeaters/logic/tree-adapter.js +43 -0
  83. package/dist/map-v3/features/repeaters/stores/repeater.data.svelte.d.ts +8 -0
  84. package/dist/map-v3/features/repeaters/stores/repeater.data.svelte.js +13 -0
  85. package/dist/map-v3/features/repeaters/stores/repeater.display.svelte.d.ts +21 -0
  86. package/dist/map-v3/features/repeaters/stores/repeater.display.svelte.js +64 -0
  87. package/dist/map-v3/features/repeaters/stores/repeater.registry.svelte.d.ts +23 -0
  88. package/dist/map-v3/features/repeaters/stores/repeater.registry.svelte.js +68 -0
  89. package/dist/map-v3/features/repeaters/types.d.ts +18 -0
  90. package/dist/map-v3/features/repeaters/types.js +1 -0
  91. package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +119 -0
  92. package/dist/map-v3/features/sites/components/SiteFilterControl.svelte.d.ts +12 -0
  93. package/dist/map-v3/features/sites/components/SiteSettingsPanel.svelte +241 -0
  94. package/dist/map-v3/features/sites/components/SiteSettingsPanel.svelte.d.ts +7 -0
  95. package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte +152 -0
  96. package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte.d.ts +11 -0
  97. package/dist/map-v3/features/sites/layers/SitesLayer.svelte +132 -0
  98. package/dist/map-v3/features/sites/layers/SitesLayer.svelte.d.ts +11 -0
  99. package/dist/map-v3/features/sites/logic/tree-adapter.d.ts +9 -0
  100. package/dist/map-v3/features/sites/logic/tree-adapter.js +75 -0
  101. package/dist/map-v3/features/sites/stores/site.data.svelte.d.ts +8 -0
  102. package/dist/map-v3/features/sites/stores/site.data.svelte.js +40 -0
  103. package/dist/map-v3/features/sites/stores/site.display.svelte.d.ts +20 -0
  104. package/dist/map-v3/features/sites/stores/site.display.svelte.js +63 -0
  105. package/dist/map-v3/features/sites/stores/site.registry.svelte.d.ts +13 -0
  106. package/dist/map-v3/features/sites/stores/site.registry.svelte.js +83 -0
  107. package/dist/map-v3/features/sites/types.d.ts +12 -0
  108. package/dist/map-v3/features/sites/types.js +1 -0
  109. package/dist/map-v3/index.d.ts +26 -0
  110. package/dist/map-v3/index.js +31 -0
  111. package/dist/map-v3/shared/controls/MapControl.svelte +242 -0
  112. package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +27 -0
  113. package/dist/map-v3/shared/index.d.ts +1 -0
  114. package/dist/map-v3/shared/index.js +1 -0
  115. package/package.json +1 -1
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import type { MapStore } from '../../../core/stores/map.store.svelte';
4
+ import type { RepeaterDataStore } from '../stores/repeater.data.svelte';
5
+ import type { RepeaterRegistry } from '../stores/repeater.registry.svelte';
6
+ import type { RepeaterDisplayStore } from '../stores/repeater.display.svelte';
7
+ import { groupRepeaters, getColorForGroup } from '../logic/grouping';
8
+ import { generateRepeaterArc, calculateRadiusInMeters } from '../logic/geometry';
9
+ import { Z_INDEX_BY_BAND } from '../../cells/constants';
10
+ import type { TechnologyBandKey } from '../../cells/types';
11
+ import type mapboxgl from 'mapbox-gl';
12
+
13
+ interface Props {
14
+ dataStore: RepeaterDataStore;
15
+ registry: RepeaterRegistry;
16
+ displayStore: RepeaterDisplayStore;
17
+ }
18
+
19
+ let { dataStore, registry, displayStore }: Props = $props();
20
+
21
+ const mapStore = getContext<MapStore>('MAP_CONTEXT');
22
+ let sourceId = 'repeaters-source';
23
+ let layerId = 'repeaters-layer';
24
+ let lineLayerId = 'repeaters-line-layer';
25
+
26
+ let updateTimeout: any;
27
+
28
+ $effect(() => {
29
+ const map = mapStore.map;
30
+ if (!map) return;
31
+
32
+ if (map.getLayer(layerId)) {
33
+ map.setPaintProperty(layerId, 'fill-opacity', displayStore.fillOpacity);
34
+ }
35
+ if (map.getLayer(lineLayerId)) {
36
+ map.setPaintProperty(lineLayerId, 'line-width', displayStore.lineWidth);
37
+ }
38
+ });
39
+
40
+ $effect(() => {
41
+ const map = mapStore.map;
42
+ if (!map) return;
43
+
44
+ const addLayers = () => {
45
+ if (!map.getSource(sourceId)) {
46
+ map.addSource(sourceId, {
47
+ type: 'geojson',
48
+ data: { type: 'FeatureCollection', features: [] }
49
+ });
50
+ }
51
+
52
+ if (!map.getLayer(layerId)) {
53
+ map.addLayer({
54
+ id: layerId,
55
+ type: 'fill',
56
+ source: sourceId,
57
+ paint: {
58
+ 'fill-color': ['get', 'color'],
59
+ 'fill-opacity': displayStore.fillOpacity
60
+ },
61
+ layout: {
62
+ 'fill-sort-key': ['get', 'zIndex']
63
+ }
64
+ });
65
+ }
66
+
67
+ if (!map.getLayer(lineLayerId)) {
68
+ map.addLayer({
69
+ id: lineLayerId,
70
+ type: 'line',
71
+ source: sourceId,
72
+ paint: {
73
+ 'line-color': '#000',
74
+ 'line-width': displayStore.lineWidth,
75
+ 'line-opacity': 0.5,
76
+ 'line-dasharray': [2, 1]
77
+ }
78
+ });
79
+ }
80
+
81
+ updateLayer();
82
+ };
83
+
84
+ addLayers();
85
+ map.on('style.load', addLayers);
86
+ map.on('moveend', updateLayer);
87
+ map.on('zoomend', updateLayer);
88
+
89
+ return () => {
90
+ map.off('style.load', addLayers);
91
+ map.off('moveend', updateLayer);
92
+ map.off('zoomend', updateLayer);
93
+
94
+ if (map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);
95
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
96
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
97
+ };
98
+ });
99
+
100
+ $effect(() => {
101
+ const _repeaters = dataStore.filteredRepeaters;
102
+ const _pixelSize = displayStore.targetPixelSize;
103
+ const _registryVersion = registry.version;
104
+ const _l1 = displayStore.level1;
105
+ const _l2 = displayStore.level2;
106
+
107
+ updateLayer();
108
+ });
109
+
110
+ function updateLayer() {
111
+ const map = mapStore.map;
112
+ if (!map) return;
113
+
114
+ clearTimeout(updateTimeout);
115
+ updateTimeout = setTimeout(() => {
116
+ renderRepeaters(map);
117
+ }, 100);
118
+ }
119
+
120
+ function renderRepeaters(map: mapboxgl.Map) {
121
+ const bounds = map.getBounds();
122
+ if (!bounds) return;
123
+
124
+ const zoom = map.getZoom();
125
+ const centerLat = map.getCenter().lat;
126
+
127
+ const radiusMeters = calculateRadiusInMeters(centerLat, zoom, displayStore.targetPixelSize);
128
+
129
+ const groups = groupRepeaters(dataStore.filteredRepeaters, displayStore.level1, displayStore.level2);
130
+
131
+ const features: GeoJSON.Feature[] = [];
132
+ let groupIndex = 0;
133
+
134
+ for (const [groupId, repeaters] of groups) {
135
+ const defaultColor = getColorForGroup(groupIndex++);
136
+ const style = registry.getStyle(groupId, defaultColor);
137
+
138
+ if (!style.visible) continue;
139
+
140
+ for (const repeater of repeaters) {
141
+ if (bounds.contains([repeater.longitude, repeater.latitude])) {
142
+ const zIndexKey = `${repeater.tech}_${repeater.fband}` as TechnologyBandKey;
143
+ const zIndex = Z_INDEX_BY_BAND[zIndexKey] || 10;
144
+
145
+ const MAX_Z = 15;
146
+ const scaleFactor = 1 + Math.max(0, MAX_Z - zIndex) * 0.08;
147
+ const effectiveRadius = radiusMeters * scaleFactor;
148
+
149
+ const feature = generateRepeaterArc(repeater, effectiveRadius, zIndex, style.color);
150
+ features.push(feature);
151
+ }
152
+ }
153
+ }
154
+
155
+ const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
156
+ if (source) {
157
+ source.setData({
158
+ type: 'FeatureCollection',
159
+ features: features as any
160
+ });
161
+ }
162
+ }
163
+ </script>
@@ -0,0 +1,11 @@
1
+ import type { RepeaterDataStore } from '../stores/repeater.data.svelte';
2
+ import type { RepeaterRegistry } from '../stores/repeater.registry.svelte';
3
+ import type { RepeaterDisplayStore } from '../stores/repeater.display.svelte';
4
+ interface Props {
5
+ dataStore: RepeaterDataStore;
6
+ registry: RepeaterRegistry;
7
+ displayStore: RepeaterDisplayStore;
8
+ }
9
+ declare const RepeatersLayer: import("svelte").Component<Props, {}, "">;
10
+ type RepeatersLayer = ReturnType<typeof RepeatersLayer>;
11
+ export default RepeatersLayer;
@@ -0,0 +1,3 @@
1
+ import type { Repeater } from '../types';
2
+ export declare function calculateRadiusInMeters(latitude: number, zoom: number, targetPixelSize: number): number;
3
+ export declare function generateRepeaterArc(repeater: Repeater, radiusMeters: number, zIndex: number, color: string): GeoJSON.Feature<GeoJSON.Polygon>;
@@ -0,0 +1,23 @@
1
+ import * as turf from '@turf/turf';
2
+ export function calculateRadiusInMeters(latitude, zoom, targetPixelSize) {
3
+ const metersPerPixel = (156543.03392 * Math.cos((latitude * Math.PI) / 180)) / Math.pow(2, zoom);
4
+ return targetPixelSize * metersPerPixel;
5
+ }
6
+ export function generateRepeaterArc(repeater, radiusMeters, zIndex, color) {
7
+ const center = [repeater.longitude, repeater.latitude];
8
+ const bearing1 = repeater.azimuth - (repeater.beamwidth / 2);
9
+ const bearing2 = repeater.azimuth + (repeater.beamwidth / 2);
10
+ const sector = turf.sector(center, radiusMeters / 1000, bearing1, bearing2, {
11
+ steps: 10
12
+ });
13
+ sector.properties = {
14
+ id: repeater.repeaterId,
15
+ name: repeater.donorCellName,
16
+ tech: repeater.tech,
17
+ fband: repeater.fband,
18
+ zIndex: zIndex,
19
+ color: color,
20
+ type: 'repeater'
21
+ };
22
+ return sector;
23
+ }
@@ -0,0 +1,8 @@
1
+ import type { Repeater, RepeaterGroupingField } from '../types';
2
+ export interface TreeData {
3
+ nodes: Map<string, any>;
4
+ rootPaths: string[];
5
+ }
6
+ export declare function generateLeafId(level1: string, level2: string): string;
7
+ export declare function getColorForGroup(index: number): string;
8
+ export declare function groupRepeaters(repeaters: Repeater[], level1Field: RepeaterGroupingField, level2Field: RepeaterGroupingField): Map<string, Repeater[]>;
@@ -0,0 +1,20 @@
1
+ import { DEFAULT_PALETTE } from '../../cells/constants';
2
+ export function generateLeafId(level1, level2) {
3
+ return `${level1}__${level2}`;
4
+ }
5
+ export function getColorForGroup(index) {
6
+ return DEFAULT_PALETTE[index % DEFAULT_PALETTE.length];
7
+ }
8
+ export function groupRepeaters(repeaters, level1Field, level2Field) {
9
+ const groups = new Map();
10
+ for (const repeater of repeaters) {
11
+ const l1 = repeater[level1Field] || 'Unknown';
12
+ const l2 = repeater[level2Field] || 'Unknown';
13
+ const key = generateLeafId(String(l1), String(l2));
14
+ if (!groups.has(key)) {
15
+ groups.set(key, []);
16
+ }
17
+ groups.get(key)?.push(repeater);
18
+ }
19
+ return groups;
20
+ }
@@ -0,0 +1,8 @@
1
+ import type { TreeNode } from '../../../../core/TreeView/tree.model';
2
+ import type { Repeater, RepeaterGroupingField } from '../types';
3
+ import type { RepeaterRegistry } from '../stores/repeater.registry.svelte';
4
+ export declare function buildRepeaterTree(repeaters: Repeater[], registry: RepeaterRegistry, level1?: RepeaterGroupingField, level2?: RepeaterGroupingField): TreeNode<{
5
+ color: string;
6
+ count: number;
7
+ groupId: string;
8
+ }>[];
@@ -0,0 +1,43 @@
1
+ import { groupRepeaters, generateLeafId, getColorForGroup } from './grouping';
2
+ export function buildRepeaterTree(repeaters, registry, level1 = 'tech', level2 = 'fband') {
3
+ const groups = groupRepeaters(repeaters, level1, level2);
4
+ const level1Nodes = new Map();
5
+ let groupIndex = 0;
6
+ for (const [groupId, groupRepeaters] of groups) {
7
+ if (groupRepeaters.length === 0)
8
+ continue;
9
+ const sample = groupRepeaters[0];
10
+ const l1Value = String(sample[level1] || 'Unknown');
11
+ const l2Value = String(sample[level2] || 'Unknown');
12
+ if (!level1Nodes.has(l1Value)) {
13
+ level1Nodes.set(l1Value, {
14
+ id: l1Value,
15
+ label: l1Value,
16
+ children: [],
17
+ defaultExpanded: true,
18
+ });
19
+ }
20
+ const defaultColor = getColorForGroup(groupIndex++);
21
+ const style = registry.getStyle(groupId, defaultColor);
22
+ const leafNode = {
23
+ id: groupId,
24
+ label: `${l2Value} (${groupRepeaters.length})`,
25
+ metadata: {
26
+ color: style.color,
27
+ count: groupRepeaters.length,
28
+ groupId: groupId
29
+ },
30
+ defaultChecked: style.visible,
31
+ };
32
+ level1Nodes.get(l1Value)?.children?.push(leafNode);
33
+ }
34
+ const sortedNodes = Array.from(level1Nodes.values()).sort((a, b) => a.label.localeCompare(b.label));
35
+ const rootNode = {
36
+ id: 'root-repeaters',
37
+ label: `Repeaters (${repeaters.length})`,
38
+ children: sortedNodes,
39
+ defaultExpanded: true,
40
+ defaultChecked: true,
41
+ };
42
+ return [rootNode];
43
+ }
@@ -0,0 +1,8 @@
1
+ import type { Repeater } from '../types';
2
+ export declare class RepeaterDataStore {
3
+ rawRepeaters: Repeater[];
4
+ constructor();
5
+ setRepeaters(repeaters: Repeater[]): void;
6
+ get filteredRepeaters(): Repeater[];
7
+ }
8
+ export declare function createRepeaterDataStore(): RepeaterDataStore;
@@ -0,0 +1,13 @@
1
+ export class RepeaterDataStore {
2
+ rawRepeaters = $state([]);
3
+ constructor() { }
4
+ setRepeaters(repeaters) {
5
+ this.rawRepeaters = repeaters;
6
+ }
7
+ get filteredRepeaters() {
8
+ return this.rawRepeaters;
9
+ }
10
+ }
11
+ export function createRepeaterDataStore() {
12
+ return new RepeaterDataStore();
13
+ }
@@ -0,0 +1,21 @@
1
+ import type { RepeaterGroupingField } from '../types';
2
+ export declare class RepeaterDisplayStore {
3
+ key: string;
4
+ targetPixelSize: number;
5
+ fillOpacity: number;
6
+ lineWidth: number;
7
+ level1: RepeaterGroupingField;
8
+ level2: RepeaterGroupingField;
9
+ showLabels: boolean;
10
+ labelPixelDistance: number;
11
+ labelFontSize: number;
12
+ labelAzimuthTolerance: number;
13
+ labelColor: string;
14
+ labelHaloColor: string;
15
+ labelHaloWidth: number;
16
+ labels: {
17
+ primary: string;
18
+ secondary: string;
19
+ };
20
+ constructor();
21
+ }
@@ -0,0 +1,64 @@
1
+ import { browser } from '$app/environment';
2
+ export class RepeaterDisplayStore {
3
+ key = 'map-v3-repeater-display';
4
+ // State
5
+ targetPixelSize = $state(40); // Slightly smaller default than cells
6
+ fillOpacity = $state(0.6);
7
+ lineWidth = $state(1);
8
+ // Grouping
9
+ level1 = $state('tech');
10
+ level2 = $state('fband');
11
+ // Label Settings
12
+ showLabels = $state(false);
13
+ labelPixelDistance = $state(60);
14
+ labelFontSize = $state(12);
15
+ labelAzimuthTolerance = $state(10);
16
+ labelColor = $state('#333333');
17
+ labelHaloColor = $state('#ffffff');
18
+ labelHaloWidth = $state(1);
19
+ // Unified label config
20
+ labels = $state({ primary: 'repeaterId', secondary: 'none' });
21
+ constructor() {
22
+ if (browser) {
23
+ const saved = localStorage.getItem(this.key);
24
+ if (saved) {
25
+ try {
26
+ const parsed = JSON.parse(saved);
27
+ this.targetPixelSize = parsed.targetPixelSize ?? 40;
28
+ this.fillOpacity = parsed.fillOpacity ?? 0.6;
29
+ this.lineWidth = parsed.lineWidth ?? 1;
30
+ this.level1 = parsed.level1 ?? 'tech';
31
+ this.level2 = parsed.level2 ?? 'fband';
32
+ this.showLabels = parsed.showLabels ?? false;
33
+ this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
34
+ this.labelFontSize = parsed.labelFontSize ?? 12;
35
+ this.labelAzimuthTolerance = parsed.labelAzimuthTolerance ?? 10;
36
+ this.labelColor = parsed.labelColor ?? '#333333';
37
+ this.labelHaloColor = parsed.labelHaloColor ?? '#ffffff';
38
+ this.labelHaloWidth = parsed.labelHaloWidth ?? 1;
39
+ this.labels = parsed.labels ?? { primary: 'repeaterId', secondary: 'none' };
40
+ }
41
+ catch (e) {
42
+ console.error('Failed to load repeater display settings', e);
43
+ }
44
+ }
45
+ $effect(() => {
46
+ localStorage.setItem(this.key, JSON.stringify({
47
+ targetPixelSize: this.targetPixelSize,
48
+ fillOpacity: this.fillOpacity,
49
+ lineWidth: this.lineWidth,
50
+ level1: this.level1,
51
+ level2: this.level2,
52
+ showLabels: this.showLabels,
53
+ labelPixelDistance: this.labelPixelDistance,
54
+ labelFontSize: this.labelFontSize,
55
+ labelAzimuthTolerance: this.labelAzimuthTolerance,
56
+ labelColor: this.labelColor,
57
+ labelHaloColor: this.labelHaloColor,
58
+ labelHaloWidth: this.labelHaloWidth,
59
+ labels: this.labels
60
+ }));
61
+ });
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Persistent store for repeater styling (colors, visibility)
3
+ * Key: Stable Group ID (e.g. "4G__LTE700")
4
+ * Value: { color: string, visible: boolean }
5
+ */
6
+ export declare class RepeaterRegistry {
7
+ state: Record<string, {
8
+ color: string;
9
+ visible: boolean;
10
+ }>;
11
+ version: number;
12
+ namespace: string;
13
+ constructor(namespace?: string);
14
+ load(): void;
15
+ save(): void;
16
+ getStyle(id: string, defaultColor: string): {
17
+ color: string;
18
+ visible: boolean;
19
+ };
20
+ toggleVisibility(id: string): void;
21
+ setColor(id: string, color: string): void;
22
+ }
23
+ export declare function createRepeaterRegistry(namespace: string): RepeaterRegistry;
@@ -0,0 +1,68 @@
1
+ import { writable } from 'svelte/store';
2
+ /**
3
+ * Persistent store for repeater styling (colors, visibility)
4
+ * Key: Stable Group ID (e.g. "4G__LTE700")
5
+ * Value: { color: string, visible: boolean }
6
+ */
7
+ export class RepeaterRegistry {
8
+ state = $state({});
9
+ version = $state(0); // Signal for reactivity
10
+ namespace;
11
+ constructor(namespace = 'default') {
12
+ this.namespace = namespace;
13
+ this.load();
14
+ $effect(() => {
15
+ this.save();
16
+ });
17
+ }
18
+ load() {
19
+ if (typeof window === 'undefined')
20
+ return;
21
+ try {
22
+ const stored = localStorage.getItem(`${this.namespace}:repeater-registry`);
23
+ if (stored) {
24
+ this.state = JSON.parse(stored);
25
+ this.version++;
26
+ }
27
+ }
28
+ catch (e) {
29
+ console.warn('Failed to load repeater registry', e);
30
+ }
31
+ }
32
+ save() {
33
+ if (typeof window === 'undefined')
34
+ return;
35
+ try {
36
+ localStorage.setItem(`${this.namespace}:repeater-registry`, JSON.stringify(this.state));
37
+ }
38
+ catch (e) {
39
+ console.warn('Failed to save repeater registry', e);
40
+ }
41
+ }
42
+ getStyle(id, defaultColor) {
43
+ if (!this.state[id]) {
44
+ // Initialize if missing
45
+ this.state[id] = { color: defaultColor, visible: true };
46
+ // No version bump here to avoid loops during render
47
+ }
48
+ return this.state[id];
49
+ }
50
+ toggleVisibility(id) {
51
+ if (this.state[id]) {
52
+ this.state[id].visible = !this.state[id].visible;
53
+ this.version++;
54
+ }
55
+ else {
56
+ console.warn(`[RepeaterRegistry] Tried to toggle missing ID: ${id}`);
57
+ }
58
+ }
59
+ setColor(id, color) {
60
+ if (this.state[id]) {
61
+ this.state[id].color = color;
62
+ this.version++;
63
+ }
64
+ }
65
+ }
66
+ export function createRepeaterRegistry(namespace) {
67
+ return new RepeaterRegistry(namespace);
68
+ }
@@ -0,0 +1,18 @@
1
+ export interface Repeater {
2
+ repeaterId: string;
3
+ donorCellId: string;
4
+ donorCellName: string;
5
+ latitude: number;
6
+ longitude: number;
7
+ azimuth: number;
8
+ beamwidth: number;
9
+ tech: string;
10
+ fband: string;
11
+ type: 'REPEATER';
12
+ height: number;
13
+ factory_nbr: string;
14
+ provider: string;
15
+ featureGroup: string;
16
+ [key: string]: any;
17
+ }
18
+ export type RepeaterGroupingField = 'tech' | 'fband' | 'provider' | 'featureGroup' | 'none';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { MapControl } from '../../../shared';
4
+ import { createTreeStore } from '../../../../core/TreeView/tree.store';
5
+ import TreeView from '../../../../core/TreeView/TreeView.svelte';
6
+ import type { SiteDataStore } from '../stores/site.data.svelte';
7
+ import type { SiteRegistry } from '../stores/site.registry.svelte';
8
+ import type { SiteDisplayStore } from '../stores/site.display.svelte';
9
+ import { buildSiteTree } from '../logic/tree-adapter';
10
+
11
+ interface Props {
12
+ dataStore: SiteDataStore;
13
+ registry: SiteRegistry;
14
+ displayStore: SiteDisplayStore;
15
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
16
+ }
17
+
18
+ let { dataStore, registry, displayStore, position = 'top-left' }: Props = $props();
19
+
20
+ // Create Tree Store
21
+ let treeStore = $derived.by(() => {
22
+ const _sites = dataStore.sites;
23
+
24
+ return untrack(() => {
25
+ const nodes = buildSiteTree(_sites);
26
+ return createTreeStore({
27
+ nodes,
28
+ namespace: `${registry.namespace}:site-tree`,
29
+ persistState: true,
30
+ defaultExpandAll: false
31
+ });
32
+ });
33
+ });
34
+
35
+ // Sync Tree Selection -> Site Registry Visibility
36
+ $effect(() => {
37
+ const unsubscribe = treeStore.subscribe((val) => {
38
+ // Iterate all leaf nodes to sync visibility
39
+
40
+ // Collect all site IDs from the tree
41
+ const allSiteIds: string[] = [];
42
+ const visibleSiteIds: string[] = [];
43
+
44
+ val.state.nodes.forEach((nodeState) => {
45
+ // Check if it's a leaf node (Level 2 group)
46
+ if (nodeState.node.children && nodeState.node.children.length > 0) return;
47
+
48
+ // It's a leaf (Level 2 group)
49
+ const siteIds = nodeState.node.metadata?.siteIds || [];
50
+ allSiteIds.push(...siteIds);
51
+
52
+ if (val.state.checkedPaths.has(nodeState.path)) {
53
+ visibleSiteIds.push(...siteIds);
54
+ }
55
+ });
56
+
57
+ // Update registry
58
+ const hiddenIds = allSiteIds.filter(id => !visibleSiteIds.includes(id));
59
+
60
+ // Optimization: Check if anything actually changed
61
+ let needsUpdate = false;
62
+ // Simple check: if counts match, assume mostly correct, but better to check content
63
+ // For now, just update. Registry handles diffing somewhat.
64
+ registry.setVisible(visibleSiteIds, true);
65
+ registry.setVisible(hiddenIds, false);
66
+ });
67
+
68
+ return () => {
69
+ unsubscribe();
70
+ };
71
+ });
72
+
73
+ function handleColorChange(node: any, event: Event) {
74
+ const input = event.target as HTMLInputElement;
75
+ const color = input.value;
76
+ if (node.metadata?.level1 && node.metadata?.level2) {
77
+ registry.setGroupColor(node.metadata.level1, node.metadata.level2, color);
78
+ }
79
+ }
80
+
81
+ function getGroupColor(node: any): string {
82
+ if (node.metadata?.level1 && node.metadata?.level2) {
83
+ return registry.getColor(node.metadata.level1, node.metadata.level2, node.metadata.color);
84
+ }
85
+ return '#3388ff';
86
+ }
87
+ </script>
88
+
89
+ <MapControl {position} title="Sites" icon="broadcast-pin" controlWidth="300px">
90
+ <div class="p-2">
91
+ <!-- <div class="mb-2 text-muted small">
92
+ {dataStore.sites.length} Sites
93
+ </div> -->
94
+ <div class="border rounded bg-white" style="max-height: 400px; overflow-y: auto;">
95
+ <TreeView showControls={false} store={$treeStore}>
96
+ {#snippet children({ node, state })}
97
+ <!-- Color Picker (Only for leaves / Level 2) -->
98
+ {#if !node.children || node.children.length === 0}
99
+ <div
100
+ class="d-flex align-items-center"
101
+ role="group"
102
+ onclick={(e) => e.stopPropagation()}
103
+ onkeydown={(e) => e.stopPropagation()}
104
+ >
105
+ <input
106
+ type="color"
107
+ class="form-control form-control-color form-control-sm border-0 p-0"
108
+ style="width: 16px; height: 16px; min-height: 0;"
109
+ value={getGroupColor(node)}
110
+ oninput={(e) => handleColorChange(node, e)}
111
+ title="Change color"
112
+ />
113
+ </div>
114
+ {/if}
115
+ {/snippet}
116
+ </TreeView>
117
+ </div>
118
+ </div>
119
+ </MapControl>
@@ -0,0 +1,12 @@
1
+ import type { SiteDataStore } from '../stores/site.data.svelte';
2
+ import type { SiteRegistry } from '../stores/site.registry.svelte';
3
+ import type { SiteDisplayStore } from '../stores/site.display.svelte';
4
+ interface Props {
5
+ dataStore: SiteDataStore;
6
+ registry: SiteRegistry;
7
+ displayStore: SiteDisplayStore;
8
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
9
+ }
10
+ declare const SiteFilterControl: import("svelte").Component<Props, {}, "">;
11
+ type SiteFilterControl = ReturnType<typeof SiteFilterControl>;
12
+ export default SiteFilterControl;