@smartnet360/svelte-components 0.0.87 → 0.0.88
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-v3/core/components/MapStoreBridge.svelte +25 -0
- package/dist/map-v3/core/components/MapStoreBridge.svelte.d.ts +8 -0
- package/dist/map-v3/core/index.d.ts +1 -0
- package/dist/map-v3/core/index.js +1 -0
- package/dist/map-v3/demo/DemoMap.svelte +10 -0
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -6
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte.d.ts +3 -0
- package/dist/map-v3/features/cells/constants/statusStyles.d.ts +14 -0
- package/dist/map-v3/features/cells/constants/statusStyles.js +49 -0
- package/dist/map-v3/features/cells/constants.js +27 -13
- package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte +7 -2
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +16 -7
- package/dist/map-v3/features/cells/logic/geometry.js +22 -2
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -0
- package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte +57 -26
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +309 -0
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +20 -0
- package/dist/map-v3/features/selection/index.d.ts +9 -0
- package/dist/map-v3/features/selection/index.js +10 -0
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +209 -0
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +13 -0
- package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +84 -0
- package/dist/map-v3/features/selection/stores/selection.store.svelte.js +174 -0
- package/dist/map-v3/features/selection/types.d.ts +25 -0
- package/dist/map-v3/features/selection/types.js +4 -0
- package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +14 -0
- package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte +57 -26
- package/dist/map-v3/index.d.ts +1 -0
- package/dist/map-v3/index.js +2 -0
- package/dist/map-v3/shared/controls/MapControl.svelte +37 -10
- package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, getContext } from 'svelte';
|
|
3
|
+
import { MapControl } from '../../../shared';
|
|
4
|
+
import type { MapStore } from '../../../core/stores/map.store.svelte';
|
|
5
|
+
import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
|
|
6
|
+
import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
|
|
7
|
+
import { createFeatureSelectionStore } from '../stores/selection.store.svelte';
|
|
8
|
+
import SelectionHighlightLayers from '../layers/SelectionHighlightLayers.svelte';
|
|
9
|
+
import type { SelectedFeature } from '../types';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
cellDataStore?: CellDataStore;
|
|
13
|
+
cellDisplayStore?: CellDisplayStore;
|
|
14
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
15
|
+
title?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
18
|
+
onAction?: (featureIds: string[]) => void;
|
|
19
|
+
actionButtonLabel?: string;
|
|
20
|
+
featureIcon?: string;
|
|
21
|
+
idPropertyOptions?: string[];
|
|
22
|
+
defaultIdProperty?: string;
|
|
23
|
+
highlightColor?: string;
|
|
24
|
+
highlightWidth?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
cellDataStore,
|
|
29
|
+
cellDisplayStore,
|
|
30
|
+
position = 'top-left',
|
|
31
|
+
title = 'Feature Selection',
|
|
32
|
+
icon = 'cursor-fill',
|
|
33
|
+
iconOnlyWhenCollapsed = true,
|
|
34
|
+
onAction,
|
|
35
|
+
actionButtonLabel = 'Process Selection',
|
|
36
|
+
featureIcon = 'geo-alt-fill',
|
|
37
|
+
idPropertyOptions = ['siteId', 'sectorId', 'cellName'],
|
|
38
|
+
defaultIdProperty = 'siteId',
|
|
39
|
+
highlightColor = '#FF6B00',
|
|
40
|
+
highlightWidth = 4
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
|
|
43
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
44
|
+
const store = createFeatureSelectionStore();
|
|
45
|
+
|
|
46
|
+
store.setIdProperty(defaultIdProperty);
|
|
47
|
+
|
|
48
|
+
let selectedFeatures = $derived(store.getSelectedFeatures());
|
|
49
|
+
let selectionCount = $derived(store.count);
|
|
50
|
+
let hasSelection = $derived(selectionCount > 0);
|
|
51
|
+
let isCollapsed = $state(false);
|
|
52
|
+
|
|
53
|
+
$effect(() => {
|
|
54
|
+
const map = mapStore.map;
|
|
55
|
+
if (map && !store['map']) {
|
|
56
|
+
store.setMap(map);
|
|
57
|
+
store.enableSelectionMode();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
onDestroy(() => {
|
|
62
|
+
store.destroy();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function handleCollapseToggle(collapsed: boolean) {
|
|
66
|
+
isCollapsed = collapsed;
|
|
67
|
+
if (collapsed) {
|
|
68
|
+
store.disableSelectionMode();
|
|
69
|
+
} else {
|
|
70
|
+
store.enableSelectionMode();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleIdPropertyChange(event: Event) {
|
|
75
|
+
const target = event.target as HTMLSelectElement;
|
|
76
|
+
store.setIdProperty(target.value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleRemoveFeature(featureId: string) {
|
|
80
|
+
store.removeFeatureSelection(featureId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleClearAll() {
|
|
84
|
+
store.clearSelection();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleCopy() {
|
|
88
|
+
const ids = store.getSelectedIds().join(',');
|
|
89
|
+
try {
|
|
90
|
+
await navigator.clipboard.writeText(ids);
|
|
91
|
+
console.log('[FeatureSelection] Copied to clipboard:', ids);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('[FeatureSelection] Failed to copy:', err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleAction() {
|
|
98
|
+
if (onAction && hasSelection) {
|
|
99
|
+
const ids = store.getSelectedIds();
|
|
100
|
+
onAction(ids);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<MapControl
|
|
106
|
+
{position}
|
|
107
|
+
{title}
|
|
108
|
+
{icon}
|
|
109
|
+
{iconOnlyWhenCollapsed}
|
|
110
|
+
collapsible={true}
|
|
111
|
+
onCollapseToggle={handleCollapseToggle}
|
|
112
|
+
controlWidth="300px"
|
|
113
|
+
>
|
|
114
|
+
<div class="feature-selection-control">
|
|
115
|
+
<!-- ID Property Selector -->
|
|
116
|
+
<div class="mb-3">
|
|
117
|
+
<label for="id-property-select" class="form-label small fw-semibold">Select By</label>
|
|
118
|
+
<select
|
|
119
|
+
id="id-property-select"
|
|
120
|
+
class="form-select form-select-sm"
|
|
121
|
+
value={store.idProperty}
|
|
122
|
+
onchange={handleIdPropertyChange}
|
|
123
|
+
>
|
|
124
|
+
{#each idPropertyOptions as option}
|
|
125
|
+
<option value={option}>{option}</option>
|
|
126
|
+
{/each}
|
|
127
|
+
</select>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Selection Stats -->
|
|
131
|
+
<div class="selection-stats mb-2 text-secondary">
|
|
132
|
+
<strong>{selectionCount}</strong>
|
|
133
|
+
{selectionCount === 1 ? 'item' : 'items'} selected
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Action Buttons -->
|
|
137
|
+
{#if hasSelection}
|
|
138
|
+
<div class="action-buttons mb-3">
|
|
139
|
+
<div class="btn-group w-100" role="group">
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
class="btn btn-sm btn-outline-danger"
|
|
143
|
+
onclick={handleClearAll}
|
|
144
|
+
title="Clear all"
|
|
145
|
+
>
|
|
146
|
+
<i class="bi bi-trash"></i> Clear
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="btn btn-sm btn-outline-secondary"
|
|
151
|
+
onclick={handleCopy}
|
|
152
|
+
title="Copy feature IDs"
|
|
153
|
+
>
|
|
154
|
+
<i class="bi bi-clipboard"></i> Copy
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<!-- Feature List -->
|
|
161
|
+
{#if hasSelection}
|
|
162
|
+
<div class="feature-list">
|
|
163
|
+
{#each selectedFeatures as feature (feature.id)}
|
|
164
|
+
<div class="feature-item">
|
|
165
|
+
<i class="bi bi-{featureIcon} feature-icon"></i>
|
|
166
|
+
<div class="feature-info">
|
|
167
|
+
<span class="feature-id">{feature.id}</span>
|
|
168
|
+
{#if feature.layerId}
|
|
169
|
+
<small class="feature-layer text-muted">{feature.layerId}</small>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
class="btn-remove"
|
|
175
|
+
onclick={() => handleRemoveFeature(feature.id)}
|
|
176
|
+
title="Remove"
|
|
177
|
+
aria-label="Remove {feature.id}"
|
|
178
|
+
>
|
|
179
|
+
<i class="bi bi-x"></i>
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
{/each}
|
|
183
|
+
</div>
|
|
184
|
+
{:else}
|
|
185
|
+
<div class="text-muted small text-center py-3">
|
|
186
|
+
<i class="bi bi-inbox" style="font-size: 2rem; opacity: 0.3;"></i>
|
|
187
|
+
<div class="mt-2">No items selected</div>
|
|
188
|
+
<div class="mt-1">Click on features to select</div>
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
|
|
192
|
+
<!-- Action Button -->
|
|
193
|
+
{#if onAction}
|
|
194
|
+
<div class="mt-3">
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
class="btn btn-primary w-100"
|
|
198
|
+
disabled={!hasSelection}
|
|
199
|
+
onclick={handleAction}
|
|
200
|
+
>
|
|
201
|
+
<i class="bi bi-lightning-charge-fill"></i> {actionButtonLabel}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
</MapControl>
|
|
207
|
+
|
|
208
|
+
<!-- Highlight Layers -->
|
|
209
|
+
<SelectionHighlightLayers
|
|
210
|
+
{selectedFeatures}
|
|
211
|
+
{cellDataStore}
|
|
212
|
+
{cellDisplayStore}
|
|
213
|
+
{highlightColor}
|
|
214
|
+
{highlightWidth}
|
|
215
|
+
/>
|
|
216
|
+
|
|
217
|
+
<style>
|
|
218
|
+
.feature-selection-control {
|
|
219
|
+
width: 100%;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.selection-stats {
|
|
223
|
+
font-size: 0.875rem;
|
|
224
|
+
color: #495057;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.feature-list {
|
|
228
|
+
max-height: 300px;
|
|
229
|
+
overflow-y: auto;
|
|
230
|
+
border: 1px solid #dee2e6;
|
|
231
|
+
border-radius: 4px;
|
|
232
|
+
padding: 0.5rem;
|
|
233
|
+
background: #fff;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.feature-item {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 0.5rem;
|
|
240
|
+
padding: 0.5rem;
|
|
241
|
+
border-bottom: 1px solid #f1f3f5;
|
|
242
|
+
transition: background-color 0.15s;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.feature-item:last-child {
|
|
246
|
+
border-bottom: none;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.feature-item:hover {
|
|
250
|
+
background-color: #f8f9fa;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.feature-icon {
|
|
254
|
+
font-size: 1rem;
|
|
255
|
+
flex-shrink: 0;
|
|
256
|
+
color: #0d6efd;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.feature-info {
|
|
260
|
+
flex: 1;
|
|
261
|
+
display: flex;
|
|
262
|
+
flex-direction: column;
|
|
263
|
+
gap: 0.125rem;
|
|
264
|
+
min-width: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.feature-id {
|
|
268
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
269
|
+
font-size: 0.875rem;
|
|
270
|
+
color: #212529;
|
|
271
|
+
white-space: nowrap;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
text-overflow: ellipsis;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.feature-layer {
|
|
277
|
+
font-size: 0.75rem;
|
|
278
|
+
white-space: nowrap;
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
text-overflow: ellipsis;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.btn-remove {
|
|
284
|
+
background: none;
|
|
285
|
+
border: none;
|
|
286
|
+
color: #6c757d;
|
|
287
|
+
font-size: 1.25rem;
|
|
288
|
+
line-height: 1;
|
|
289
|
+
padding: 0;
|
|
290
|
+
width: 24px;
|
|
291
|
+
height: 24px;
|
|
292
|
+
display: flex;
|
|
293
|
+
align-items: center;
|
|
294
|
+
justify-content: center;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
border-radius: 4px;
|
|
297
|
+
transition: all 0.15s;
|
|
298
|
+
flex-shrink: 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.btn-remove:hover {
|
|
302
|
+
background-color: #ffe6e6;
|
|
303
|
+
color: #dc3545;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.btn-remove:active {
|
|
307
|
+
background-color: #ffcccc;
|
|
308
|
+
}
|
|
309
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
|
|
2
|
+
import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
cellDataStore?: CellDataStore;
|
|
5
|
+
cellDisplayStore?: CellDisplayStore;
|
|
6
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
|
+
title?: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
10
|
+
onAction?: (featureIds: string[]) => void;
|
|
11
|
+
actionButtonLabel?: string;
|
|
12
|
+
featureIcon?: string;
|
|
13
|
+
idPropertyOptions?: string[];
|
|
14
|
+
defaultIdProperty?: string;
|
|
15
|
+
highlightColor?: string;
|
|
16
|
+
highlightWidth?: number;
|
|
17
|
+
}
|
|
18
|
+
declare const FeatureSelectionControl: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type FeatureSelectionControl = ReturnType<typeof FeatureSelectionControl>;
|
|
20
|
+
export default FeatureSelectionControl;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Selection - Public API
|
|
3
|
+
*
|
|
4
|
+
* Exports all selection-related components, stores, and types.
|
|
5
|
+
*/
|
|
6
|
+
export type { SelectedFeature, SelectionStoreState } from './types';
|
|
7
|
+
export { createFeatureSelectionStore, FeatureSelectionStore } from './stores/selection.store.svelte';
|
|
8
|
+
export { default as FeatureSelectionControl } from './components/FeatureSelectionControl.svelte';
|
|
9
|
+
export { default as SelectionHighlightLayers } from './layers/SelectionHighlightLayers.svelte';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Selection - Public API
|
|
3
|
+
*
|
|
4
|
+
* Exports all selection-related components, stores, and types.
|
|
5
|
+
*/
|
|
6
|
+
// Store
|
|
7
|
+
export { createFeatureSelectionStore, FeatureSelectionStore } from './stores/selection.store.svelte';
|
|
8
|
+
// Components
|
|
9
|
+
export { default as FeatureSelectionControl } from './components/FeatureSelectionControl.svelte';
|
|
10
|
+
export { default as SelectionHighlightLayers } from './layers/SelectionHighlightLayers.svelte';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import type { MapStore } from '../../../core/stores/map.store.svelte';
|
|
4
|
+
import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
|
|
5
|
+
import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
|
|
6
|
+
import type { SelectedFeature } from '../types';
|
|
7
|
+
import { generateCellArc, calculateRadiusInMeters } from '../../cells/logic/geometry';
|
|
8
|
+
import type mapboxgl from 'mapbox-gl';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
selectedFeatures: SelectedFeature[];
|
|
12
|
+
cellDataStore?: CellDataStore;
|
|
13
|
+
cellDisplayStore?: CellDisplayStore;
|
|
14
|
+
highlightColor?: string;
|
|
15
|
+
highlightWidth?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
selectedFeatures,
|
|
20
|
+
cellDataStore,
|
|
21
|
+
cellDisplayStore,
|
|
22
|
+
highlightColor = '#FF6B00',
|
|
23
|
+
highlightWidth = 4
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
27
|
+
|
|
28
|
+
let cellsSourceId = 'cells-selection-source';
|
|
29
|
+
let cellsLayerId = 'cells-selection-highlight';
|
|
30
|
+
let sitesSourceId = 'sites-selection-source';
|
|
31
|
+
let sitesLayerId = 'sites-selection-highlight';
|
|
32
|
+
|
|
33
|
+
// Track current zoom level for radius calculation
|
|
34
|
+
let currentZoom = $state(13);
|
|
35
|
+
let centerLat = $state(0);
|
|
36
|
+
|
|
37
|
+
// Update zoom level when map changes
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const map = mapStore.map;
|
|
40
|
+
if (!map) return;
|
|
41
|
+
|
|
42
|
+
const updateZoom = () => {
|
|
43
|
+
currentZoom = map.getZoom();
|
|
44
|
+
centerLat = map.getCenter().lat;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
updateZoom();
|
|
48
|
+
map.on('zoom', updateZoom);
|
|
49
|
+
map.on('move', updateZoom);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
map.off('zoom', updateZoom);
|
|
53
|
+
map.off('move', updateZoom);
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Derive highlighted cells based on selected features
|
|
58
|
+
let highlightedCells = $derived.by(() => {
|
|
59
|
+
if (!cellDataStore) return [];
|
|
60
|
+
|
|
61
|
+
const cellIds = selectedFeatures
|
|
62
|
+
.filter(f => f.layerId === 'cells-layer')
|
|
63
|
+
.map(f => f.id);
|
|
64
|
+
|
|
65
|
+
if (cellIds.length === 0) return [];
|
|
66
|
+
|
|
67
|
+
return cellDataStore.filteredCells.filter(cell =>
|
|
68
|
+
cellIds.includes(cell.siteId) ||
|
|
69
|
+
cellIds.includes(cell.cellName) ||
|
|
70
|
+
cellIds.includes(cell.cellID)
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Derive highlighted sites based on selected features
|
|
75
|
+
// TODO: Add site highlighting when SiteDataStore is available
|
|
76
|
+
let highlightedSites = $derived.by(() => {
|
|
77
|
+
return [];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Generate GeoJSON for cell highlights
|
|
81
|
+
let cellHighlightGeoJSON = $derived.by(() => {
|
|
82
|
+
if (!cellDisplayStore || highlightedCells.length === 0) {
|
|
83
|
+
return { type: 'FeatureCollection', features: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculate radius for current zoom level (same as CellsLayer does)
|
|
87
|
+
const radiusMeters = calculateRadiusInMeters(centerLat, currentZoom, cellDisplayStore.targetPixelSize);
|
|
88
|
+
|
|
89
|
+
const features = highlightedCells.map(cell =>
|
|
90
|
+
generateCellArc(cell, radiusMeters, 100, highlightColor)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
type: 'FeatureCollection' as const,
|
|
95
|
+
features
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Generate GeoJSON for site highlights
|
|
100
|
+
let siteHighlightGeoJSON = $derived.by(() => {
|
|
101
|
+
if (highlightedSites.length === 0) {
|
|
102
|
+
return { type: 'FeatureCollection', features: [] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const features = highlightedSites.map((site: any) => ({
|
|
106
|
+
type: 'Feature' as const,
|
|
107
|
+
geometry: {
|
|
108
|
+
type: 'Point' as const,
|
|
109
|
+
coordinates: [site.longitude, site.latitude]
|
|
110
|
+
},
|
|
111
|
+
properties: { siteId: site.siteId }
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
type: 'FeatureCollection' as const,
|
|
116
|
+
features
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Initialize and manage layers
|
|
121
|
+
$effect(() => {
|
|
122
|
+
const map = mapStore.map;
|
|
123
|
+
if (!map) return;
|
|
124
|
+
|
|
125
|
+
const addLayers = () => {
|
|
126
|
+
// Cell Selection Highlight Layer
|
|
127
|
+
if (!map.getSource(cellsSourceId)) {
|
|
128
|
+
map.addSource(cellsSourceId, {
|
|
129
|
+
type: 'geojson',
|
|
130
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!map.getLayer(cellsLayerId)) {
|
|
135
|
+
map.addLayer({
|
|
136
|
+
id: cellsLayerId,
|
|
137
|
+
type: 'line',
|
|
138
|
+
source: cellsSourceId,
|
|
139
|
+
paint: {
|
|
140
|
+
'line-color': highlightColor,
|
|
141
|
+
'line-width': highlightWidth,
|
|
142
|
+
'line-opacity': 1
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Site Selection Highlight Layer
|
|
148
|
+
if (!map.getSource(sitesSourceId)) {
|
|
149
|
+
map.addSource(sitesSourceId, {
|
|
150
|
+
type: 'geojson',
|
|
151
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!map.getLayer(sitesLayerId)) {
|
|
156
|
+
map.addLayer({
|
|
157
|
+
id: sitesLayerId,
|
|
158
|
+
type: 'circle',
|
|
159
|
+
source: sitesSourceId,
|
|
160
|
+
paint: {
|
|
161
|
+
'circle-radius': 12,
|
|
162
|
+
'circle-color': 'transparent',
|
|
163
|
+
'circle-stroke-color': highlightColor,
|
|
164
|
+
'circle-stroke-width': highlightWidth,
|
|
165
|
+
'circle-stroke-opacity': 1
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Initial setup
|
|
172
|
+
addLayers();
|
|
173
|
+
|
|
174
|
+
// Events
|
|
175
|
+
map.on('style.load', addLayers);
|
|
176
|
+
|
|
177
|
+
// Cleanup
|
|
178
|
+
return () => {
|
|
179
|
+
map.off('style.load', addLayers);
|
|
180
|
+
|
|
181
|
+
if (map.getLayer(cellsLayerId)) map.removeLayer(cellsLayerId);
|
|
182
|
+
if (map.getLayer(sitesLayerId)) map.removeLayer(sitesLayerId);
|
|
183
|
+
if (map.getSource(cellsSourceId)) map.removeSource(cellsSourceId);
|
|
184
|
+
if (map.getSource(sitesSourceId)) map.removeSource(sitesSourceId);
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Update cell highlight source
|
|
189
|
+
$effect(() => {
|
|
190
|
+
const map = mapStore.map;
|
|
191
|
+
if (!map) return;
|
|
192
|
+
|
|
193
|
+
const source = map.getSource(cellsSourceId) as mapboxgl.GeoJSONSource;
|
|
194
|
+
if (source) {
|
|
195
|
+
source.setData(cellHighlightGeoJSON as any);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Update site highlight source
|
|
200
|
+
$effect(() => {
|
|
201
|
+
const map = mapStore.map;
|
|
202
|
+
if (!map) return;
|
|
203
|
+
|
|
204
|
+
const source = map.getSource(sitesSourceId) as mapboxgl.GeoJSONSource;
|
|
205
|
+
if (source) {
|
|
206
|
+
source.setData(siteHighlightGeoJSON as any);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
</script>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
|
|
2
|
+
import type { CellDisplayStore } from '../../cells/stores/cell.display.svelte';
|
|
3
|
+
import type { SelectedFeature } from '../types';
|
|
4
|
+
interface Props {
|
|
5
|
+
selectedFeatures: SelectedFeature[];
|
|
6
|
+
cellDataStore?: CellDataStore;
|
|
7
|
+
cellDisplayStore?: CellDisplayStore;
|
|
8
|
+
highlightColor?: string;
|
|
9
|
+
highlightWidth?: number;
|
|
10
|
+
}
|
|
11
|
+
declare const SelectionHighlightLayers: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type SelectionHighlightLayers = ReturnType<typeof SelectionHighlightLayers>;
|
|
13
|
+
export default SelectionHighlightLayers;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Selection Store - Svelte 5 Runes Implementation
|
|
3
|
+
*
|
|
4
|
+
* Manages selection of map features (cells, sites) with click detection.
|
|
5
|
+
*/
|
|
6
|
+
import type { Map as MapboxMap } from 'mapbox-gl';
|
|
7
|
+
import type { SelectedFeature } from '../types';
|
|
8
|
+
export declare class FeatureSelectionStore {
|
|
9
|
+
private selectedFeatures;
|
|
10
|
+
private map;
|
|
11
|
+
selectionMode: boolean;
|
|
12
|
+
idProperty: string;
|
|
13
|
+
queryLayers: string[];
|
|
14
|
+
private clickHandler;
|
|
15
|
+
private onSelectionChange?;
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the store with a map instance
|
|
18
|
+
*/
|
|
19
|
+
setMap(mapInstance: MapboxMap): void;
|
|
20
|
+
/**
|
|
21
|
+
* Set which property to use as the ID
|
|
22
|
+
*/
|
|
23
|
+
setIdProperty(property: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Set which layers to query for features
|
|
26
|
+
*/
|
|
27
|
+
setQueryLayers(layers: string[]): void;
|
|
28
|
+
/**
|
|
29
|
+
* Register callback for selection changes
|
|
30
|
+
*/
|
|
31
|
+
setSelectionChangeCallback(callback: (selected: SelectedFeature[]) => void): void;
|
|
32
|
+
/**
|
|
33
|
+
* Setup global click handler for any feature
|
|
34
|
+
*/
|
|
35
|
+
private setupClickHandler;
|
|
36
|
+
/**
|
|
37
|
+
* Enable selection mode
|
|
38
|
+
*/
|
|
39
|
+
enableSelectionMode(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Disable selection mode
|
|
42
|
+
*/
|
|
43
|
+
disableSelectionMode(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Toggle a feature in the selection
|
|
46
|
+
*/
|
|
47
|
+
toggleFeatureSelection(id: string, layerId?: string, properties?: Record<string, any>, siteId?: string, cellName?: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Add a feature to the selection
|
|
50
|
+
*/
|
|
51
|
+
addFeatureSelection(id: string, layerId?: string, properties?: Record<string, any>, siteId?: string, cellName?: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Remove a feature from the selection
|
|
54
|
+
*/
|
|
55
|
+
removeFeatureSelection(id: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Clear all selections
|
|
58
|
+
*/
|
|
59
|
+
clearSelection(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Get all selected features
|
|
62
|
+
*/
|
|
63
|
+
getSelectedFeatures(): SelectedFeature[];
|
|
64
|
+
/**
|
|
65
|
+
* Get selected feature IDs only
|
|
66
|
+
*/
|
|
67
|
+
getSelectedIds(): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Check if a feature is selected
|
|
70
|
+
*/
|
|
71
|
+
isFeatureSelected(id: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Get selection count
|
|
74
|
+
*/
|
|
75
|
+
get count(): number;
|
|
76
|
+
/**
|
|
77
|
+
* Cleanup - remove event handlers
|
|
78
|
+
*/
|
|
79
|
+
destroy(): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Factory function to create a new feature selection store
|
|
83
|
+
*/
|
|
84
|
+
export declare function createFeatureSelectionStore(): FeatureSelectionStore;
|