@smartnet360/svelte-components 0.0.50 → 0.0.53

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 (71) hide show
  1. package/dist/apps/site-check/SiteCheck.svelte +7 -1
  2. package/dist/apps/site-check/SiteCheck.svelte.d.ts +1 -0
  3. package/dist/apps/site-check/transforms.js +2 -2
  4. package/dist/core/Charts/ChartCard.svelte +6 -1
  5. package/dist/core/Charts/ChartComponent.svelte +11 -4
  6. package/dist/core/Charts/ChartComponent.svelte.d.ts +1 -0
  7. package/dist/core/Charts/GlobalControls.svelte +171 -25
  8. package/dist/core/Charts/GlobalControls.svelte.d.ts +1 -0
  9. package/dist/core/Charts/adapt.js +1 -1
  10. package/dist/core/Charts/charts.model.d.ts +3 -0
  11. package/dist/core/Charts/data-utils.js +8 -1
  12. package/dist/core/FeatureRegistry/index.js +1 -1
  13. package/dist/core/index.d.ts +0 -1
  14. package/dist/core/index.js +2 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +2 -0
  17. package/dist/map/controls/MapControl.svelte +204 -0
  18. package/dist/map/controls/MapControl.svelte.d.ts +17 -0
  19. package/dist/map/controls/SiteFilterControl.svelte +126 -0
  20. package/dist/map/controls/SiteFilterControl.svelte.d.ts +16 -0
  21. package/dist/map/demo/DemoMap.svelte +98 -0
  22. package/dist/map/demo/DemoMap.svelte.d.ts +12 -0
  23. package/dist/map/demo/demo-data.d.ts +12 -0
  24. package/dist/map/demo/demo-data.js +220 -0
  25. package/dist/map/hooks/useCellData.d.ts +14 -0
  26. package/dist/map/hooks/useCellData.js +29 -0
  27. package/dist/map/hooks/useMapbox.d.ts +14 -0
  28. package/dist/map/hooks/useMapbox.js +29 -0
  29. package/dist/map/index.d.ts +27 -0
  30. package/dist/map/index.js +47 -0
  31. package/dist/map/layers/CellsLayer.svelte +242 -0
  32. package/dist/map/layers/CellsLayer.svelte.d.ts +21 -0
  33. package/dist/map/layers/CoverageLayer.svelte +37 -0
  34. package/dist/map/layers/CoverageLayer.svelte.d.ts +9 -0
  35. package/dist/map/layers/LayerBase.d.ts +42 -0
  36. package/dist/map/layers/LayerBase.js +58 -0
  37. package/dist/map/layers/SitesLayer.svelte +282 -0
  38. package/dist/map/layers/SitesLayer.svelte.d.ts +19 -0
  39. package/dist/map/providers/CellDataProvider.svelte +43 -0
  40. package/dist/map/providers/CellDataProvider.svelte.d.ts +12 -0
  41. package/dist/map/providers/MapboxProvider.svelte +38 -0
  42. package/dist/map/providers/MapboxProvider.svelte.d.ts +9 -0
  43. package/dist/map/providers/providerHelpers.d.ts +17 -0
  44. package/dist/map/providers/providerHelpers.js +26 -0
  45. package/dist/map/stores/cellDataStore.d.ts +21 -0
  46. package/dist/map/stores/cellDataStore.js +53 -0
  47. package/dist/map/stores/interactions.d.ts +20 -0
  48. package/dist/map/stores/interactions.js +33 -0
  49. package/dist/map/stores/mapStore.d.ts +8 -0
  50. package/dist/map/stores/mapStore.js +10 -0
  51. package/dist/map/types.d.ts +115 -0
  52. package/dist/map/types.js +10 -0
  53. package/dist/map/utils/geojson.d.ts +20 -0
  54. package/dist/map/utils/geojson.js +78 -0
  55. package/dist/map/utils/mapboxHelpers.d.ts +51 -0
  56. package/dist/map/utils/mapboxHelpers.js +98 -0
  57. package/dist/map/utils/math.d.ts +40 -0
  58. package/dist/map/utils/math.js +95 -0
  59. package/dist/map/utils/siteTreeUtils.d.ts +27 -0
  60. package/dist/map/utils/siteTreeUtils.js +164 -0
  61. package/package.json +1 -1
  62. package/dist/core/Map/Map.svelte +0 -312
  63. package/dist/core/Map/Map.svelte.d.ts +0 -230
  64. package/dist/core/Map/index.d.ts +0 -9
  65. package/dist/core/Map/index.js +0 -9
  66. package/dist/core/Map/mapSettings.d.ts +0 -147
  67. package/dist/core/Map/mapSettings.js +0 -226
  68. package/dist/core/Map/mapStore.d.ts +0 -73
  69. package/dist/core/Map/mapStore.js +0 -136
  70. package/dist/core/Map/types.d.ts +0 -72
  71. package/dist/core/Map/types.js +0 -32
@@ -0,0 +1,204 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MapControl - Reusable wrapper for Mapbox custom controls
4
+ *
5
+ * Creates a custom control that can be positioned anywhere on the map
6
+ * and contain any content via slots.
7
+ *
8
+ * Usage:
9
+ * <MapControl position="top-left" title="My Control">
10
+ * <div>Custom content here</div>
11
+ * </MapControl>
12
+ */
13
+ import { onMount, onDestroy } from 'svelte';
14
+ import mapboxgl from 'mapbox-gl';
15
+ import { tryUseMapbox } from '../hooks/useMapbox';
16
+
17
+ interface Props {
18
+ /** Position on the map */
19
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
20
+ /** Control title (shown in header) */
21
+ title?: string;
22
+ /** Is the control collapsible? */
23
+ collapsible?: boolean;
24
+ /** Initial collapsed state */
25
+ initiallyCollapsed?: boolean;
26
+ /** Custom CSS class for the container */
27
+ className?: string;
28
+ /** Child content */
29
+ children?: import('svelte').Snippet;
30
+ }
31
+
32
+ let {
33
+ position = 'top-left',
34
+ title,
35
+ collapsible = true,
36
+ initiallyCollapsed = false,
37
+ className = '',
38
+ children
39
+ }: Props = $props();
40
+
41
+ const mapStore = tryUseMapbox();
42
+
43
+ if (!mapStore) {
44
+ console.error('MapControl: No map context available. Make sure MapControl is used inside MapboxProvider.');
45
+ }
46
+
47
+ let map: mapboxgl.Map | null = null;
48
+ let controlElement: HTMLDivElement;
49
+ let collapsed = $state(initiallyCollapsed);
50
+ let control: mapboxgl.IControl | null = null;
51
+
52
+ onMount(() => {
53
+ if (!mapStore) {
54
+ console.error('MapControl: Cannot mount - no map store available');
55
+ return;
56
+ }
57
+
58
+ const unsub = mapStore.subscribe((m) => {
59
+ if (!m) return;
60
+ map = m;
61
+ addControl();
62
+ });
63
+
64
+ return () => {
65
+ unsub();
66
+ removeControl();
67
+ };
68
+ });
69
+
70
+ onDestroy(() => {
71
+ removeControl();
72
+ });
73
+
74
+ function addControl() {
75
+ if (!map || control) return;
76
+
77
+ // Create a custom Mapbox control
78
+ control = {
79
+ onAdd: () => {
80
+ return controlElement;
81
+ },
82
+ onRemove: () => {
83
+ // Cleanup handled in onDestroy
84
+ }
85
+ };
86
+
87
+ map.addControl(control, position);
88
+ }
89
+
90
+ function removeControl() {
91
+ if (map && control) {
92
+ try {
93
+ map.removeControl(control);
94
+ } catch (e) {
95
+ // Control may already be removed
96
+ }
97
+ control = null;
98
+ }
99
+ }
100
+
101
+ function toggleCollapse() {
102
+ collapsed = !collapsed;
103
+ }
104
+ </script>
105
+
106
+ <div
107
+ bind:this={controlElement}
108
+ class="mapboxgl-ctrl mapboxgl-ctrl-group map-control-container {className}"
109
+ >
110
+ {#if title}
111
+ <div class="map-control-header">
112
+ <span class="map-control-title">{title}</span>
113
+ {#if collapsible}
114
+ <button
115
+ class="map-control-toggle"
116
+ onclick={toggleCollapse}
117
+ aria-label={collapsed ? 'Expand' : 'Collapse'}
118
+ title={collapsed ? 'Expand' : 'Collapse'}
119
+ >
120
+ <i class="bi bi-chevron-{collapsed ? 'down' : 'up'}"></i>
121
+ </button>
122
+ {/if}
123
+ </div>
124
+ {/if}
125
+
126
+ {#if !collapsed}
127
+ <div class="map-control-content">
128
+ {#if children}
129
+ {@render children()}
130
+ {/if}
131
+ </div>
132
+ {/if}
133
+ </div>
134
+
135
+ <style>
136
+ .map-control-container {
137
+ background: white;
138
+ border-radius: 4px;
139
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
140
+ overflow: hidden;
141
+ max-width: 300px;
142
+ font-family: system-ui, -apple-system, sans-serif;
143
+ }
144
+
145
+ .map-control-header {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: space-between;
149
+ padding: 8px 12px;
150
+ background: #f8f9fa;
151
+ border-bottom: 1px solid #dee2e6;
152
+ font-weight: 600;
153
+ font-size: 13px;
154
+ color: #212529;
155
+ }
156
+
157
+ .map-control-title {
158
+ flex: 1;
159
+ user-select: none;
160
+ }
161
+
162
+ .map-control-toggle {
163
+ background: none;
164
+ border: none;
165
+ padding: 4px;
166
+ cursor: pointer;
167
+ color: #6c757d;
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ border-radius: 3px;
172
+ transition: all 0.2s;
173
+ }
174
+
175
+ .map-control-toggle:hover {
176
+ background: rgba(0, 0, 0, 0.05);
177
+ color: #212529;
178
+ }
179
+
180
+ .map-control-content {
181
+ padding: 12px;
182
+ max-height: 400px;
183
+ overflow-y: auto;
184
+ font-size: 13px;
185
+ }
186
+
187
+ /* Custom scrollbar */
188
+ .map-control-content::-webkit-scrollbar {
189
+ width: 8px;
190
+ }
191
+
192
+ .map-control-content::-webkit-scrollbar-track {
193
+ background: #f1f1f1;
194
+ }
195
+
196
+ .map-control-content::-webkit-scrollbar-thumb {
197
+ background: #888;
198
+ border-radius: 4px;
199
+ }
200
+
201
+ .map-control-content::-webkit-scrollbar-thumb:hover {
202
+ background: #555;
203
+ }
204
+ </style>
@@ -0,0 +1,17 @@
1
+ interface Props {
2
+ /** Position on the map */
3
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
4
+ /** Control title (shown in header) */
5
+ title?: string;
6
+ /** Is the control collapsible? */
7
+ collapsible?: boolean;
8
+ /** Initial collapsed state */
9
+ initiallyCollapsed?: boolean;
10
+ /** Custom CSS class for the container */
11
+ className?: string;
12
+ /** Child content */
13
+ children?: import('svelte').Snippet;
14
+ }
15
+ declare const MapControl: import("svelte").Component<Props, {}, "">;
16
+ type MapControl = ReturnType<typeof MapControl>;
17
+ export default MapControl;
@@ -0,0 +1,126 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SiteFilterControl - TreeView-based site filtering control for Mapbox
4
+ *
5
+ * Displays a hierarchical tree: All Sites -> Provider -> Feature Group
6
+ * Filters sites based on checked state and persists to localStorage
7
+ *
8
+ * Usage:
9
+ * <SiteFilterControl
10
+ * {sites}
11
+ * bind:filteredSites
12
+ * position="top-left"
13
+ * />
14
+ */
15
+ import { onMount } from 'svelte';
16
+ import type { Writable } from 'svelte/store';
17
+ import MapControl from './MapControl.svelte';
18
+ import TreeView from '../../core/TreeView/TreeView.svelte';
19
+ import { createTreeStore } from '../../core/TreeView/tree.store';
20
+ import { buildSiteTree, getFilteredSites, saveTreeState } from '../utils/siteTreeUtils';
21
+ import type { Site } from '../types';
22
+ import type { TreeStoreValue } from '../../core/TreeView/tree.model';
23
+
24
+ interface Props {
25
+ /** All sites to filter */
26
+ sites: Site[];
27
+ /** Filtered sites output */
28
+ filteredSites?: Site[];
29
+ /** Control position on map */
30
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
31
+ /** Control title */
32
+ title?: string;
33
+ /** Initially collapsed? */
34
+ initiallyCollapsed?: boolean;
35
+ }
36
+
37
+ let {
38
+ sites,
39
+ filteredSites = $bindable([]),
40
+ position = 'top-left',
41
+ title = 'Site Filter',
42
+ initiallyCollapsed = false
43
+ }: Props = $props();
44
+
45
+ let treeStore = $state<Writable<TreeStoreValue> | null>(null);
46
+
47
+ // Initialize filteredSites with all sites immediately
48
+ if (filteredSites.length === 0 && sites.length > 0) {
49
+ filteredSites = [...sites];
50
+ }
51
+
52
+ // Build tree and create store when sites change (run only once on mount)
53
+ onMount(() => {
54
+ console.log('SiteFilterControl: Mounted with', sites.length, 'sites');
55
+
56
+ if (sites.length > 0) {
57
+ console.log('SiteFilterControl: Building tree...');
58
+ const treeNodes = buildSiteTree(sites);
59
+
60
+ treeStore = createTreeStore({
61
+ nodes: [treeNodes],
62
+ namespace: 'cellular-site-filter',
63
+ persistState: true,
64
+ defaultExpandAll: false
65
+ });
66
+
67
+ console.log('SiteFilterControl: Tree store created');
68
+
69
+ // Subscribe to tree changes and update filtered sites
70
+ if (treeStore) {
71
+ const unsub = treeStore.subscribe((store: TreeStoreValue) => {
72
+ const checkedPaths = store.getCheckedPaths();
73
+ console.log('TreeStore updated, checked paths:', checkedPaths);
74
+
75
+ const newFilteredSites = getFilteredSites(checkedPaths, sites);
76
+ console.log('Filtered sites count:', newFilteredSites.length, 'of', sites.length);
77
+
78
+ filteredSites = newFilteredSites;
79
+
80
+ // Save state to our custom storage
81
+ saveTreeState(checkedPaths);
82
+ });
83
+
84
+ return () => unsub();
85
+ }
86
+ }
87
+ });
88
+ </script>
89
+
90
+ <MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
91
+ {#if treeStore}
92
+ <div class="site-filter-tree">
93
+ <TreeView store={$treeStore} showControls={false} />
94
+
95
+ <!-- <div class="site-filter-stats">
96
+ <small class="text-muted">
97
+ Showing {filteredSites.length} of {sites.length} sites
98
+ </small>
99
+ </div> -->
100
+ </div>
101
+ {:else}
102
+ <div class="text-muted small">Loading sites...</div>
103
+ {/if}
104
+ </MapControl>
105
+
106
+ <style>
107
+ .site-filter-tree {
108
+ min-width: 250px;
109
+ max-width: 300px;
110
+ }
111
+
112
+ .site-filter-stats {
113
+ margin-top: 8px;
114
+ padding-top: 8px;
115
+ border-top: 1px solid #dee2e6;
116
+ text-align: center;
117
+ }
118
+
119
+ :global(.site-filter-tree .tree-node-label) {
120
+ font-size: 13px;
121
+ }
122
+
123
+ :global(.site-filter-tree .tree-node-checkbox) {
124
+ margin-right: 6px;
125
+ }
126
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Site } from '../types';
2
+ interface Props {
3
+ /** All sites to filter */
4
+ sites: Site[];
5
+ /** Filtered sites output */
6
+ filteredSites?: Site[];
7
+ /** Control position on map */
8
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
9
+ /** Control title */
10
+ title?: string;
11
+ /** Initially collapsed? */
12
+ initiallyCollapsed?: boolean;
13
+ }
14
+ declare const SiteFilterControl: import("svelte").Component<Props, {}, "filteredSites">;
15
+ type SiteFilterControl = ReturnType<typeof SiteFilterControl>;
16
+ export default SiteFilterControl;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ /**
3
+ * DemoMap - Complete demo component with map initialization and sample data
4
+ */
5
+ import { onMount } from 'svelte';
6
+ import mapboxgl from 'mapbox-gl';
7
+ import 'mapbox-gl/dist/mapbox-gl.css';
8
+
9
+ import MapboxProvider from '../providers/MapboxProvider.svelte';
10
+ import CellDataProvider from '../providers/CellDataProvider.svelte';
11
+ import SitesLayer from '../layers/SitesLayer.svelte';
12
+ import CellsLayer from '../layers/CellsLayer.svelte';
13
+ import SiteFilterControl from '../controls/SiteFilterControl.svelte';
14
+ import { demoSites, demoCells } from './demo-data';
15
+ import type { Site, Cell } from '../types';
16
+
17
+ interface Props {
18
+ /** Mapbox access token */
19
+ accessToken?: string;
20
+ /** Initial center coordinates [lng, lat] */
21
+ center?: [number, number];
22
+ /** Initial zoom level */
23
+ zoom?: number;
24
+ }
25
+
26
+ let {
27
+ accessToken = '',
28
+ center = [-122.4194, 37.7749], // San Francisco
29
+ zoom = 12
30
+ }: Props = $props();
31
+
32
+ let mapContainer: HTMLDivElement;
33
+ let map = $state<mapboxgl.Map | null>(null);
34
+ let mapReady = $state(false);
35
+
36
+ // Data
37
+ let sites = $state<Site[]>(demoSites);
38
+ let cells = $state<Cell[]>(demoCells);
39
+ let filteredSites = $state<Site[]>(sites);
40
+
41
+ onMount(() => {
42
+ // Initialize Mapbox map
43
+ map = new mapboxgl.Map({
44
+ container: mapContainer,
45
+ style: 'mapbox://styles/mapbox/streets-v12',
46
+ center,
47
+ zoom,
48
+ accessToken
49
+ });
50
+
51
+ // Add navigation controls
52
+ map.addControl(new mapboxgl.NavigationControl(), 'top-right');
53
+
54
+ // Wait for map to load
55
+ map.on('load', () => {
56
+ mapReady = true;
57
+ });
58
+
59
+ return () => {
60
+ map?.remove();
61
+ };
62
+ });
63
+ </script>
64
+
65
+ <div class="demo-map-container">
66
+ <div bind:this={mapContainer} class="w-100 h-100"></div>
67
+
68
+ {#if !mapReady}
69
+ <div class="position-absolute top-50 start-50 translate-middle bg-white p-4 rounded shadow">
70
+ <div class="spinner-border text-primary me-2" role="status">
71
+ <span class="visually-hidden">Loading...</span>
72
+ </div>
73
+ <span>Loading map...</span>
74
+ </div>
75
+ {/if}
76
+
77
+ {#if mapReady && map}
78
+ <MapboxProvider mapInstance={map}>
79
+ <CellDataProvider sites={filteredSites} {cells}>
80
+ <SitesLayer />
81
+ <!-- <CellsLayer /> -->
82
+ <SiteFilterControl {sites} bind:filteredSites position="top-left" />
83
+ </CellDataProvider>
84
+ </MapboxProvider>
85
+ {/if}
86
+ </div>
87
+
88
+ <style>
89
+ .demo-map-container {
90
+ position: absolute;
91
+ top: 0;
92
+ left: 0;
93
+ right: 0;
94
+ bottom: 0;
95
+ width: 100%;
96
+ height: 100%;
97
+ }
98
+ </style>
@@ -0,0 +1,12 @@
1
+ import 'mapbox-gl/dist/mapbox-gl.css';
2
+ interface Props {
3
+ /** Mapbox access token */
4
+ accessToken?: string;
5
+ /** Initial center coordinates [lng, lat] */
6
+ center?: [number, number];
7
+ /** Initial zoom level */
8
+ zoom?: number;
9
+ }
10
+ declare const DemoMap: import("svelte").Component<Props, {}, "">;
11
+ type DemoMap = ReturnType<typeof DemoMap>;
12
+ export default DemoMap;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Demo cellular data for testing and examples
3
+ */
4
+ import type { Site, Cell } from '../types';
5
+ /**
6
+ * Sample cellular sites (San Francisco area)
7
+ */
8
+ export declare const demoSites: Site[];
9
+ /**
10
+ * Sample cellular cells/sectors
11
+ */
12
+ export declare const demoCells: Cell[];
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Demo cellular data for testing and examples
3
+ */
4
+ /**
5
+ * Sample cellular sites (San Francisco area)
6
+ */
7
+ export const demoSites = [
8
+ {
9
+ id: 'site-001',
10
+ name: 'Downtown Tower',
11
+ longitude: -122.4194,
12
+ latitude: 37.7749,
13
+ fbands: ['2100', '1800'],
14
+ technology: '5G',
15
+ properties: {
16
+ height: 45,
17
+ type: 'macro'
18
+ },
19
+ cellNames: ['cell-001-1', 'cell-001-2', 'cell-001-3'],
20
+ provider: 'Verizon',
21
+ featureGroup: 'Urban Macro'
22
+ },
23
+ {
24
+ id: 'site-002',
25
+ name: 'Financial District',
26
+ longitude: -122.4008,
27
+ latitude: 37.7946,
28
+ fbands: ['1800', '850'],
29
+ technology: 'LTE',
30
+ properties: {
31
+ height: 60,
32
+ type: 'macro'
33
+ },
34
+ cellNames: ['cell-002-1', 'cell-002-2', 'cell-002-3'],
35
+ provider: 'AT&T',
36
+ featureGroup: 'Urban Macro'
37
+ },
38
+ {
39
+ id: 'site-003',
40
+ name: 'Mission Bay',
41
+ longitude: -122.3912,
42
+ latitude: 37.7699,
43
+ fbands: ['2600'],
44
+ technology: '5G',
45
+ properties: {
46
+ height: 30,
47
+ type: 'micro'
48
+ },
49
+ cellNames: ['cell-003-1', 'cell-003-2'],
50
+ provider: 'T-Mobile',
51
+ featureGroup: 'Small Cell'
52
+ },
53
+ {
54
+ id: 'site-004',
55
+ name: 'Golden Gate Park',
56
+ longitude: -122.4862,
57
+ latitude: 37.7694,
58
+ fbands: ['850', '700'],
59
+ technology: 'LTE',
60
+ properties: {
61
+ height: 40,
62
+ type: 'macro'
63
+ },
64
+ cellNames: ['cell-004-1', 'cell-004-2', 'cell-004-3'],
65
+ provider: 'Verizon',
66
+ featureGroup: 'Rural Macro'
67
+ }
68
+ ];
69
+ /**
70
+ * Sample cellular cells/sectors
71
+ */
72
+ export const demoCells = [
73
+ // Site 001 - Downtown Tower (3 sectors)
74
+ {
75
+ id: 'cell-001-1',
76
+ siteId: 'site-001',
77
+ sector: 1,
78
+ azimuth: 0,
79
+ beamwidth: 65,
80
+ radius: 800,
81
+ properties: {
82
+ band: '2100MHz',
83
+ technology: '5G',
84
+ power: 40
85
+ }
86
+ },
87
+ {
88
+ id: 'cell-001-2',
89
+ siteId: 'site-001',
90
+ sector: 2,
91
+ azimuth: 120,
92
+ beamwidth: 65,
93
+ radius: 800,
94
+ properties: {
95
+ band: '2100MHz',
96
+ technology: '5G',
97
+ power: 40
98
+ }
99
+ },
100
+ {
101
+ id: 'cell-001-3',
102
+ siteId: 'site-001',
103
+ sector: 3,
104
+ azimuth: 240,
105
+ beamwidth: 65,
106
+ radius: 800,
107
+ properties: {
108
+ band: '2100MHz',
109
+ technology: '5G',
110
+ power: 40
111
+ }
112
+ },
113
+ // Site 002 - Financial District (3 sectors)
114
+ {
115
+ id: 'cell-002-1',
116
+ siteId: 'site-002',
117
+ sector: 1,
118
+ azimuth: 30,
119
+ beamwidth: 65,
120
+ radius: 700,
121
+ properties: {
122
+ band: '1800MHz',
123
+ technology: 'LTE',
124
+ power: 38
125
+ }
126
+ },
127
+ {
128
+ id: 'cell-002-2',
129
+ siteId: 'site-002',
130
+ sector: 2,
131
+ azimuth: 150,
132
+ beamwidth: 65,
133
+ radius: 700,
134
+ properties: {
135
+ band: '1800MHz',
136
+ technology: 'LTE',
137
+ power: 38
138
+ }
139
+ },
140
+ {
141
+ id: 'cell-002-3',
142
+ siteId: 'site-002',
143
+ sector: 3,
144
+ azimuth: 270,
145
+ beamwidth: 65,
146
+ radius: 700,
147
+ properties: {
148
+ band: '1800MHz',
149
+ technology: 'LTE',
150
+ power: 38
151
+ }
152
+ },
153
+ // Site 003 - Mission Bay (2 sectors, smaller coverage)
154
+ {
155
+ id: 'cell-003-1',
156
+ siteId: 'site-003',
157
+ sector: 1,
158
+ azimuth: 90,
159
+ beamwidth: 90,
160
+ radius: 400,
161
+ properties: {
162
+ band: '2600MHz',
163
+ technology: '5G',
164
+ power: 30
165
+ }
166
+ },
167
+ {
168
+ id: 'cell-003-2',
169
+ siteId: 'site-003',
170
+ sector: 2,
171
+ azimuth: 270,
172
+ beamwidth: 90,
173
+ radius: 400,
174
+ properties: {
175
+ band: '2600MHz',
176
+ technology: '5G',
177
+ power: 30
178
+ }
179
+ },
180
+ // Site 004 - Golden Gate Park (3 sectors)
181
+ {
182
+ id: 'cell-004-1',
183
+ siteId: 'site-004',
184
+ sector: 1,
185
+ azimuth: 0,
186
+ beamwidth: 120,
187
+ radius: 900,
188
+ properties: {
189
+ band: '850MHz',
190
+ technology: 'LTE',
191
+ power: 43
192
+ }
193
+ },
194
+ {
195
+ id: 'cell-004-2',
196
+ siteId: 'site-004',
197
+ sector: 2,
198
+ azimuth: 120,
199
+ beamwidth: 120,
200
+ radius: 900,
201
+ properties: {
202
+ band: '850MHz',
203
+ technology: 'LTE',
204
+ power: 43
205
+ }
206
+ },
207
+ {
208
+ id: 'cell-004-3',
209
+ siteId: 'site-004',
210
+ sector: 3,
211
+ azimuth: 240,
212
+ beamwidth: 120,
213
+ radius: 900,
214
+ properties: {
215
+ band: '850MHz',
216
+ technology: 'LTE',
217
+ power: 43
218
+ }
219
+ }
220
+ ];