@smartnet360/svelte-components 0.0.76 → 0.0.78

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.
@@ -27,6 +27,7 @@
27
27
  import RepeaterLabelsLayer from '../features/repeaters/layers/RepeaterLabelsLayer.svelte';
28
28
  import RepeaterFilterControl from '../features/repeaters/controls/RepeaterFilterControl.svelte';
29
29
  import FeatureSettingsControl from '../shared/controls/FeatureSettingsControl.svelte';
30
+ import FeatureSelectionControl from '../shared/controls/FeatureSelectionControl.svelte';
30
31
  import { createSiteStoreContext } from '../features/sites/stores/siteStoreContext.svelte';
31
32
  import { createCellStoreContext } from '../features/cells/stores/cellStoreContext.svelte';
32
33
  import { createRepeaterStoreContext } from '../features/repeaters/stores/repeaterStoreContext.svelte';
@@ -87,6 +88,12 @@
87
88
  alert(`Selected ${siteIds.length} sites:\n${siteIds.join(', ')}`);
88
89
  }
89
90
 
91
+ // Handler for generic feature selection action button
92
+ function handleProcessFeatures(featureIds: string[]) {
93
+ console.log('Process features:', featureIds);
94
+ alert(`Selected ${featureIds.length} features:\n${featureIds.join(', ')}`);
95
+ }
96
+
90
97
  // Cell filter grouping configuration
91
98
  const cellLevel1Options: Array<Exclude<CellGroupingField, 'none'>> = [
92
99
  'tech',
@@ -222,6 +229,17 @@
222
229
  actionButtonLabel="Open Cluster KPIs"
223
230
  />
224
231
 
232
+ <!-- Generic feature selection control - works with any layer -->
233
+ <FeatureSelectionControl
234
+ position="bottom-left"
235
+ title="Any Feature"
236
+ icon="cursor-fill"
237
+ iconOnlyWhenCollapsed={useIconHeaders}
238
+ onAction={handleProcessFeatures}
239
+ actionButtonLabel="Process Features"
240
+ featureIcon="pin-map-fill"
241
+ />
242
+
225
243
  <!-- Unified feature settings control - Sites, Cells, and Repeaters -->
226
244
  <FeatureSettingsControl
227
245
  siteStore={siteStore}
@@ -10,7 +10,7 @@
10
10
  const BASE_LAT = 37.7749;
11
11
  const BASE_LNG = -122.4194;
12
12
  // Grid parameters for distributing sites
13
- const NUM_SITES = 10;
13
+ const NUM_SITES = 1700;
14
14
  const GRID_SIZE = 10; // 10×10 grid
15
15
  const LAT_SPACING = 0.01; // ~1.1 km spacing
16
16
  const LNG_SPACING = 0.015; // ~1.1 km spacing (adjusted for longitude)
@@ -56,7 +56,16 @@
56
56
  let treeStore = $state<Writable<TreeStoreValue> | null>(null);
57
57
  let level1 = $state<Exclude<CellGroupingField, 'none'>>(store.groupingConfig.level1);
58
58
  let level2 = $state<CellGroupingField>(store.groupingConfig.level2);
59
-
59
+
60
+ // Track checked paths separately to avoid re-filtering on expand/collapse
61
+ let checkedPaths = $state<Set<string>>(new Set());
62
+
63
+ // When checkedPaths change, re-run the filter
64
+ $effect(() => {
65
+ const newFilteredCells = getFilteredCells(checkedPaths, store.cellsFilteredByStatus);
66
+ store.setFilteredCells(newFilteredCells);
67
+ });
68
+
60
69
  // Track config for localStorage invalidation
61
70
  const STORAGE_CONFIG_KEY = 'cellular-cell-filter:config';
62
71
 
@@ -100,24 +109,30 @@
100
109
  persistState: true,
101
110
  defaultExpandAll: false
102
111
  });
103
-
112
+
104
113
  // Subscribe to tree changes and update filtered cells
105
114
  if (treeStore) {
115
+ // This subscription now only syncs the checkedPaths state
116
+ // The $effect above will handle the actual filtering
106
117
  const unsub = treeStore.subscribe((treeValue: TreeStoreValue) => {
107
- const checkedPaths = treeValue.getCheckedPaths();
108
-
109
- // Convert string[] to Set<string>
110
- const checkedPathsSet = new Set(checkedPaths);
111
- const newFilteredCells = getFilteredCells(checkedPathsSet, store.cellsFilteredByStatus); // Use filtered cells
112
-
113
- // Update the cell store directly
114
- store.setFilteredCells(newFilteredCells);
118
+ // getCheckedPaths() is expensive, so check if tree is initializing
119
+ if (treeValue.isInitializing) return;
120
+
121
+ const newCheckedPaths = new Set(treeValue.getCheckedPaths());
122
+
123
+ // Avoid unnecessary updates if the set hasn't changed
124
+ if (
125
+ newCheckedPaths.size !== checkedPaths.size ||
126
+ ![...newCheckedPaths].every((path) => checkedPaths.has(path))
127
+ ) {
128
+ checkedPaths = newCheckedPaths;
129
+ }
115
130
  });
116
-
131
+
117
132
  return () => unsub();
118
133
  }
119
134
  }
120
-
135
+
121
136
  onMount(() => {
122
137
  rebuildTree();
123
138
  });
@@ -5,7 +5,7 @@
5
5
  * Each feature (sites, cells) is completely independent with its own store, layers, and controls.
6
6
  */
7
7
  export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyleControl, createMapStore, createViewportStore, type ViewportStore, type ViewportState, useMapbox, tryUseMapbox } from './core';
8
- export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
8
+ export { MapControl, FeatureSettingsControl, FeatureSelectionControl, createFeatureSelectionStore, FeatureSelectionStore, type SelectedFeature, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
9
9
  export { type Site, type SiteStoreValue, type SiteStoreContext, createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSelectionControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
10
10
  export { type Cell, type CellStatus, type CellStatusStyle, type CellGroupingField, type CellGroupingLabels, type CellTreeConfig, type TechnologyBandKey, type CellStoreValue, type CellStoreContext, createCellStoreContext, CellsLayer, CellLabelsLayer, CellFilterControl, CellStyleControl, cellsToGeoJSON, buildCellTree, getFilteredCells, calculateRadius, getZoomFactor, createArcPolygon, DEFAULT_CELL_TREE_CONFIG, TECHNOLOGY_BAND_COLORS, DEFAULT_STATUS_STYLES, RADIUS_MULTIPLIER } from './features/cells';
11
11
  export { type Repeater, type RepeaterTreeNode, type RepeaterStoreValue, type RepeaterStoreContext, createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER } from './features/repeaters';
@@ -11,7 +11,7 @@ export { MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyle
11
11
  // ============================================================================
12
12
  // SHARED UTILITIES
13
13
  // ============================================================================
14
- export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
14
+ export { MapControl, FeatureSettingsControl, FeatureSelectionControl, createFeatureSelectionStore, FeatureSelectionStore, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
15
15
  // ============================================================================
16
16
  // SITE FEATURE
17
17
  // ============================================================================
@@ -0,0 +1,342 @@
1
+ <script lang="ts">
2
+ /**
3
+ * FeatureSelectionControl - Layer-agnostic feature selection control
4
+ *
5
+ * Features:
6
+ * - Works with ANY map layer that has features with 'id' property
7
+ * - Toggle to enable/disable click-to-select mode
8
+ * - List of selected feature IDs
9
+ * - Individual delete and clear all
10
+ * - Copy feature IDs to clipboard
11
+ * - Custom action button with callback
12
+ * - Self-contained with its own store
13
+ */
14
+
15
+ import { onMount, onDestroy, getContext } from 'svelte';
16
+ import { get } from 'svelte/store';
17
+ import MapControl from './MapControl.svelte';
18
+ import { createFeatureSelectionStore, type SelectedFeature } from './featureSelectionStore.svelte';
19
+ import { MAP_CONTEXT_KEY, type MapStore } from '../../core/types';
20
+
21
+ interface Props {
22
+ /** Control position */
23
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
24
+ /** Control title */
25
+ title?: string;
26
+ /** Optional header icon */
27
+ icon?: string;
28
+ /** Show icon when collapsed (default: true) */
29
+ iconOnlyWhenCollapsed?: boolean;
30
+ /** Callback when action button clicked */
31
+ onAction?: (featureIds: string[]) => void;
32
+ /** Action button label */
33
+ actionButtonLabel?: string;
34
+ /** Feature icon (default: geo-alt-fill) */
35
+ featureIcon?: string;
36
+ }
37
+
38
+ let {
39
+ position = 'top-left',
40
+ title = 'Feature Selection',
41
+ icon = 'check2-square',
42
+ iconOnlyWhenCollapsed = true,
43
+ onAction,
44
+ actionButtonLabel = 'Process',
45
+ featureIcon = 'geo-alt-fill'
46
+ }: Props = $props();
47
+
48
+ // Get map from context
49
+ const mapStore = getContext<MapStore>(MAP_CONTEXT_KEY);
50
+ console.log('[FeatureSelectionControl] mapStore from context:', mapStore);
51
+
52
+ // Create self-contained store
53
+ const store = createFeatureSelectionStore();
54
+ console.log('[FeatureSelectionControl] Store created:', store);
55
+
56
+ let selectedFeatures = $derived(store.getSelectedFeatures());
57
+ let selectionCount = $derived(store.count);
58
+ let hasSelection = $derived(selectionCount > 0);
59
+
60
+ // Subscribe to map store and initialize when map becomes available
61
+ let unsubscribe: (() => void) | null = null;
62
+
63
+ onMount(() => {
64
+ console.log('[FeatureSelectionControl] Component mounted');
65
+
66
+ // Subscribe to map store to get notified when map is ready
67
+ unsubscribe = mapStore.subscribe((map) => {
68
+ console.log('[FeatureSelectionControl] Map store updated:', map);
69
+ if (map && !store['map']) {
70
+ console.log('[FeatureSelectionControl] Setting map on selection store');
71
+ store.setMap(map);
72
+ }
73
+ });
74
+ });
75
+
76
+ onDestroy(() => {
77
+ // Unsubscribe from map store
78
+ if (unsubscribe) {
79
+ unsubscribe();
80
+ }
81
+ // Cleanup event handlers
82
+ store.destroy();
83
+ });
84
+
85
+ function handleToggleMode() {
86
+ store.toggleSelectionMode();
87
+ }
88
+
89
+ function handleRemoveFeature(featureId: string) {
90
+ store.removeFeatureSelection(featureId);
91
+ }
92
+
93
+ function handleClearAll() {
94
+ store.clearSelection();
95
+ }
96
+
97
+ async function handleCopy() {
98
+ const ids = store.getSelectedIds().join(',');
99
+ try {
100
+ await navigator.clipboard.writeText(ids);
101
+ // Optional: Show toast notification
102
+ } catch (err) {
103
+ console.error('Failed to copy:', err);
104
+ }
105
+ }
106
+
107
+ function handleAction() {
108
+ if (onAction && hasSelection) {
109
+ const ids = store.getSelectedIds();
110
+ onAction(ids);
111
+ }
112
+ }
113
+ </script>
114
+
115
+ <MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true}>
116
+ <div class="feature-selection-control">
117
+ <!-- Selection Mode Toggle -->
118
+ <div class="selection-mode-toggle mb-3">
119
+ <div class="form-check form-switch">
120
+ <input
121
+ type="checkbox"
122
+ class="form-check-input"
123
+ id="feature-selection-mode-toggle"
124
+ checked={store.selectionMode}
125
+ onchange={handleToggleMode}
126
+ />
127
+ <label class="form-check-label" for="feature-selection-mode-toggle">
128
+ Selection Mode
129
+ <span class="badge ms-2" class:bg-success={store.selectionMode} class:bg-secondary={!store.selectionMode}>
130
+ {store.selectionMode ? 'ON' : 'OFF'}
131
+ </span>
132
+ </label>
133
+ </div>
134
+ {#if store.selectionMode}
135
+ <small class="text-muted d-block mt-1">
136
+ Click any feature on the map to select
137
+ </small>
138
+ {/if}
139
+ </div>
140
+
141
+ <!-- Selection Stats -->
142
+ <div class="selection-stats mb-2">
143
+ <strong>{selectionCount}</strong>
144
+ {selectionCount === 1 ? 'feature' : 'features'} selected
145
+ </div>
146
+
147
+ <!-- Action Buttons -->
148
+ {#if hasSelection}
149
+ <div class="action-buttons mb-3">
150
+ <div class="btn-group w-100" role="group">
151
+ <button
152
+ type="button"
153
+ class="btn btn-sm btn-outline-danger"
154
+ onclick={handleClearAll}
155
+ title="Clear all"
156
+ >
157
+ <i class="bi bi-trash"></i> Clear
158
+ </button>
159
+ <button
160
+ type="button"
161
+ class="btn btn-sm btn-outline-secondary"
162
+ onclick={handleCopy}
163
+ title="Copy feature IDs"
164
+ >
165
+ <i class="bi bi-clipboard"></i> Copy
166
+ </button>
167
+ </div>
168
+ </div>
169
+ {/if}
170
+
171
+ <!-- Feature List -->
172
+ {#if hasSelection}
173
+ <div class="feature-list">
174
+ {#each selectedFeatures as feature (feature.id)}
175
+ <div class="feature-item">
176
+ <i class="bi bi-{featureIcon} feature-icon"></i>
177
+ <div class="feature-info">
178
+ <span class="feature-id">{feature.id}</span>
179
+ {#if feature.layerId}
180
+ <small class="feature-layer text-muted">{feature.layerId}</small>
181
+ {/if}
182
+ </div>
183
+ <button
184
+ type="button"
185
+ class="btn-remove"
186
+ onclick={() => handleRemoveFeature(feature.id)}
187
+ title="Remove"
188
+ aria-label="Remove {feature.id}"
189
+ >
190
+ <i class="bi bi-x"></i>
191
+ </button>
192
+ </div>
193
+ {/each}
194
+ </div>
195
+ {:else}
196
+ <div class="text-muted small text-center py-2">
197
+ <i class="bi bi-inbox"></i>
198
+ <div class="mt-1">No features selected</div>
199
+ </div>
200
+ {/if}
201
+
202
+ <!-- Action Button -->
203
+ {#if onAction}
204
+ <div class="mt-3">
205
+ <button
206
+ type="button"
207
+ class="btn btn-primary w-100"
208
+ disabled={!hasSelection}
209
+ onclick={handleAction}
210
+ >
211
+ <i class="bi bi-lightning-charge-fill"></i> {actionButtonLabel}
212
+ </button>
213
+ </div>
214
+ {/if}
215
+ </div>
216
+ </MapControl>
217
+
218
+ <style>
219
+ .feature-selection-control {
220
+ width: 100%;
221
+ min-width: 250px;
222
+ }
223
+
224
+ .selection-mode-toggle {
225
+ padding-bottom: 0.75rem;
226
+ border-bottom: 1px solid #dee2e6;
227
+ }
228
+
229
+ .selection-stats {
230
+ font-size: 0.875rem;
231
+ color: #495057;
232
+ }
233
+
234
+ .action-buttons {
235
+ display: flex;
236
+ gap: 0.5rem;
237
+ }
238
+
239
+ .feature-list {
240
+ max-height: 300px;
241
+ overflow-y: auto;
242
+ border: 1px solid #dee2e6;
243
+ border-radius: 4px;
244
+ padding: 0.5rem;
245
+ }
246
+
247
+ .feature-item {
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 0.5rem;
251
+ padding: 0.5rem;
252
+ border-bottom: 1px solid #f1f3f5;
253
+ transition: background-color 0.15s;
254
+ }
255
+
256
+ .feature-item:last-child {
257
+ border-bottom: none;
258
+ }
259
+
260
+ .feature-item:hover {
261
+ background-color: #f8f9fa;
262
+ }
263
+
264
+ .feature-icon {
265
+ font-size: 1rem;
266
+ flex-shrink: 0;
267
+ color: #0d6efd;
268
+ }
269
+
270
+ .feature-info {
271
+ flex: 1;
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: 0.125rem;
275
+ min-width: 0;
276
+ }
277
+
278
+ .feature-id {
279
+ font-family: 'Monaco', 'Courier New', monospace;
280
+ font-size: 0.875rem;
281
+ color: #212529;
282
+ white-space: nowrap;
283
+ overflow: hidden;
284
+ text-overflow: ellipsis;
285
+ }
286
+
287
+ .feature-layer {
288
+ font-size: 0.75rem;
289
+ white-space: nowrap;
290
+ overflow: hidden;
291
+ text-overflow: ellipsis;
292
+ }
293
+
294
+ .btn-remove {
295
+ background: none;
296
+ border: none;
297
+ color: #6c757d;
298
+ font-size: 1.25rem;
299
+ line-height: 1;
300
+ padding: 0;
301
+ width: 24px;
302
+ height: 24px;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ cursor: pointer;
307
+ border-radius: 4px;
308
+ transition: all 0.15s;
309
+ flex-shrink: 0;
310
+ }
311
+
312
+ .btn-remove:hover {
313
+ background-color: #ffe6e6;
314
+ color: #dc3545;
315
+ }
316
+
317
+ .btn-remove:active {
318
+ background-color: #ffcccc;
319
+ }
320
+
321
+ /* Ensure primary action button keeps Bootstrap styling inside Mapbox control */
322
+ .feature-selection-control .btn-primary {
323
+ background-color: var(--bs-btn-bg, var(--bs-primary));
324
+ border-color: var(--bs-btn-border-color, var(--bs-primary));
325
+ color: var(--bs-btn-color, var(--bs-body-color));
326
+ }
327
+
328
+ .feature-selection-control .btn-primary:hover,
329
+ .feature-selection-control .btn-primary:focus {
330
+ background-color: var(--bs-btn-hover-bg, var(--bs-primary));
331
+ border-color: var(--bs-btn-hover-border-color, var(--bs-primary));
332
+ color: var(--bs-btn-hover-color, var(--bs-btn-color, var(--bs-body-color)));
333
+ }
334
+
335
+ .feature-selection-control .btn-primary:disabled,
336
+ .feature-selection-control .btn-primary:disabled:hover {
337
+ background-color: var(--bs-btn-disabled-bg, var(--bs-btn-bg, var(--bs-primary)));
338
+ border-color: var(--bs-btn-disabled-border-color, var(--bs-btn-border-color, var(--bs-primary)));
339
+ color: var(--bs-btn-disabled-color, var(--bs-btn-color, var(--bs-body-color)));
340
+ opacity: var(--bs-btn-disabled-opacity, 0.65);
341
+ }
342
+ </style>
@@ -0,0 +1,19 @@
1
+ interface Props {
2
+ /** Control position */
3
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
4
+ /** Control title */
5
+ title?: string;
6
+ /** Optional header icon */
7
+ icon?: string;
8
+ /** Show icon when collapsed (default: true) */
9
+ iconOnlyWhenCollapsed?: boolean;
10
+ /** Callback when action button clicked */
11
+ onAction?: (featureIds: string[]) => void;
12
+ /** Action button label */
13
+ actionButtonLabel?: string;
14
+ /** Feature icon (default: geo-alt-fill) */
15
+ featureIcon?: string;
16
+ }
17
+ declare const FeatureSelectionControl: import("svelte").Component<Props, {}, "">;
18
+ type FeatureSelectionControl = ReturnType<typeof FeatureSelectionControl>;
19
+ export default FeatureSelectionControl;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Feature Selection Store - Generic layer-agnostic feature selection
3
+ *
4
+ * Manages selection of any map features that have an 'id' property.
5
+ * Self-contained store that can be used independently of specific layers.
6
+ */
7
+ import type { Map as MapboxMap } from 'mapbox-gl';
8
+ export interface SelectedFeature {
9
+ id: string;
10
+ layerId?: string;
11
+ properties?: Record<string, any>;
12
+ }
13
+ export declare class FeatureSelectionStore {
14
+ private selectedFeatures;
15
+ private map;
16
+ selectionMode: boolean;
17
+ private clickHandler;
18
+ constructor();
19
+ /**
20
+ * Initialize the store with a map instance
21
+ */
22
+ setMap(mapInstance: MapboxMap): void;
23
+ /**
24
+ * Setup global click handler for any feature
25
+ */
26
+ private setupClickHandler;
27
+ /**
28
+ * Toggle selection mode on/off
29
+ */
30
+ toggleSelectionMode(): void;
31
+ /**
32
+ * Enable selection mode
33
+ */
34
+ enableSelectionMode(): void;
35
+ /**
36
+ * Disable selection mode
37
+ */
38
+ disableSelectionMode(): void;
39
+ /**
40
+ * Toggle a feature in the selection
41
+ */
42
+ toggleFeatureSelection(id: string, layerId?: string, properties?: Record<string, any>): void;
43
+ /**
44
+ * Add a feature to the selection
45
+ */
46
+ addFeatureSelection(id: string, layerId?: string, properties?: Record<string, any>): void;
47
+ /**
48
+ * Remove a feature from the selection
49
+ */
50
+ removeFeatureSelection(id: string): void;
51
+ /**
52
+ * Clear all selections
53
+ */
54
+ clearSelection(): void;
55
+ /**
56
+ * Get all selected features
57
+ */
58
+ getSelectedFeatures(): SelectedFeature[];
59
+ /**
60
+ * Get selected feature IDs only
61
+ */
62
+ getSelectedIds(): string[];
63
+ /**
64
+ * Check if a feature is selected
65
+ */
66
+ isFeatureSelected(id: string): boolean;
67
+ /**
68
+ * Get selection count
69
+ */
70
+ get count(): number;
71
+ /**
72
+ * Cleanup - remove event handlers
73
+ */
74
+ destroy(): void;
75
+ }
76
+ /**
77
+ * Factory function to create a new feature selection store
78
+ */
79
+ export declare function createFeatureSelectionStore(): FeatureSelectionStore;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Feature Selection Store - Generic layer-agnostic feature selection
3
+ *
4
+ * Manages selection of any map features that have an 'id' property.
5
+ * Self-contained store that can be used independently of specific layers.
6
+ */
7
+ export class FeatureSelectionStore {
8
+ selectedFeatures = $state([]);
9
+ map = $state(null);
10
+ selectionMode = $state(false);
11
+ clickHandler = null;
12
+ constructor() { }
13
+ /**
14
+ * Initialize the store with a map instance
15
+ */
16
+ setMap(mapInstance) {
17
+ console.log('[FeatureSelection] setMap called with:', mapInstance);
18
+ this.map = mapInstance;
19
+ this.setupClickHandler();
20
+ }
21
+ /**
22
+ * Setup global click handler for any feature
23
+ */
24
+ setupClickHandler() {
25
+ if (!this.map)
26
+ return;
27
+ this.clickHandler = (e) => {
28
+ console.log('[FeatureSelection] Map clicked, selectionMode:', this.selectionMode);
29
+ if (!this.selectionMode)
30
+ return;
31
+ // Query all rendered features at the click point
32
+ const features = this.map.queryRenderedFeatures(e.point);
33
+ console.log('[FeatureSelection] Features found:', features?.length || 0);
34
+ if (features && features.length > 0) {
35
+ // Get the topmost feature with an id
36
+ for (const feature of features) {
37
+ console.log('[FeatureSelection] Feature:', {
38
+ layer: feature.layer?.id,
39
+ properties: feature.properties,
40
+ id: feature.id
41
+ });
42
+ const featureId = feature.properties?.id || feature.id;
43
+ if (featureId) {
44
+ console.log('[FeatureSelection] Found feature with ID:', featureId);
45
+ this.toggleFeatureSelection(String(featureId), feature.layer?.id, feature.properties || undefined);
46
+ break; // Only select the topmost feature
47
+ }
48
+ else {
49
+ console.log('[FeatureSelection] Feature has no id property');
50
+ }
51
+ }
52
+ }
53
+ };
54
+ console.log('[FeatureSelection] Click handler registered on map');
55
+ this.map.on('click', this.clickHandler);
56
+ }
57
+ /**
58
+ * Toggle selection mode on/off
59
+ */
60
+ toggleSelectionMode() {
61
+ this.selectionMode = !this.selectionMode;
62
+ console.log('[FeatureSelection] Selection mode toggled to:', this.selectionMode);
63
+ }
64
+ /**
65
+ * Enable selection mode
66
+ */
67
+ enableSelectionMode() {
68
+ this.selectionMode = true;
69
+ }
70
+ /**
71
+ * Disable selection mode
72
+ */
73
+ disableSelectionMode() {
74
+ this.selectionMode = false;
75
+ }
76
+ /**
77
+ * Toggle a feature in the selection
78
+ */
79
+ toggleFeatureSelection(id, layerId, properties) {
80
+ const index = this.selectedFeatures.findIndex(f => f.id === id);
81
+ if (index >= 0) {
82
+ // Remove if already selected
83
+ this.selectedFeatures.splice(index, 1);
84
+ }
85
+ else {
86
+ // Add if not selected
87
+ this.selectedFeatures.push({ id, layerId, properties });
88
+ }
89
+ }
90
+ /**
91
+ * Add a feature to the selection
92
+ */
93
+ addFeatureSelection(id, layerId, properties) {
94
+ const exists = this.selectedFeatures.some(f => f.id === id);
95
+ if (!exists) {
96
+ this.selectedFeatures.push({ id, layerId, properties });
97
+ }
98
+ }
99
+ /**
100
+ * Remove a feature from the selection
101
+ */
102
+ removeFeatureSelection(id) {
103
+ const index = this.selectedFeatures.findIndex(f => f.id === id);
104
+ if (index >= 0) {
105
+ this.selectedFeatures.splice(index, 1);
106
+ }
107
+ }
108
+ /**
109
+ * Clear all selections
110
+ */
111
+ clearSelection() {
112
+ this.selectedFeatures = [];
113
+ }
114
+ /**
115
+ * Get all selected features
116
+ */
117
+ getSelectedFeatures() {
118
+ return this.selectedFeatures;
119
+ }
120
+ /**
121
+ * Get selected feature IDs only
122
+ */
123
+ getSelectedIds() {
124
+ return this.selectedFeatures.map(f => f.id);
125
+ }
126
+ /**
127
+ * Check if a feature is selected
128
+ */
129
+ isFeatureSelected(id) {
130
+ return this.selectedFeatures.some(f => f.id === id);
131
+ }
132
+ /**
133
+ * Get selection count
134
+ */
135
+ get count() {
136
+ return this.selectedFeatures.length;
137
+ }
138
+ /**
139
+ * Cleanup - remove event handlers
140
+ */
141
+ destroy() {
142
+ if (this.map && this.clickHandler) {
143
+ this.map.off('click', this.clickHandler);
144
+ }
145
+ this.clearSelection();
146
+ this.map = null;
147
+ }
148
+ }
149
+ /**
150
+ * Factory function to create a new feature selection store
151
+ */
152
+ export function createFeatureSelectionStore() {
153
+ return new FeatureSelectionStore();
154
+ }
@@ -5,4 +5,7 @@
5
5
  */
6
6
  export { default as MapControl } from './controls/MapControl.svelte';
7
7
  export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
8
+ export { default as FeatureSelectionControl } from './controls/FeatureSelectionControl.svelte';
9
+ export { createFeatureSelectionStore, FeatureSelectionStore } from './controls/featureSelectionStore.svelte';
10
+ export type { SelectedFeature } from './controls/featureSelectionStore.svelte';
8
11
  export { addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './utils/mapboxHelpers';
@@ -6,5 +6,8 @@
6
6
  // Controls
7
7
  export { default as MapControl } from './controls/MapControl.svelte';
8
8
  export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
9
+ export { default as FeatureSelectionControl } from './controls/FeatureSelectionControl.svelte';
10
+ // Feature Selection Store
11
+ export { createFeatureSelectionStore, FeatureSelectionStore } from './controls/featureSelectionStore.svelte';
9
12
  // Mapbox Helpers
10
13
  export { addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './utils/mapboxHelpers';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",