@smartnet360/svelte-components 0.0.84 → 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/SiteCheck.svelte +60 -80
- package/dist/apps/site-check/data-loader.d.ts +9 -6
- package/dist/apps/site-check/data-loader.js +2 -11
- package/dist/apps/site-check/helper.d.ts +3 -2
- package/dist/apps/site-check/helper.js +7 -5
- package/dist/apps/site-check/index.d.ts +1 -1
- package/dist/apps/site-check/transforms.d.ts +4 -2
- package/dist/apps/site-check/transforms.js +49 -10
- 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,177 @@
|
|
|
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 { RepeaterDisplayStore } from '../stores/repeater.display.svelte';
|
|
6
|
+
import type { RepeaterRegistry } from '../stores/repeater.registry.svelte';
|
|
7
|
+
import { groupRepeaters, getColorForGroup } from '../logic/grouping';
|
|
8
|
+
import type { Repeater } from '../types';
|
|
9
|
+
import type mapboxgl from 'mapbox-gl';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
dataStore: RepeaterDataStore;
|
|
13
|
+
displayStore: RepeaterDisplayStore;
|
|
14
|
+
registry: RepeaterRegistry;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { dataStore, displayStore, registry }: Props = $props();
|
|
18
|
+
|
|
19
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
20
|
+
let sourceId = 'repeater-labels-source';
|
|
21
|
+
let layerId = 'repeater-labels-layer';
|
|
22
|
+
let updateTimeout: any;
|
|
23
|
+
|
|
24
|
+
// Helper to get label text
|
|
25
|
+
function getLabelText(repeater: Repeater, config: { primary: string, secondary: string }): string {
|
|
26
|
+
const pVal = repeater[config.primary as keyof Repeater];
|
|
27
|
+
const sVal = config.secondary !== 'none' ? repeater[config.secondary as keyof Repeater] : null;
|
|
28
|
+
|
|
29
|
+
let text = pVal != null ? String(pVal) : '';
|
|
30
|
+
if (sVal != null) {
|
|
31
|
+
text += ` | ${sVal}`;
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// React to changes
|
|
37
|
+
$effect(() => {
|
|
38
|
+
// Read dependencies
|
|
39
|
+
const _repeaters = dataStore.filteredRepeaters;
|
|
40
|
+
const _show = displayStore.showLabels;
|
|
41
|
+
const _dist = displayStore.labelPixelDistance;
|
|
42
|
+
const _size = displayStore.labelFontSize;
|
|
43
|
+
const _tol = displayStore.labelAzimuthTolerance;
|
|
44
|
+
// Track specific label properties for reactivity
|
|
45
|
+
const _lPrim = displayStore.labels.primary;
|
|
46
|
+
const _lSec = displayStore.labels.secondary;
|
|
47
|
+
const _color = displayStore.labelColor;
|
|
48
|
+
const _haloColor = displayStore.labelHaloColor;
|
|
49
|
+
const _haloWidth = displayStore.labelHaloWidth;
|
|
50
|
+
const _registryVersion = registry.version;
|
|
51
|
+
|
|
52
|
+
const map = mapStore.map;
|
|
53
|
+
if (map && map.getLayer(layerId)) {
|
|
54
|
+
map.setPaintProperty(layerId, 'text-color', displayStore.labelColor);
|
|
55
|
+
map.setPaintProperty(layerId, 'text-halo-color', displayStore.labelHaloColor);
|
|
56
|
+
map.setPaintProperty(layerId, 'text-halo-width', displayStore.labelHaloWidth);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateLayer();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function updateLayer() {
|
|
63
|
+
const map = mapStore.map;
|
|
64
|
+
if (!map) return;
|
|
65
|
+
|
|
66
|
+
clearTimeout(updateTimeout);
|
|
67
|
+
updateTimeout = setTimeout(() => {
|
|
68
|
+
renderLabels(map);
|
|
69
|
+
}, 150);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderLabels(map: mapboxgl.Map) {
|
|
73
|
+
if (!displayStore.showLabels) {
|
|
74
|
+
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, 'visibility', 'none');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
79
|
+
|
|
80
|
+
// Update text size if changed
|
|
81
|
+
if (map.getLayer(layerId)) {
|
|
82
|
+
map.setLayoutProperty(layerId, 'text-size', displayStore.labelFontSize);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bounds = map.getBounds();
|
|
86
|
+
if (!bounds) return;
|
|
87
|
+
|
|
88
|
+
// 1. Filter visible repeaters (Bounds + Registry Visibility)
|
|
89
|
+
const visibleRepeaters: Repeater[] = [];
|
|
90
|
+
|
|
91
|
+
// Group repeaters using the same logic as RepeatersLayer to determine visibility
|
|
92
|
+
const repeaterGroups = groupRepeaters(dataStore.filteredRepeaters, displayStore.level1, displayStore.level2);
|
|
93
|
+
let groupIndex = 0;
|
|
94
|
+
|
|
95
|
+
for (const [groupId, repeaters] of repeaterGroups) {
|
|
96
|
+
const defaultColor = getColorForGroup(groupIndex++);
|
|
97
|
+
const style = registry.getStyle(groupId, defaultColor);
|
|
98
|
+
|
|
99
|
+
if (style.visible) {
|
|
100
|
+
// Check bounds
|
|
101
|
+
for (const r of repeaters) {
|
|
102
|
+
if (bounds.contains([r.longitude, r.latitude])) {
|
|
103
|
+
visibleRepeaters.push(r);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Generate Label Features
|
|
110
|
+
const features: GeoJSON.Feature[] = [];
|
|
111
|
+
|
|
112
|
+
// Convert pixel distance to ems (approx 1em = 16px or labelFontSize)
|
|
113
|
+
const distEm = displayStore.labelPixelDistance / displayStore.labelFontSize;
|
|
114
|
+
|
|
115
|
+
for (const r of visibleRepeaters) {
|
|
116
|
+
const labelText = getLabelText(r, displayStore.labels);
|
|
117
|
+
if (!labelText) continue;
|
|
118
|
+
|
|
119
|
+
// Calculate offset based on azimuth
|
|
120
|
+
// Mapbox text-offset: [x, y]
|
|
121
|
+
// x = sin(az) * dist
|
|
122
|
+
// y = -cos(az) * dist (negative because y is down)
|
|
123
|
+
const az = r.azimuth ?? 0;
|
|
124
|
+
const azRad = (az * Math.PI) / 180;
|
|
125
|
+
const offsetX = Math.sin(azRad) * distEm;
|
|
126
|
+
const offsetY = -Math.cos(azRad) * distEm;
|
|
127
|
+
|
|
128
|
+
features.push({
|
|
129
|
+
type: 'Feature',
|
|
130
|
+
geometry: {
|
|
131
|
+
type: 'Point',
|
|
132
|
+
coordinates: [r.longitude, r.latitude]
|
|
133
|
+
},
|
|
134
|
+
properties: {
|
|
135
|
+
label: labelText,
|
|
136
|
+
offset: [offsetX, offsetY]
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const geojson: GeoJSON.FeatureCollection = {
|
|
142
|
+
type: 'FeatureCollection',
|
|
143
|
+
features
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
147
|
+
if (source) {
|
|
148
|
+
source.setData(geojson);
|
|
149
|
+
} else {
|
|
150
|
+
map.addSource(sourceId, {
|
|
151
|
+
type: 'geojson',
|
|
152
|
+
data: geojson
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
map.addLayer({
|
|
156
|
+
id: layerId,
|
|
157
|
+
type: 'symbol',
|
|
158
|
+
source: sourceId,
|
|
159
|
+
layout: {
|
|
160
|
+
'text-field': ['get', 'label'],
|
|
161
|
+
'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
|
|
162
|
+
'text-size': displayStore.labelFontSize,
|
|
163
|
+
'text-offset': ['get', 'offset'],
|
|
164
|
+
'text-anchor': 'center',
|
|
165
|
+
'text-justify': 'auto',
|
|
166
|
+
'text-allow-overlap': false,
|
|
167
|
+
'text-ignore-placement': false
|
|
168
|
+
},
|
|
169
|
+
paint: {
|
|
170
|
+
'text-color': displayStore.labelColor,
|
|
171
|
+
'text-halo-color': displayStore.labelHaloColor,
|
|
172
|
+
'text-halo-width': displayStore.labelHaloWidth
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RepeaterDataStore } from '../stores/repeater.data.svelte';
|
|
2
|
+
import type { RepeaterDisplayStore } from '../stores/repeater.display.svelte';
|
|
3
|
+
import type { RepeaterRegistry } from '../stores/repeater.registry.svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
dataStore: RepeaterDataStore;
|
|
6
|
+
displayStore: RepeaterDisplayStore;
|
|
7
|
+
registry: RepeaterRegistry;
|
|
8
|
+
}
|
|
9
|
+
declare const RepeaterLabelsLayer: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type RepeaterLabelsLayer = ReturnType<typeof RepeaterLabelsLayer>;
|
|
11
|
+
export default RepeaterLabelsLayer;
|
|
@@ -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
|
+
}
|