@smartnet360/svelte-components 0.0.51 → 0.0.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/site-check/transforms.js +2 -2
- package/dist/core/Charts/ChartCard.svelte +6 -1
- package/dist/core/Charts/ChartComponent.svelte +8 -3
- package/dist/core/Charts/GlobalControls.svelte +116 -14
- package/dist/core/Charts/adapt.js +1 -1
- package/dist/core/Charts/charts.model.d.ts +3 -0
- package/dist/core/FeatureRegistry/index.js +1 -1
- package/dist/core/TreeView/TreeNode.svelte +40 -45
- package/dist/core/TreeView/TreeNode.svelte.d.ts +10 -0
- package/dist/core/TreeView/TreeView.svelte +14 -2
- package/dist/core/TreeView/TreeView.svelte.d.ts +10 -0
- package/dist/core/TreeView/tree-utils.d.ts +3 -0
- package/dist/core/TreeView/tree-utils.js +33 -9
- package/dist/core/TreeView/tree.store.js +49 -24
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/map/controls/MapControl.svelte +204 -0
- package/dist/map/controls/MapControl.svelte.d.ts +17 -0
- package/dist/map/controls/SiteFilterControl.svelte +126 -0
- package/dist/map/controls/SiteFilterControl.svelte.d.ts +16 -0
- package/dist/map/demo/DemoMap.svelte +98 -0
- package/dist/map/demo/DemoMap.svelte.d.ts +12 -0
- package/dist/map/demo/demo-data.d.ts +12 -0
- package/dist/map/demo/demo-data.js +220 -0
- package/dist/map/hooks/useCellData.d.ts +14 -0
- package/dist/map/hooks/useCellData.js +29 -0
- package/dist/map/hooks/useMapbox.d.ts +14 -0
- package/dist/map/hooks/useMapbox.js +29 -0
- package/dist/map/index.d.ts +27 -0
- package/dist/map/index.js +47 -0
- package/dist/map/layers/CellsLayer.svelte +242 -0
- package/dist/map/layers/CellsLayer.svelte.d.ts +21 -0
- package/dist/map/layers/CoverageLayer.svelte +37 -0
- package/dist/map/layers/CoverageLayer.svelte.d.ts +9 -0
- package/dist/map/layers/LayerBase.d.ts +42 -0
- package/dist/map/layers/LayerBase.js +58 -0
- package/dist/map/layers/SitesLayer.svelte +282 -0
- package/dist/map/layers/SitesLayer.svelte.d.ts +19 -0
- package/dist/map/providers/CellDataProvider.svelte +43 -0
- package/dist/map/providers/CellDataProvider.svelte.d.ts +12 -0
- package/dist/map/providers/MapboxProvider.svelte +38 -0
- package/dist/map/providers/MapboxProvider.svelte.d.ts +9 -0
- package/dist/map/providers/providerHelpers.d.ts +17 -0
- package/dist/map/providers/providerHelpers.js +26 -0
- package/dist/map/stores/cellDataStore.d.ts +21 -0
- package/dist/map/stores/cellDataStore.js +53 -0
- package/dist/map/stores/interactions.d.ts +20 -0
- package/dist/map/stores/interactions.js +33 -0
- package/dist/map/stores/mapStore.d.ts +8 -0
- package/dist/map/stores/mapStore.js +10 -0
- package/dist/map/types.d.ts +115 -0
- package/dist/map/types.js +10 -0
- package/dist/map/utils/geojson.d.ts +20 -0
- package/dist/map/utils/geojson.js +78 -0
- package/dist/map/utils/mapboxHelpers.d.ts +51 -0
- package/dist/map/utils/mapboxHelpers.js +98 -0
- package/dist/map/utils/math.d.ts +40 -0
- package/dist/map/utils/math.js +95 -0
- package/dist/map/utils/siteTreeUtils.d.ts +27 -0
- package/dist/map/utils/siteTreeUtils.js +164 -0
- package/package.json +1 -1
- package/dist/core/Map/Map.svelte +0 -312
- package/dist/core/Map/Map.svelte.d.ts +0 -230
- package/dist/core/Map/index.d.ts +0 -9
- package/dist/core/Map/index.js +0 -9
- package/dist/core/Map/mapSettings.d.ts +0 -147
- package/dist/core/Map/mapSettings.js +0 -226
- package/dist/core/Map/mapStore.d.ts +0 -73
- package/dist/core/Map/mapStore.js +0 -136
- package/dist/core/Map/types.d.ts +0 -72
- package/dist/core/Map/types.js +0 -32
|
@@ -76,21 +76,45 @@ export function flattenTree(nodes, config, parentPath = '', level = 0) {
|
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
78
|
* Calculate which nodes should be indeterminate
|
|
79
|
+
* A node is indeterminate if:
|
|
80
|
+
* - It has children AND
|
|
81
|
+
* - Some (but not all) of its direct children are checked OR indeterminate
|
|
79
82
|
*/
|
|
80
83
|
export function calculateIndeterminateStates(nodes, checkedPaths) {
|
|
81
84
|
const indeterminate = new Set();
|
|
82
|
-
// For each node with children
|
|
83
|
-
|
|
85
|
+
// For each node with children, check from deepest to shallowest
|
|
86
|
+
// This ensures we calculate indeterminate state correctly
|
|
87
|
+
const nodesArray = Array.from(nodes.entries());
|
|
88
|
+
// Sort by level (deepest first) to ensure we process children before parents
|
|
89
|
+
nodesArray.sort((a, b) => b[1].level - a[1].level);
|
|
90
|
+
for (const [path, nodeState] of nodesArray) {
|
|
91
|
+
// Skip leaf nodes - they can't be indeterminate
|
|
84
92
|
if (nodeState.childPaths.length === 0)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
continue;
|
|
94
|
+
// Count direct children that are either checked or indeterminate
|
|
95
|
+
let checkedCount = 0;
|
|
96
|
+
let indeterminateCount = 0;
|
|
97
|
+
for (const childPath of nodeState.childPaths) {
|
|
98
|
+
if (checkedPaths.has(childPath)) {
|
|
99
|
+
checkedCount++;
|
|
100
|
+
}
|
|
101
|
+
else if (indeterminate.has(childPath)) {
|
|
102
|
+
indeterminateCount++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const totalChildren = nodeState.childPaths.length;
|
|
106
|
+
const affectedChildren = checkedCount + indeterminateCount;
|
|
107
|
+
// Node is indeterminate if:
|
|
108
|
+
// 1. Some children are checked/indeterminate, but not all
|
|
109
|
+
// 2. At least one child is indeterminate (even if all are checked/indeterminate)
|
|
110
|
+
if (affectedChildren > 0 && affectedChildren < totalChildren) {
|
|
91
111
|
indeterminate.add(path);
|
|
92
112
|
}
|
|
93
|
-
|
|
113
|
+
else if (indeterminateCount > 0) {
|
|
114
|
+
// If any child is indeterminate, parent is indeterminate
|
|
115
|
+
indeterminate.add(path);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
return indeterminate;
|
|
95
119
|
}
|
|
96
120
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Svelte writable store for managing tree state with persistence
|
|
4
4
|
*/
|
|
5
5
|
import { writable } from 'svelte/store';
|
|
6
|
-
import { flattenTree, buildInitialState, calculateIndeterminateStates, getDescendantPaths, getParentPath, saveStateToStorage, loadStateFromStorage, clearStorageForNamespace } from './tree-utils';
|
|
6
|
+
import { flattenTree, buildInitialState, calculateIndeterminateStates, getDescendantPaths, getParentPath, getAncestorPaths, saveStateToStorage, loadStateFromStorage, clearStorageForNamespace } from './tree-utils';
|
|
7
7
|
import { log } from '../logger';
|
|
8
8
|
/**
|
|
9
9
|
* Create a tree store with state management and persistence
|
|
@@ -70,6 +70,11 @@ export function createTreeStore(config) {
|
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Toggle a node's checked state (with cascading)
|
|
73
|
+
*
|
|
74
|
+
* Logic:
|
|
75
|
+
* 1. Toggle the clicked node
|
|
76
|
+
* 2. Cascade DOWN to all descendants (check/uncheck all children)
|
|
77
|
+
* 3. Propagate UP to all ancestors (update based on their children's states)
|
|
73
78
|
*/
|
|
74
79
|
function toggle(path) {
|
|
75
80
|
log('🔄 Toggling node', { path });
|
|
@@ -81,16 +86,17 @@ export function createTreeStore(config) {
|
|
|
81
86
|
}
|
|
82
87
|
const newChecked = !state.checkedPaths.has(path);
|
|
83
88
|
const newCheckedPaths = new Set(state.checkedPaths);
|
|
84
|
-
|
|
89
|
+
log('📌 Toggle action', { path, newChecked });
|
|
90
|
+
// STEP 1: Update this node
|
|
85
91
|
if (newChecked) {
|
|
86
92
|
newCheckedPaths.add(path);
|
|
87
93
|
}
|
|
88
94
|
else {
|
|
89
95
|
newCheckedPaths.delete(path);
|
|
90
96
|
}
|
|
91
|
-
//
|
|
97
|
+
// STEP 2: CASCADE DOWN - Update all descendants to match
|
|
92
98
|
const descendants = getDescendantPaths(path, state.nodes, separator);
|
|
93
|
-
log('
|
|
99
|
+
log('⬇️ Cascading to descendants', {
|
|
94
100
|
path,
|
|
95
101
|
descendantCount: descendants.length,
|
|
96
102
|
newChecked
|
|
@@ -103,29 +109,48 @@ export function createTreeStore(config) {
|
|
|
103
109
|
newCheckedPaths.delete(descendantPath);
|
|
104
110
|
}
|
|
105
111
|
});
|
|
106
|
-
// Update ancestors
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
// STEP 3: PROPAGATE UP - Update all ancestors based on their children
|
|
113
|
+
// We need to update from the deepest ancestor up to the root
|
|
114
|
+
const ancestorPaths = getAncestorPaths(path, separator);
|
|
115
|
+
log('⬆️ Propagating to ancestors', {
|
|
116
|
+
path,
|
|
117
|
+
ancestorCount: ancestorPaths.length,
|
|
118
|
+
ancestors: ancestorPaths
|
|
119
|
+
});
|
|
120
|
+
// Process ancestors from deepest to shallowest (reverse order)
|
|
121
|
+
// This ensures we calculate states correctly as we go up
|
|
122
|
+
for (let i = ancestorPaths.length - 1; i >= 0; i--) {
|
|
123
|
+
const ancestorPath = ancestorPaths[i];
|
|
124
|
+
const ancestor = state.nodes.get(ancestorPath);
|
|
125
|
+
if (!ancestor)
|
|
126
|
+
continue;
|
|
127
|
+
// Count how many direct children are checked
|
|
128
|
+
const checkedChildrenCount = ancestor.childPaths.filter(childPath => newCheckedPaths.has(childPath)).length;
|
|
129
|
+
const totalChildren = ancestor.childPaths.length;
|
|
130
|
+
log('👨👧👦 Checking ancestor children', {
|
|
131
|
+
ancestorPath,
|
|
132
|
+
checkedChildrenCount,
|
|
133
|
+
totalChildren
|
|
134
|
+
});
|
|
135
|
+
// Update ancestor based on children states:
|
|
136
|
+
// - All children checked → check parent
|
|
137
|
+
// - No children checked → uncheck parent
|
|
138
|
+
// - Some children checked → uncheck parent (will show indeterminate)
|
|
139
|
+
if (checkedChildrenCount === totalChildren) {
|
|
140
|
+
newCheckedPaths.add(ancestorPath);
|
|
141
|
+
log('✅ All children checked, checking parent', { ancestorPath });
|
|
120
142
|
}
|
|
121
|
-
else
|
|
122
|
-
newCheckedPaths.delete(
|
|
143
|
+
else {
|
|
144
|
+
newCheckedPaths.delete(ancestorPath);
|
|
145
|
+
if (checkedChildrenCount > 0) {
|
|
146
|
+
log('➖ Some children checked, parent will be indeterminate', { ancestorPath });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
log('❌ No children checked, unchecking parent', { ancestorPath });
|
|
150
|
+
}
|
|
123
151
|
}
|
|
124
|
-
// If some checked, parent state depends on whether we're checking or unchecking
|
|
125
|
-
// For better UX: leave parent as-is (will show indeterminate)
|
|
126
|
-
currentPath = parentPath;
|
|
127
152
|
}
|
|
128
|
-
// Recalculate indeterminate states
|
|
153
|
+
// STEP 4: Recalculate indeterminate states
|
|
129
154
|
const newIndeterminatePaths = calculateIndeterminateStates(state.nodes, newCheckedPaths);
|
|
130
155
|
log('✅ Toggle complete', {
|
|
131
156
|
path,
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from './Settings/index.js';
|
|
|
11
11
|
// Logger utility for debugging and monitoring
|
|
12
12
|
export * from './logger/index.js';
|
|
13
13
|
// Map component - Mapbox GL + Deck.GL integration
|
|
14
|
-
|
|
14
|
+
// TODO: Moved to top-level src/lib/map module
|
|
15
|
+
// export * from './Map/index.js';
|
|
15
16
|
// FeatureRegistry - Component access management
|
|
16
17
|
export * from './FeatureRegistry/index.js';
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
// This approach keeps the main index clean and allows for easy expansion
|
|
3
3
|
// Core components (Desktop orchestration + Charts + TreeView)
|
|
4
4
|
export * from './core/index.js';
|
|
5
|
+
// Map components (Mapbox cellular visualization)
|
|
6
|
+
export * from './map/index.js';
|
|
5
7
|
// Complete applications
|
|
6
8
|
export * from './apps/index.js';
|
|
@@ -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 && $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[];
|