@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.
- package/dist/apps/antenna-pattern/components/AntennaControls.svelte +1 -106
- package/dist/apps/antenna-pattern/components/AntennaDiagrams.svelte +0 -36
- package/dist/apps/antenna-pattern/components/AntennaSettingsModal.svelte +0 -2
- package/dist/apps/antenna-pattern/components/PlotlyRadarChart.svelte +0 -22
- package/dist/apps/antenna-pattern/components/chart-engines/PolarAreaChart.svelte +0 -2
- package/dist/apps/antenna-pattern/components/chart-engines/PolarBarChart.svelte +0 -2
- package/dist/apps/antenna-pattern/components/chart-engines/PolarLineChart.svelte +0 -2
- package/dist/apps/site-check/data-loader.js +0 -8
- package/dist/core/Charts/GlobalControls.svelte +0 -4
- package/dist/core/Desktop/Grid/ResizeHandle.svelte +0 -7
- package/dist/core/Desktop/Grid/resizeStore.js +0 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/map-v2/demo/DemoMap.svelte +0 -2
- package/dist/map-v2/demo/demo-cells.js +0 -1
- package/dist/map-v2/features/cells/layers/CellsLayer.svelte +7 -26
- package/dist/map-v2/features/cells/utils/cellTree.js +0 -29
- package/dist/map-v2/features/repeaters/layers/RepeaterLabelsLayer.svelte +3 -27
- package/dist/map-v2/features/repeaters/layers/RepeatersLayer.svelte +8 -25
- package/dist/map-v2/features/repeaters/utils/repeaterTree.js +0 -6
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +0 -8
- package/dist/map-v2/features/sites/utils/siteTreeUtils.js +0 -6
- package/dist/map-v3/core/components/Map.svelte +89 -0
- package/dist/map-v3/core/components/Map.svelte.d.ts +13 -0
- package/dist/map-v3/core/controls/FeatureSettingsControl.svelte +103 -0
- package/dist/map-v3/core/controls/FeatureSettingsControl.svelte.d.ts +15 -0
- package/dist/map-v3/core/controls/MapStyleControl.svelte +271 -0
- package/dist/map-v3/core/controls/MapStyleControl.svelte.d.ts +28 -0
- package/dist/map-v3/core/index.d.ts +3 -0
- package/dist/map-v3/core/index.js +3 -0
- package/dist/map-v3/core/stores/map.store.svelte.d.ts +8 -0
- package/dist/map-v3/core/stores/map.store.svelte.js +29 -0
- package/dist/map-v3/core/stores/viewport.store.svelte.d.ts +38 -0
- package/dist/map-v3/core/stores/viewport.store.svelte.js +107 -0
- package/dist/map-v3/demo/DemoMap.svelte +104 -0
- package/dist/map-v3/demo/DemoMap.svelte.d.ts +6 -0
- package/dist/map-v3/demo/demo-cells.d.ts +13 -0
- package/dist/map-v3/demo/demo-cells.js +130 -0
- package/dist/map-v3/demo/demo-data.d.ts +8 -0
- package/dist/map-v3/demo/demo-data.js +104 -0
- package/dist/map-v3/demo/demo-repeaters.d.ts +13 -0
- package/dist/map-v3/demo/demo-repeaters.js +73 -0
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte +208 -0
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte.d.ts +12 -0
- package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte +229 -0
- package/dist/map-v3/features/cells/components/CellSettingsPanel.svelte.d.ts +7 -0
- package/dist/map-v3/features/cells/constants.d.ts +18 -0
- package/dist/map-v3/features/cells/constants.js +37 -0
- package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte +230 -0
- package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte.d.ts +11 -0
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +194 -0
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte.d.ts +11 -0
- package/dist/map-v3/features/cells/layers/index.d.ts +2 -0
- package/dist/map-v3/features/cells/layers/index.js +2 -0
- package/dist/map-v3/features/cells/logic/geometry.d.ts +12 -0
- package/dist/map-v3/features/cells/logic/geometry.js +35 -0
- package/dist/map-v3/features/cells/logic/grouping.d.ts +18 -0
- package/dist/map-v3/features/cells/logic/grouping.js +30 -0
- package/dist/map-v3/features/cells/logic/tree-adapter.d.ts +11 -0
- package/dist/map-v3/features/cells/logic/tree-adapter.js +53 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +9 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.js +16 -0
- package/dist/map-v3/features/cells/stores/cell.display.svelte.d.ts +25 -0
- package/dist/map-v3/features/cells/stores/cell.display.svelte.js +67 -0
- package/dist/map-v3/features/cells/stores/cell.registry.svelte.d.ts +23 -0
- package/dist/map-v3/features/cells/stores/cell.registry.svelte.js +68 -0
- package/dist/map-v3/features/cells/types.d.ts +62 -0
- package/dist/map-v3/features/cells/types.js +6 -0
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +148 -0
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte.d.ts +12 -0
- package/dist/map-v3/features/repeaters/components/RepeaterSettingsPanel.svelte +209 -0
- package/dist/map-v3/features/repeaters/components/RepeaterSettingsPanel.svelte.d.ts +7 -0
- package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte +177 -0
- package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte.d.ts +11 -0
- package/dist/map-v3/features/repeaters/layers/RepeatersLayer.svelte +163 -0
- package/dist/map-v3/features/repeaters/layers/RepeatersLayer.svelte.d.ts +11 -0
- package/dist/map-v3/features/repeaters/logic/geometry.d.ts +3 -0
- package/dist/map-v3/features/repeaters/logic/geometry.js +23 -0
- package/dist/map-v3/features/repeaters/logic/grouping.d.ts +8 -0
- package/dist/map-v3/features/repeaters/logic/grouping.js +20 -0
- package/dist/map-v3/features/repeaters/logic/tree-adapter.d.ts +8 -0
- package/dist/map-v3/features/repeaters/logic/tree-adapter.js +43 -0
- package/dist/map-v3/features/repeaters/stores/repeater.data.svelte.d.ts +8 -0
- package/dist/map-v3/features/repeaters/stores/repeater.data.svelte.js +13 -0
- package/dist/map-v3/features/repeaters/stores/repeater.display.svelte.d.ts +21 -0
- package/dist/map-v3/features/repeaters/stores/repeater.display.svelte.js +64 -0
- package/dist/map-v3/features/repeaters/stores/repeater.registry.svelte.d.ts +23 -0
- package/dist/map-v3/features/repeaters/stores/repeater.registry.svelte.js +68 -0
- package/dist/map-v3/features/repeaters/types.d.ts +18 -0
- package/dist/map-v3/features/repeaters/types.js +1 -0
- package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +119 -0
- package/dist/map-v3/features/sites/components/SiteFilterControl.svelte.d.ts +12 -0
- package/dist/map-v3/features/sites/components/SiteSettingsPanel.svelte +241 -0
- package/dist/map-v3/features/sites/components/SiteSettingsPanel.svelte.d.ts +7 -0
- package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte +152 -0
- package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte.d.ts +11 -0
- package/dist/map-v3/features/sites/layers/SitesLayer.svelte +132 -0
- package/dist/map-v3/features/sites/layers/SitesLayer.svelte.d.ts +11 -0
- package/dist/map-v3/features/sites/logic/tree-adapter.d.ts +9 -0
- package/dist/map-v3/features/sites/logic/tree-adapter.js +75 -0
- package/dist/map-v3/features/sites/stores/site.data.svelte.d.ts +8 -0
- package/dist/map-v3/features/sites/stores/site.data.svelte.js +40 -0
- package/dist/map-v3/features/sites/stores/site.display.svelte.d.ts +20 -0
- package/dist/map-v3/features/sites/stores/site.display.svelte.js +63 -0
- package/dist/map-v3/features/sites/stores/site.registry.svelte.d.ts +13 -0
- package/dist/map-v3/features/sites/stores/site.registry.svelte.js +83 -0
- package/dist/map-v3/features/sites/types.d.ts +12 -0
- package/dist/map-v3/features/sites/types.js +1 -0
- package/dist/map-v3/index.d.ts +26 -0
- package/dist/map-v3/index.js +31 -0
- package/dist/map-v3/shared/controls/MapControl.svelte +242 -0
- package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +27 -0
- package/dist/map-v3/shared/index.d.ts +1 -0
- package/dist/map-v3/shared/index.js +1 -0
- 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;
|