@smartnet360/svelte-components 0.0.57 → 0.0.58
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/map-v2/core/components/ViewportSync.svelte +79 -0
- package/dist/map-v2/core/components/ViewportSync.svelte.d.ts +8 -0
- package/dist/map-v2/core/index.d.ts +2 -0
- package/dist/map-v2/core/index.js +3 -0
- package/dist/map-v2/core/stores/viewportStore.svelte.d.ts +51 -0
- package/dist/map-v2/core/stores/viewportStore.svelte.js +120 -0
- package/dist/map-v2/demo/DemoMap.svelte +30 -3
- package/dist/map-v2/demo/demo-cells.d.ts +12 -0
- package/dist/map-v2/demo/demo-cells.js +114 -0
- package/dist/map-v2/demo/index.d.ts +1 -0
- package/dist/map-v2/demo/index.js +1 -0
- package/dist/map-v2/features/cells/constants/colors.d.ts +7 -0
- package/dist/map-v2/features/cells/constants/colors.js +21 -0
- package/dist/map-v2/features/cells/constants/radiusMultipliers.d.ts +8 -0
- package/dist/map-v2/features/cells/constants/radiusMultipliers.js +22 -0
- package/dist/map-v2/features/cells/constants/statusStyles.d.ts +7 -0
- package/dist/map-v2/features/cells/constants/statusStyles.js +49 -0
- package/dist/map-v2/features/cells/constants/zIndex.d.ts +14 -0
- package/dist/map-v2/features/cells/constants/zIndex.js +28 -0
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +242 -0
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte.d.ts +14 -0
- package/dist/map-v2/features/cells/controls/CellStyleControl.svelte +139 -0
- package/dist/map-v2/features/cells/controls/CellStyleControl.svelte.d.ts +14 -0
- package/dist/map-v2/features/cells/index.d.ts +20 -0
- package/dist/map-v2/features/cells/index.js +24 -0
- package/dist/map-v2/features/cells/layers/CellsLayer.svelte +195 -0
- package/dist/map-v2/features/cells/layers/CellsLayer.svelte.d.ts +10 -0
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.d.ts +46 -0
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.js +137 -0
- package/dist/map-v2/features/cells/types.d.ts +99 -0
- package/dist/map-v2/features/cells/types.js +12 -0
- package/dist/map-v2/features/cells/utils/arcGeometry.d.ts +36 -0
- package/dist/map-v2/features/cells/utils/arcGeometry.js +55 -0
- package/dist/map-v2/features/cells/utils/cellGeoJSON.d.ts +22 -0
- package/dist/map-v2/features/cells/utils/cellGeoJSON.js +81 -0
- package/dist/map-v2/features/cells/utils/cellTree.d.ts +25 -0
- package/dist/map-v2/features/cells/utils/cellTree.js +226 -0
- package/dist/map-v2/features/cells/utils/techBandParser.d.ts +11 -0
- package/dist/map-v2/features/cells/utils/techBandParser.js +17 -0
- package/dist/map-v2/features/cells/utils/zoomScaling.d.ts +42 -0
- package/dist/map-v2/features/cells/utils/zoomScaling.js +53 -0
- package/dist/map-v2/index.d.ts +3 -2
- package/dist/map-v2/index.js +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CellFilterControl - Dynamic hierarchical filter control for cells
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Level1 & Level2 grouping dropdown selectors
|
|
7
|
+
* - Dynamic tree rebuild on grouping config change
|
|
8
|
+
* - TreeView with checkboxes for filtering
|
|
9
|
+
* - Color pickers on leaf nodes (group-level customization)
|
|
10
|
+
* - Persists tree state and grouping config to localStorage
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { onMount } from 'svelte';
|
|
14
|
+
import type { Writable } from 'svelte/store';
|
|
15
|
+
import MapControl from '../../../shared/controls/MapControl.svelte';
|
|
16
|
+
import TreeView from '../../../../core/TreeView/TreeView.svelte';
|
|
17
|
+
import { createTreeStore } from '../../../../core/TreeView/tree.store';
|
|
18
|
+
import { buildCellTree, getFilteredCells } from '../utils/cellTree';
|
|
19
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
20
|
+
import type { CellGroupingField } from '../types';
|
|
21
|
+
import type { TreeStoreValue } from '../../../../core/TreeView/tree.model';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** Cell store context */
|
|
25
|
+
store: CellStoreContext;
|
|
26
|
+
/** Control position */
|
|
27
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
28
|
+
/** Control title */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Initially collapsed? */
|
|
31
|
+
initiallyCollapsed?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
store,
|
|
36
|
+
position = 'top-left',
|
|
37
|
+
title = 'Cell Filter',
|
|
38
|
+
initiallyCollapsed = false
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
// Grouping options (excluding 'none' from level1)
|
|
42
|
+
const GROUPING_OPTIONS: Exclude<CellGroupingField, 'none'>[] = [
|
|
43
|
+
'tech',
|
|
44
|
+
'band',
|
|
45
|
+
'status',
|
|
46
|
+
'siteId',
|
|
47
|
+
'customSubgroup',
|
|
48
|
+
'type',
|
|
49
|
+
'planner'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Level2 options include 'none' for flat tree
|
|
53
|
+
const LEVEL2_OPTIONS: CellGroupingField[] = ['none', ...GROUPING_OPTIONS];
|
|
54
|
+
|
|
55
|
+
let treeStore = $state<Writable<TreeStoreValue> | null>(null);
|
|
56
|
+
let level1 = $state<Exclude<CellGroupingField, 'none'>>(store.groupingConfig.level1);
|
|
57
|
+
let level2 = $state<CellGroupingField>(store.groupingConfig.level2);
|
|
58
|
+
|
|
59
|
+
// Rebuild tree when grouping config or cells change
|
|
60
|
+
function rebuildTree() {
|
|
61
|
+
if (store.cells.length === 0) return;
|
|
62
|
+
|
|
63
|
+
console.log('CellFilterControl: Building tree with config:', { level1, level2 });
|
|
64
|
+
|
|
65
|
+
const treeNodes = buildCellTree(
|
|
66
|
+
store.cells,
|
|
67
|
+
{ level1, level2 },
|
|
68
|
+
store.groupColorMap
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Create or recreate tree store
|
|
72
|
+
treeStore = createTreeStore({
|
|
73
|
+
nodes: [treeNodes],
|
|
74
|
+
namespace: 'cellular-cell-filter',
|
|
75
|
+
persistState: true,
|
|
76
|
+
defaultExpandAll: false
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log('CellFilterControl: Tree store created');
|
|
80
|
+
|
|
81
|
+
// Subscribe to tree changes and update filtered cells
|
|
82
|
+
if (treeStore) {
|
|
83
|
+
const unsub = treeStore.subscribe((treeValue: TreeStoreValue) => {
|
|
84
|
+
const checkedPaths = treeValue.getCheckedPaths();
|
|
85
|
+
console.log('TreeStore updated, checked paths:', checkedPaths.length);
|
|
86
|
+
|
|
87
|
+
// Convert string[] to Set<string>
|
|
88
|
+
const checkedPathsSet = new Set(checkedPaths);
|
|
89
|
+
const newFilteredCells = getFilteredCells(checkedPathsSet, store.cells);
|
|
90
|
+
console.log('Filtered cells count:', newFilteredCells.length, 'of', store.cells.length);
|
|
91
|
+
|
|
92
|
+
// Update the cell store directly
|
|
93
|
+
store.setFilteredCells(newFilteredCells);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return () => unsub();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onMount(() => {
|
|
101
|
+
console.log('CellFilterControl: Mounted with', store.cells.length, 'cells');
|
|
102
|
+
rebuildTree();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Handle grouping config changes
|
|
106
|
+
function handleLevel1Change(event: Event) {
|
|
107
|
+
const target = event.currentTarget as HTMLSelectElement;
|
|
108
|
+
level1 = target.value as Exclude<CellGroupingField, 'none'>;
|
|
109
|
+
|
|
110
|
+
// Update store config
|
|
111
|
+
store.setGroupingConfig({ level1, level2 });
|
|
112
|
+
|
|
113
|
+
// Rebuild tree
|
|
114
|
+
rebuildTree();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleLevel2Change(event: Event) {
|
|
118
|
+
const target = event.currentTarget as HTMLSelectElement;
|
|
119
|
+
level2 = target.value as CellGroupingField;
|
|
120
|
+
|
|
121
|
+
// Update store config
|
|
122
|
+
store.setGroupingConfig({ level1, level2 });
|
|
123
|
+
|
|
124
|
+
// Rebuild tree
|
|
125
|
+
rebuildTree();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleColorChange(groupKey: string, color: string) {
|
|
129
|
+
store.setGroupColor(groupKey, color);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Label mappings for display
|
|
133
|
+
const FIELD_LABELS: Record<CellGroupingField | 'none', string> = {
|
|
134
|
+
tech: 'Technology',
|
|
135
|
+
band: 'Frequency Band',
|
|
136
|
+
status: 'Status',
|
|
137
|
+
siteId: 'Site ID',
|
|
138
|
+
customSubgroup: 'Custom Subgroup',
|
|
139
|
+
type: 'Type',
|
|
140
|
+
planner: 'Planner',
|
|
141
|
+
none: 'None (2-Level Tree)'
|
|
142
|
+
};
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
|
|
146
|
+
<div class="cell-filter-control">
|
|
147
|
+
<!-- Grouping Configuration -->
|
|
148
|
+
<div class="grouping-config">
|
|
149
|
+
<div class="mb-2">
|
|
150
|
+
<label for="level1-select" class="form-label small mb-1">Level 1 Grouping</label>
|
|
151
|
+
<select
|
|
152
|
+
id="level1-select"
|
|
153
|
+
class="form-select form-select-sm"
|
|
154
|
+
value={level1}
|
|
155
|
+
onchange={handleLevel1Change}
|
|
156
|
+
>
|
|
157
|
+
{#each GROUPING_OPTIONS as option}
|
|
158
|
+
<option value={option}>{FIELD_LABELS[option]}</option>
|
|
159
|
+
{/each}
|
|
160
|
+
</select>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="mb-3">
|
|
164
|
+
<label for="level2-select" class="form-label small mb-1">Level 2 Grouping</label>
|
|
165
|
+
<select
|
|
166
|
+
id="level2-select"
|
|
167
|
+
class="form-select form-select-sm"
|
|
168
|
+
value={level2}
|
|
169
|
+
onchange={handleLevel2Change}
|
|
170
|
+
>
|
|
171
|
+
{#each LEVEL2_OPTIONS as option}
|
|
172
|
+
<option value={option}>{FIELD_LABELS[option]}</option>
|
|
173
|
+
{/each}
|
|
174
|
+
</select>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Tree View -->
|
|
179
|
+
{#if treeStore && $treeStore}
|
|
180
|
+
<div class="cell-filter-tree">
|
|
181
|
+
<TreeView store={$treeStore} showControls={false}>
|
|
182
|
+
{#snippet children({ node, state })}
|
|
183
|
+
<!-- Custom node rendering with color picker for leaf nodes -->
|
|
184
|
+
{#if node.metadata?.isLeafGroup}
|
|
185
|
+
<input
|
|
186
|
+
type="color"
|
|
187
|
+
class="color-picker"
|
|
188
|
+
value={node.metadata.color || '#888888'}
|
|
189
|
+
oninput={(e) => handleColorChange(node.id, e.currentTarget.value)}
|
|
190
|
+
onclick={(e) => e.stopPropagation()}
|
|
191
|
+
title="Choose color for this cell group"
|
|
192
|
+
/>
|
|
193
|
+
{/if}
|
|
194
|
+
{/snippet}
|
|
195
|
+
</TreeView>
|
|
196
|
+
|
|
197
|
+
<div class="cell-filter-stats">
|
|
198
|
+
<small class="text-muted">
|
|
199
|
+
Showing {store.filteredCells.length} of {store.cells.length} cells
|
|
200
|
+
</small>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
{:else}
|
|
204
|
+
<div class="text-muted small">Loading cells...</div>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
</MapControl>
|
|
208
|
+
|
|
209
|
+
<style>
|
|
210
|
+
.cell-filter-control {
|
|
211
|
+
min-width: 280px;
|
|
212
|
+
max-width: 320px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.grouping-config {
|
|
216
|
+
padding-bottom: 0.5rem;
|
|
217
|
+
border-bottom: 1px solid #dee2e6;
|
|
218
|
+
margin-bottom: 0.75rem;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cell-filter-tree {
|
|
222
|
+
max-height: 400px;
|
|
223
|
+
overflow-y: auto;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cell-filter-stats {
|
|
227
|
+
margin-top: 8px;
|
|
228
|
+
padding-top: 8px;
|
|
229
|
+
border-top: 1px solid #dee2e6;
|
|
230
|
+
text-align: center;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.color-picker {
|
|
234
|
+
width: 24px;
|
|
235
|
+
height: 24px;
|
|
236
|
+
border: 1px solid #ccc;
|
|
237
|
+
border-radius: 4px;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
margin-left: 8px;
|
|
240
|
+
vertical-align: middle;
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Cell store context */
|
|
4
|
+
store: CellStoreContext;
|
|
5
|
+
/** Control position */
|
|
6
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
|
+
/** Control title */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Initially collapsed? */
|
|
10
|
+
initiallyCollapsed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const CellFilterControl: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type CellFilterControl = ReturnType<typeof CellFilterControl>;
|
|
14
|
+
export default CellFilterControl;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CellStyleControl - Visual settings control for cells
|
|
4
|
+
*
|
|
5
|
+
* Sliders for:
|
|
6
|
+
* - Base radius
|
|
7
|
+
* - Line width
|
|
8
|
+
* - Fill opacity
|
|
9
|
+
* - Show/hide cells
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import MapControl from '../../../shared/controls/MapControl.svelte';
|
|
13
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Cell store context */
|
|
17
|
+
store: CellStoreContext;
|
|
18
|
+
/** Control position */
|
|
19
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
20
|
+
/** Control title */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** Initially collapsed? */
|
|
23
|
+
initiallyCollapsed?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
store,
|
|
28
|
+
position = 'bottom-left',
|
|
29
|
+
title = 'Cell Display',
|
|
30
|
+
initiallyCollapsed = false
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
|
|
35
|
+
<div class="cell-style-controls">
|
|
36
|
+
<!-- Show cells toggle -->
|
|
37
|
+
<div class="control-row">
|
|
38
|
+
<label for="cell-show-toggle" class="control-label">
|
|
39
|
+
Show Cells
|
|
40
|
+
</label>
|
|
41
|
+
<div class="form-check form-switch">
|
|
42
|
+
<input
|
|
43
|
+
id="cell-show-toggle"
|
|
44
|
+
type="checkbox"
|
|
45
|
+
class="form-check-input"
|
|
46
|
+
role="switch"
|
|
47
|
+
bind:checked={store.showCells}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Base radius slider -->
|
|
53
|
+
<div class="control-row">
|
|
54
|
+
<label for="cell-radius-slider" class="control-label">
|
|
55
|
+
Base Radius: <strong>{store.baseRadius}m</strong>
|
|
56
|
+
</label>
|
|
57
|
+
<input
|
|
58
|
+
id="cell-radius-slider"
|
|
59
|
+
type="range"
|
|
60
|
+
class="form-range"
|
|
61
|
+
min="100"
|
|
62
|
+
max="2000"
|
|
63
|
+
step="50"
|
|
64
|
+
bind:value={store.baseRadius}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Line width slider -->
|
|
69
|
+
<div class="control-row">
|
|
70
|
+
<label for="cell-line-width-slider" class="control-label">
|
|
71
|
+
Line Width: <strong>{store.lineWidth}px</strong>
|
|
72
|
+
</label>
|
|
73
|
+
<input
|
|
74
|
+
id="cell-line-width-slider"
|
|
75
|
+
type="range"
|
|
76
|
+
class="form-range"
|
|
77
|
+
min="1"
|
|
78
|
+
max="5"
|
|
79
|
+
step="0.5"
|
|
80
|
+
bind:value={store.lineWidth}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Fill opacity slider -->
|
|
85
|
+
<div class="control-row">
|
|
86
|
+
<label for="cell-opacity-slider" class="control-label">
|
|
87
|
+
Fill Opacity: <strong>{Math.round(store.fillOpacity * 100)}%</strong>
|
|
88
|
+
</label>
|
|
89
|
+
<input
|
|
90
|
+
id="cell-opacity-slider"
|
|
91
|
+
type="range"
|
|
92
|
+
class="form-range"
|
|
93
|
+
min="0"
|
|
94
|
+
max="1"
|
|
95
|
+
step="0.1"
|
|
96
|
+
bind:value={store.fillOpacity}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</MapControl>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
.cell-style-controls {
|
|
104
|
+
min-width: 250px;
|
|
105
|
+
padding: 0.5rem 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.control-row {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
gap: 1rem;
|
|
113
|
+
margin-bottom: 1rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.control-row:last-child {
|
|
117
|
+
margin-bottom: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.control-label {
|
|
121
|
+
font-size: 0.875rem;
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
margin: 0;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.form-range {
|
|
128
|
+
flex: 1;
|
|
129
|
+
min-width: 120px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.form-check {
|
|
133
|
+
margin: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.form-switch .form-check-input {
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Cell store context */
|
|
4
|
+
store: CellStoreContext;
|
|
5
|
+
/** Control position */
|
|
6
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
|
+
/** Control title */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Initially collapsed? */
|
|
10
|
+
initiallyCollapsed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const CellStyleControl: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type CellStyleControl = ReturnType<typeof CellStyleControl>;
|
|
14
|
+
export default CellStyleControl;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell Feature - Public API
|
|
3
|
+
*
|
|
4
|
+
* Exports all cell-related types, components, utilities, and constants
|
|
5
|
+
*/
|
|
6
|
+
export type { Cell, CellStatus, CellStatusStyle, TechnologyBandKey, ParsedTechBand, CellGroupingField, CellTreeConfig } from './types';
|
|
7
|
+
export { DEFAULT_CELL_TREE_CONFIG } from './types';
|
|
8
|
+
export { TECHNOLOGY_BAND_COLORS } from './constants/colors';
|
|
9
|
+
export { RADIUS_MULTIPLIER } from './constants/radiusMultipliers';
|
|
10
|
+
export { Z_INDEX_BY_BAND, CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from './constants/zIndex';
|
|
11
|
+
export { DEFAULT_STATUS_STYLES } from './constants/statusStyles';
|
|
12
|
+
export { createCellStoreContext, type CellStoreContext, type CellStoreValue } from './stores/cellStoreContext.svelte';
|
|
13
|
+
export { default as CellsLayer } from './layers/CellsLayer.svelte';
|
|
14
|
+
export { default as CellFilterControl } from './controls/CellFilterControl.svelte';
|
|
15
|
+
export { default as CellStyleControl } from './controls/CellStyleControl.svelte';
|
|
16
|
+
export { parseTechBand } from './utils/techBandParser';
|
|
17
|
+
export { getZoomFactor, calculateRadius } from './utils/zoomScaling';
|
|
18
|
+
export { createArcPolygon } from './utils/arcGeometry';
|
|
19
|
+
export { buildCellTree, getFilteredCells } from './utils/cellTree';
|
|
20
|
+
export { cellsToGeoJSON } from './utils/cellGeoJSON';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell Feature - Public API
|
|
3
|
+
*
|
|
4
|
+
* Exports all cell-related types, components, utilities, and constants
|
|
5
|
+
*/
|
|
6
|
+
export { DEFAULT_CELL_TREE_CONFIG } from './types';
|
|
7
|
+
// Constants
|
|
8
|
+
export { TECHNOLOGY_BAND_COLORS } from './constants/colors';
|
|
9
|
+
export { RADIUS_MULTIPLIER } from './constants/radiusMultipliers';
|
|
10
|
+
export { Z_INDEX_BY_BAND, CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from './constants/zIndex';
|
|
11
|
+
export { DEFAULT_STATUS_STYLES } from './constants/statusStyles';
|
|
12
|
+
// Store
|
|
13
|
+
export { createCellStoreContext } from './stores/cellStoreContext.svelte';
|
|
14
|
+
// Layers
|
|
15
|
+
export { default as CellsLayer } from './layers/CellsLayer.svelte';
|
|
16
|
+
// Controls
|
|
17
|
+
export { default as CellFilterControl } from './controls/CellFilterControl.svelte';
|
|
18
|
+
export { default as CellStyleControl } from './controls/CellStyleControl.svelte';
|
|
19
|
+
// Utilities
|
|
20
|
+
export { parseTechBand } from './utils/techBandParser';
|
|
21
|
+
export { getZoomFactor, calculateRadius } from './utils/zoomScaling';
|
|
22
|
+
export { createArcPolygon } from './utils/arcGeometry';
|
|
23
|
+
export { buildCellTree, getFilteredCells } from './utils/cellTree';
|
|
24
|
+
export { cellsToGeoJSON } from './utils/cellGeoJSON';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CellsLayer - Renders cell sectors as arc polygons
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Mapbox fill layer (colored arcs by tech-band)
|
|
7
|
+
* - Mapbox line layer (borders styled by status)
|
|
8
|
+
* - Zoom-reactive: Regenerates arcs on zoomend
|
|
9
|
+
* - Updates when filteredCells or store settings change
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getContext, onDestroy, onMount } from 'svelte';
|
|
13
|
+
import type { Map as MapboxMap } from 'mapbox-gl';
|
|
14
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
15
|
+
import { useMapbox } from '../../../core/hooks/useMapbox';
|
|
16
|
+
import { cellsToGeoJSON } from '../utils/cellGeoJSON';
|
|
17
|
+
import { CELL_FILL_Z_INDEX, CELL_LINE_Z_INDEX } from '../constants/zIndex';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Cell store context */
|
|
21
|
+
store: CellStoreContext;
|
|
22
|
+
/** Unique namespace for layer IDs */
|
|
23
|
+
namespace: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { store, namespace }: Props = $props();
|
|
27
|
+
|
|
28
|
+
const FILL_LAYER_ID = `${namespace}-cells-fill`;
|
|
29
|
+
const LINE_LAYER_ID = `${namespace}-cells-line`;
|
|
30
|
+
const SOURCE_ID = `${namespace}-cells`;
|
|
31
|
+
|
|
32
|
+
// Get map from mapbox hook
|
|
33
|
+
const mapStore = useMapbox();
|
|
34
|
+
|
|
35
|
+
let map = $state<MapboxMap | null>(null);
|
|
36
|
+
let mounted = $state(false);
|
|
37
|
+
|
|
38
|
+
// Zoom change handler
|
|
39
|
+
function handleZoomEnd() {
|
|
40
|
+
if (!map) return;
|
|
41
|
+
const newZoom = map.getZoom();
|
|
42
|
+
store.setCurrentZoom(newZoom);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onMount(() => {
|
|
46
|
+
console.log('CellsLayer: onMount, waiting for map...');
|
|
47
|
+
|
|
48
|
+
// Subscribe to map store
|
|
49
|
+
const unsubscribe = mapStore.subscribe((mapInstance) => {
|
|
50
|
+
if (mapInstance && !map) {
|
|
51
|
+
console.log('CellsLayer: Map available, initializing...');
|
|
52
|
+
map = mapInstance;
|
|
53
|
+
mounted = true;
|
|
54
|
+
|
|
55
|
+
// Set initial zoom
|
|
56
|
+
store.setCurrentZoom(mapInstance.getZoom());
|
|
57
|
+
|
|
58
|
+
// Listen to zoom changes
|
|
59
|
+
mapInstance.on('zoomend', handleZoomEnd);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
unsubscribe();
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
onDestroy(() => {
|
|
69
|
+
if (!map) return;
|
|
70
|
+
|
|
71
|
+
map.off('zoomend', handleZoomEnd);
|
|
72
|
+
|
|
73
|
+
// Clean up layers and source
|
|
74
|
+
if (map.getLayer(LINE_LAYER_ID)) {
|
|
75
|
+
map.removeLayer(LINE_LAYER_ID);
|
|
76
|
+
}
|
|
77
|
+
if (map.getLayer(FILL_LAYER_ID)) {
|
|
78
|
+
map.removeLayer(FILL_LAYER_ID);
|
|
79
|
+
}
|
|
80
|
+
if (map.getSource(SOURCE_ID)) {
|
|
81
|
+
map.removeSource(SOURCE_ID);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Reactive: Update GeoJSON when cells/zoom/filters/settings change
|
|
86
|
+
$effect(() => {
|
|
87
|
+
console.log('CellsLayer $effect triggered:', {
|
|
88
|
+
mounted,
|
|
89
|
+
hasMap: !!map,
|
|
90
|
+
showCells: store.showCells,
|
|
91
|
+
filteredCellsCount: store.filteredCells.length,
|
|
92
|
+
currentZoom: store.currentZoom,
|
|
93
|
+
baseRadius: store.baseRadius
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!mounted || !map || !store.showCells) {
|
|
97
|
+
// Remove layers if showCells is false
|
|
98
|
+
if (mounted && map) {
|
|
99
|
+
console.log('CellsLayer: Removing layers (showCells=false or not mounted)');
|
|
100
|
+
if (map.getLayer(LINE_LAYER_ID)) {
|
|
101
|
+
map.removeLayer(LINE_LAYER_ID);
|
|
102
|
+
}
|
|
103
|
+
if (map.getLayer(FILL_LAYER_ID)) {
|
|
104
|
+
map.removeLayer(FILL_LAYER_ID);
|
|
105
|
+
}
|
|
106
|
+
if (map.getSource(SOURCE_ID)) {
|
|
107
|
+
map.removeSource(SOURCE_ID);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Generate GeoJSON from filtered cells
|
|
114
|
+
const geoJSON = cellsToGeoJSON(
|
|
115
|
+
store.filteredCells,
|
|
116
|
+
store.currentZoom,
|
|
117
|
+
store.baseRadius,
|
|
118
|
+
store.groupColorMap
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
console.log('CellsLayer: Generated GeoJSON:', {
|
|
122
|
+
featureCount: geoJSON.features.length,
|
|
123
|
+
firstFeature: geoJSON.features[0]
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Update or create source
|
|
127
|
+
const source = map.getSource(SOURCE_ID);
|
|
128
|
+
if (source && source.type === 'geojson') {
|
|
129
|
+
console.log('CellsLayer: Updating existing source');
|
|
130
|
+
source.setData(geoJSON);
|
|
131
|
+
} else {
|
|
132
|
+
console.log('CellsLayer: Creating new source');
|
|
133
|
+
map.addSource(SOURCE_ID, {
|
|
134
|
+
type: 'geojson',
|
|
135
|
+
data: geoJSON
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add fill layer if not exists
|
|
140
|
+
if (!map.getLayer(FILL_LAYER_ID)) {
|
|
141
|
+
console.log('CellsLayer: Creating fill layer');
|
|
142
|
+
map.addLayer({
|
|
143
|
+
id: FILL_LAYER_ID,
|
|
144
|
+
type: 'fill',
|
|
145
|
+
source: SOURCE_ID,
|
|
146
|
+
paint: {
|
|
147
|
+
'fill-color': [
|
|
148
|
+
'coalesce',
|
|
149
|
+
['get', 'groupColor'],
|
|
150
|
+
['get', 'techBandColor'],
|
|
151
|
+
'#888888' // Fallback
|
|
152
|
+
],
|
|
153
|
+
'fill-opacity': store.fillOpacity
|
|
154
|
+
},
|
|
155
|
+
metadata: {
|
|
156
|
+
zIndex: CELL_FILL_Z_INDEX
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
// Update fill opacity
|
|
161
|
+
console.log('CellsLayer: Updating fill opacity:', store.fillOpacity);
|
|
162
|
+
map.setPaintProperty(FILL_LAYER_ID, 'fill-opacity', store.fillOpacity);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add line layer if not exists
|
|
166
|
+
if (!map.getLayer(LINE_LAYER_ID)) {
|
|
167
|
+
console.log('CellsLayer: Creating line layer');
|
|
168
|
+
map.addLayer({
|
|
169
|
+
id: LINE_LAYER_ID,
|
|
170
|
+
type: 'line',
|
|
171
|
+
source: SOURCE_ID,
|
|
172
|
+
paint: {
|
|
173
|
+
'line-color': ['get', 'lineColor'],
|
|
174
|
+
'line-width': store.lineWidth,
|
|
175
|
+
'line-opacity': ['get', 'lineOpacity'],
|
|
176
|
+
'line-dasharray': [
|
|
177
|
+
'case',
|
|
178
|
+
['has', 'dashArray'],
|
|
179
|
+
['get', 'dashArray'],
|
|
180
|
+
['literal', [1, 0]] // Solid line default
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
metadata: {
|
|
184
|
+
zIndex: CELL_LINE_Z_INDEX
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
// Update line width
|
|
189
|
+
console.log('CellsLayer: Updating line width:', store.lineWidth);
|
|
190
|
+
map.setPaintProperty(LINE_LAYER_ID, 'line-width', store.lineWidth);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<!-- This component doesn't render DOM, only Mapbox layers -->
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Cell store context */
|
|
4
|
+
store: CellStoreContext;
|
|
5
|
+
/** Unique namespace for layer IDs */
|
|
6
|
+
namespace: string;
|
|
7
|
+
}
|
|
8
|
+
declare const CellsLayer: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type CellsLayer = ReturnType<typeof CellsLayer>;
|
|
10
|
+
export default CellsLayer;
|