@smartnet360/svelte-components 0.0.126 → 0.0.128
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/demo/DemoMap.svelte +18 -0
- package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +16 -5
- package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +26 -13
- package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte +203 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte.d.ts +15 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte +261 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte.d.ts +10 -0
- package/dist/map-v3/features/sites/custom/index.d.ts +13 -0
- package/dist/map-v3/features/sites/custom/index.js +16 -0
- package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte +201 -0
- package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte.d.ts +8 -0
- package/dist/map-v3/features/sites/custom/logic/csv-parser.d.ts +12 -0
- package/dist/map-v3/features/sites/custom/logic/csv-parser.js +182 -0
- package/dist/map-v3/features/sites/custom/logic/tree-adapter.d.ts +16 -0
- package/dist/map-v3/features/sites/custom/logic/tree-adapter.js +59 -0
- package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.d.ts +78 -0
- package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.js +248 -0
- package/dist/map-v3/features/sites/custom/types.d.ts +74 -0
- package/dist/map-v3/features/sites/custom/types.js +8 -0
- package/dist/map-v3/index.d.ts +2 -0
- package/dist/map-v3/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Sites Feature
|
|
3
|
+
*
|
|
4
|
+
* Allows users to upload CSV files with custom site locations
|
|
5
|
+
* and display them as styled circle markers on the map.
|
|
6
|
+
*/
|
|
7
|
+
// Store
|
|
8
|
+
export { CustomSiteSetsStore } from './stores/custom-site-sets.svelte';
|
|
9
|
+
// Logic
|
|
10
|
+
export { parseCustomSitesCsv } from './logic/csv-parser';
|
|
11
|
+
export { buildCustomSiteTree, getGroupCounts } from './logic/tree-adapter';
|
|
12
|
+
// Components
|
|
13
|
+
export { default as CustomSiteSetManager } from './components/CustomSiteSetManager.svelte';
|
|
14
|
+
export { default as CustomSiteFilterControl } from './components/CustomSiteFilterControl.svelte';
|
|
15
|
+
// Layers
|
|
16
|
+
export { default as CustomSitesLayer } from './layers/CustomSitesLayer.svelte';
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Custom Sites Layer
|
|
4
|
+
*
|
|
5
|
+
* Renders custom site points on the map as circle markers.
|
|
6
|
+
* Supports per-site sizing via sizeFactor and per-group coloring.
|
|
7
|
+
*/
|
|
8
|
+
import { getContext, onDestroy } from 'svelte';
|
|
9
|
+
import mapboxgl, { type Map as MapboxMap } from 'mapbox-gl';
|
|
10
|
+
import type { MapStore } from '../../../../core/stores/map.store.svelte';
|
|
11
|
+
import type { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
|
|
12
|
+
import type { CustomSiteSet } from '../types';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** The custom site sets store */
|
|
16
|
+
setsStore: CustomSiteSetsStore;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { setsStore }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
22
|
+
const SOURCE_PREFIX = 'custom-sites-source-';
|
|
23
|
+
const LAYER_PREFIX = 'custom-sites-layer-';
|
|
24
|
+
|
|
25
|
+
// Track which layers we've added
|
|
26
|
+
let addedLayers = new Set<string>();
|
|
27
|
+
|
|
28
|
+
// Update layers when data changes
|
|
29
|
+
$effect(() => {
|
|
30
|
+
const map = mapStore?.map;
|
|
31
|
+
const _version = setsStore.version;
|
|
32
|
+
const sets = setsStore.sets;
|
|
33
|
+
|
|
34
|
+
if (!map) return;
|
|
35
|
+
|
|
36
|
+
// Remove layers for deleted sets
|
|
37
|
+
const currentSetIds = new Set(sets.map(s => s.id));
|
|
38
|
+
for (const layerId of addedLayers) {
|
|
39
|
+
const setId = layerId.replace(LAYER_PREFIX, '');
|
|
40
|
+
if (!currentSetIds.has(setId)) {
|
|
41
|
+
removeLayer(map, setId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Update or create layers for each set
|
|
46
|
+
for (const set of sets) {
|
|
47
|
+
updateSetLayer(map, set);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
onDestroy(() => {
|
|
52
|
+
const map = mapStore?.map;
|
|
53
|
+
if (map) {
|
|
54
|
+
for (const layerId of addedLayers) {
|
|
55
|
+
const setId = layerId.replace(LAYER_PREFIX, '');
|
|
56
|
+
removeLayer(map, setId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function updateSetLayer(map: MapboxMap, set: CustomSiteSet) {
|
|
62
|
+
const sourceId = SOURCE_PREFIX + set.id;
|
|
63
|
+
const layerId = LAYER_PREFIX + set.id;
|
|
64
|
+
|
|
65
|
+
// Build GeoJSON features for visible sites
|
|
66
|
+
const features = buildFeatures(set);
|
|
67
|
+
|
|
68
|
+
// Check if source exists
|
|
69
|
+
const existingSource = map.getSource(sourceId);
|
|
70
|
+
|
|
71
|
+
if (existingSource) {
|
|
72
|
+
// Update existing source data
|
|
73
|
+
(existingSource as mapboxgl.GeoJSONSource).setData({
|
|
74
|
+
type: 'FeatureCollection',
|
|
75
|
+
features
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Update layer paint properties
|
|
79
|
+
if (map.getLayer(layerId)) {
|
|
80
|
+
map.setPaintProperty(layerId, 'circle-opacity', set.visible ? set.opacity : 0);
|
|
81
|
+
map.setPaintProperty(layerId, 'circle-stroke-opacity', set.visible ? set.opacity : 0);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
// Create new source and layer
|
|
85
|
+
map.addSource(sourceId, {
|
|
86
|
+
type: 'geojson',
|
|
87
|
+
data: {
|
|
88
|
+
type: 'FeatureCollection',
|
|
89
|
+
features
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
map.addLayer({
|
|
94
|
+
id: layerId,
|
|
95
|
+
type: 'circle',
|
|
96
|
+
source: sourceId,
|
|
97
|
+
paint: {
|
|
98
|
+
'circle-radius': ['get', 'radius'],
|
|
99
|
+
'circle-color': ['get', 'color'],
|
|
100
|
+
'circle-opacity': set.visible ? set.opacity : 0,
|
|
101
|
+
'circle-stroke-width': 1,
|
|
102
|
+
'circle-stroke-color': '#ffffff',
|
|
103
|
+
'circle-stroke-opacity': set.visible ? set.opacity : 0
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
addedLayers.add(layerId);
|
|
108
|
+
|
|
109
|
+
// Add popup on click
|
|
110
|
+
map.on('click', layerId, (e) => {
|
|
111
|
+
if (!e.features || e.features.length === 0) return;
|
|
112
|
+
|
|
113
|
+
const feature = e.features[0];
|
|
114
|
+
const props = feature.properties;
|
|
115
|
+
if (!props) return;
|
|
116
|
+
|
|
117
|
+
// Build popup content
|
|
118
|
+
let html = `<div class="custom-site-popup">`;
|
|
119
|
+
html += `<strong>${props.id}</strong>`;
|
|
120
|
+
html += `<div class="small text-muted">${props.group}</div>`;
|
|
121
|
+
|
|
122
|
+
// Add extra fields
|
|
123
|
+
if (props.extraFields) {
|
|
124
|
+
try {
|
|
125
|
+
const extras = JSON.parse(props.extraFields);
|
|
126
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
127
|
+
html += `<div class="small"><strong>${key}:</strong> ${value}</div>`;
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Ignore parse errors
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
html += `</div>`;
|
|
135
|
+
|
|
136
|
+
new mapboxgl.Popup()
|
|
137
|
+
.setLngLat(e.lngLat)
|
|
138
|
+
.setHTML(html)
|
|
139
|
+
.addTo(map);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Change cursor on hover
|
|
143
|
+
map.on('mouseenter', layerId, () => {
|
|
144
|
+
map.getCanvas().style.cursor = 'pointer';
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
map.on('mouseleave', layerId, () => {
|
|
148
|
+
map.getCanvas().style.cursor = '';
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function removeLayer(map: MapboxMap, setId: string) {
|
|
154
|
+
const sourceId = SOURCE_PREFIX + setId;
|
|
155
|
+
const layerId = LAYER_PREFIX + setId;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
if (map.getLayer(layerId)) {
|
|
159
|
+
map.removeLayer(layerId);
|
|
160
|
+
}
|
|
161
|
+
if (map.getSource(sourceId)) {
|
|
162
|
+
map.removeSource(sourceId);
|
|
163
|
+
}
|
|
164
|
+
addedLayers.delete(layerId);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.warn(`[CustomSitesLayer] Error removing layer ${layerId}:`, e);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildFeatures(set: CustomSiteSet): GeoJSON.Feature[] {
|
|
171
|
+
const features: GeoJSON.Feature[] = [];
|
|
172
|
+
|
|
173
|
+
for (const site of set.sites) {
|
|
174
|
+
// Skip if group is not visible
|
|
175
|
+
if (!set.visibleGroups.has(site.customGroup)) continue;
|
|
176
|
+
|
|
177
|
+
const color = set.groupColors.get(site.customGroup) || '#666666';
|
|
178
|
+
const radius = set.baseSize * site.sizeFactor;
|
|
179
|
+
|
|
180
|
+
features.push({
|
|
181
|
+
type: 'Feature',
|
|
182
|
+
geometry: {
|
|
183
|
+
type: 'Point',
|
|
184
|
+
coordinates: [site.lon, site.lat]
|
|
185
|
+
},
|
|
186
|
+
properties: {
|
|
187
|
+
id: site.id,
|
|
188
|
+
group: site.customGroup,
|
|
189
|
+
radius,
|
|
190
|
+
color,
|
|
191
|
+
sizeFactor: site.sizeFactor,
|
|
192
|
+
extraFields: JSON.stringify(site.extraFields)
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return features;
|
|
198
|
+
}
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<!-- This component only manages map layers, no DOM output -->
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The custom site sets store */
|
|
4
|
+
setsStore: CustomSiteSetsStore;
|
|
5
|
+
}
|
|
6
|
+
declare const CustomSitesLayer: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type CustomSitesLayer = ReturnType<typeof CustomSitesLayer>;
|
|
8
|
+
export default CustomSitesLayer;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Sites CSV Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses CSV files with site coordinates and grouping information.
|
|
5
|
+
* Required columns: id, lat, lon
|
|
6
|
+
* Optional columns: customGroup/subgroup/group, sizeFactor, any extras
|
|
7
|
+
*/
|
|
8
|
+
import type { CustomSiteImportResult } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* Parse a CSV string into CustomSite objects
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseCustomSitesCsv(csvContent: string): CustomSiteImportResult;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Sites CSV Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses CSV files with site coordinates and grouping information.
|
|
5
|
+
* Required columns: id, lat, lon
|
|
6
|
+
* Optional columns: customGroup/subgroup/group, sizeFactor, any extras
|
|
7
|
+
*/
|
|
8
|
+
/** Column name variations we accept */
|
|
9
|
+
const ID_COLUMNS = ['id', 'site_id', 'siteid', 'site'];
|
|
10
|
+
const LAT_COLUMNS = ['lat', 'latitude', 'y'];
|
|
11
|
+
const LON_COLUMNS = ['lon', 'lng', 'longitude', 'long', 'x'];
|
|
12
|
+
const GROUP_COLUMNS = ['customgroup', 'custom_group', 'subgroup', 'sub_group', 'group'];
|
|
13
|
+
const SIZE_COLUMNS = ['sizefactor', 'size_factor', 'size'];
|
|
14
|
+
/**
|
|
15
|
+
* Parse a CSV string into CustomSite objects
|
|
16
|
+
*/
|
|
17
|
+
export function parseCustomSitesCsv(csvContent) {
|
|
18
|
+
const lines = csvContent.trim().split(/\r?\n/);
|
|
19
|
+
if (lines.length < 2) {
|
|
20
|
+
return {
|
|
21
|
+
sites: [],
|
|
22
|
+
groups: [],
|
|
23
|
+
invalidRows: 0,
|
|
24
|
+
errors: ['CSV must have a header row and at least one data row']
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Parse header
|
|
28
|
+
const headerLine = lines[0];
|
|
29
|
+
const headers = parseCSVLine(headerLine).map(h => h.toLowerCase().trim());
|
|
30
|
+
// Find required column indices
|
|
31
|
+
const idIndex = findColumnIndex(headers, ID_COLUMNS);
|
|
32
|
+
const latIndex = findColumnIndex(headers, LAT_COLUMNS);
|
|
33
|
+
const lonIndex = findColumnIndex(headers, LON_COLUMNS);
|
|
34
|
+
// Validate required columns
|
|
35
|
+
const missingColumns = [];
|
|
36
|
+
if (idIndex === -1)
|
|
37
|
+
missingColumns.push('id');
|
|
38
|
+
if (latIndex === -1)
|
|
39
|
+
missingColumns.push('lat/latitude');
|
|
40
|
+
if (lonIndex === -1)
|
|
41
|
+
missingColumns.push('lon/longitude');
|
|
42
|
+
if (missingColumns.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
sites: [],
|
|
45
|
+
groups: [],
|
|
46
|
+
invalidRows: 0,
|
|
47
|
+
errors: [`Missing required columns: ${missingColumns.join(', ')}`]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Find optional column indices
|
|
51
|
+
const groupIndex = findColumnIndex(headers, GROUP_COLUMNS);
|
|
52
|
+
const sizeIndex = findColumnIndex(headers, SIZE_COLUMNS);
|
|
53
|
+
// Identify extra columns (not id, lat, lon, group, size)
|
|
54
|
+
const usedIndices = new Set([idIndex, latIndex, lonIndex]);
|
|
55
|
+
if (groupIndex !== -1)
|
|
56
|
+
usedIndices.add(groupIndex);
|
|
57
|
+
if (sizeIndex !== -1)
|
|
58
|
+
usedIndices.add(sizeIndex);
|
|
59
|
+
const extraColumnIndices = [];
|
|
60
|
+
headers.forEach((header, idx) => {
|
|
61
|
+
if (!usedIndices.has(idx) && header) {
|
|
62
|
+
extraColumnIndices.push({ index: idx, name: header });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Parse data rows
|
|
66
|
+
const sites = [];
|
|
67
|
+
const groupsSet = new Set();
|
|
68
|
+
const errors = [];
|
|
69
|
+
let invalidRows = 0;
|
|
70
|
+
for (let i = 1; i < lines.length; i++) {
|
|
71
|
+
const line = lines[i].trim();
|
|
72
|
+
if (!line)
|
|
73
|
+
continue;
|
|
74
|
+
const values = parseCSVLine(line);
|
|
75
|
+
// Extract required fields
|
|
76
|
+
const id = values[idIndex]?.trim();
|
|
77
|
+
const latStr = values[latIndex]?.trim();
|
|
78
|
+
const lonStr = values[lonIndex]?.trim();
|
|
79
|
+
if (!id) {
|
|
80
|
+
errors.push(`Row ${i + 1}: Missing id`);
|
|
81
|
+
invalidRows++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const lat = parseFloat(latStr);
|
|
85
|
+
const lon = parseFloat(lonStr);
|
|
86
|
+
if (isNaN(lat) || isNaN(lon)) {
|
|
87
|
+
errors.push(`Row ${i + 1}: Invalid coordinates (lat: ${latStr}, lon: ${lonStr})`);
|
|
88
|
+
invalidRows++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Validate coordinate ranges
|
|
92
|
+
if (lat < -90 || lat > 90) {
|
|
93
|
+
errors.push(`Row ${i + 1}: Latitude out of range: ${lat}`);
|
|
94
|
+
invalidRows++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (lon < -180 || lon > 180) {
|
|
98
|
+
errors.push(`Row ${i + 1}: Longitude out of range: ${lon}`);
|
|
99
|
+
invalidRows++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Extract optional fields
|
|
103
|
+
const customGroup = groupIndex !== -1
|
|
104
|
+
? (values[groupIndex]?.trim() || 'Ungrouped')
|
|
105
|
+
: 'Ungrouped';
|
|
106
|
+
const sizeFactorStr = sizeIndex !== -1 ? values[sizeIndex]?.trim() : '';
|
|
107
|
+
const sizeFactor = sizeFactorStr ? parseFloat(sizeFactorStr) : 1.0;
|
|
108
|
+
const finalSizeFactor = isNaN(sizeFactor) || sizeFactor <= 0 ? 1.0 : sizeFactor;
|
|
109
|
+
// Extract extra fields
|
|
110
|
+
const extraFields = {};
|
|
111
|
+
for (const { index, name } of extraColumnIndices) {
|
|
112
|
+
const value = values[index]?.trim();
|
|
113
|
+
if (value) {
|
|
114
|
+
extraFields[name] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
sites.push({
|
|
118
|
+
id,
|
|
119
|
+
lat,
|
|
120
|
+
lon,
|
|
121
|
+
customGroup,
|
|
122
|
+
sizeFactor: finalSizeFactor,
|
|
123
|
+
extraFields
|
|
124
|
+
});
|
|
125
|
+
groupsSet.add(customGroup);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
sites,
|
|
129
|
+
groups: Array.from(groupsSet).sort(),
|
|
130
|
+
invalidRows,
|
|
131
|
+
errors: errors.slice(0, 10) // Limit error messages
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Find the index of a column by checking multiple possible names
|
|
136
|
+
*/
|
|
137
|
+
function findColumnIndex(headers, possibleNames) {
|
|
138
|
+
for (const name of possibleNames) {
|
|
139
|
+
const index = headers.indexOf(name);
|
|
140
|
+
if (index !== -1)
|
|
141
|
+
return index;
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse a single CSV line, handling quoted values
|
|
147
|
+
*/
|
|
148
|
+
function parseCSVLine(line) {
|
|
149
|
+
const result = [];
|
|
150
|
+
let current = '';
|
|
151
|
+
let inQuotes = false;
|
|
152
|
+
for (let i = 0; i < line.length; i++) {
|
|
153
|
+
const char = line[i];
|
|
154
|
+
const nextChar = line[i + 1];
|
|
155
|
+
if (inQuotes) {
|
|
156
|
+
if (char === '"' && nextChar === '"') {
|
|
157
|
+
current += '"';
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
else if (char === '"') {
|
|
161
|
+
inQuotes = false;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
current += char;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
if (char === '"') {
|
|
169
|
+
inQuotes = true;
|
|
170
|
+
}
|
|
171
|
+
else if (char === ',') {
|
|
172
|
+
result.push(current);
|
|
173
|
+
current = '';
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
current += char;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
result.push(current);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Sites Tree Adapter
|
|
3
|
+
*
|
|
4
|
+
* Builds TreeView nodes from a CustomSiteSet for hierarchical display.
|
|
5
|
+
* Structure: Root -> Groups -> Sites
|
|
6
|
+
*/
|
|
7
|
+
import type { TreeNode } from '../../../../../core/TreeView';
|
|
8
|
+
import type { CustomSiteSet } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* Build a tree structure from a CustomSiteSet
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildCustomSiteTree(set: CustomSiteSet): TreeNode[];
|
|
13
|
+
/**
|
|
14
|
+
* Get site counts per group
|
|
15
|
+
*/
|
|
16
|
+
export declare function getGroupCounts(set: CustomSiteSet): Map<string, number>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Sites Tree Adapter
|
|
3
|
+
*
|
|
4
|
+
* Builds TreeView nodes from a CustomSiteSet for hierarchical display.
|
|
5
|
+
* Structure: Root -> Groups -> Sites
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Build a tree structure from a CustomSiteSet
|
|
9
|
+
*/
|
|
10
|
+
export function buildCustomSiteTree(set) {
|
|
11
|
+
const rootId = `root-${set.id}`;
|
|
12
|
+
// Group sites by customGroup
|
|
13
|
+
const groupedSites = new Map();
|
|
14
|
+
for (const site of set.sites) {
|
|
15
|
+
const existing = groupedSites.get(site.customGroup) || [];
|
|
16
|
+
existing.push(site);
|
|
17
|
+
groupedSites.set(site.customGroup, existing);
|
|
18
|
+
}
|
|
19
|
+
// Build group nodes
|
|
20
|
+
const groupNodes = [];
|
|
21
|
+
for (const group of set.groups) {
|
|
22
|
+
const sites = groupedSites.get(group) || [];
|
|
23
|
+
const groupId = `group-${set.id}-${group}`;
|
|
24
|
+
groupNodes.push({
|
|
25
|
+
id: groupId,
|
|
26
|
+
label: `${group} (${sites.length})`,
|
|
27
|
+
metadata: {
|
|
28
|
+
type: 'group',
|
|
29
|
+
groupId: group,
|
|
30
|
+
setId: set.id,
|
|
31
|
+
count: sites.length
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Create root node
|
|
36
|
+
const rootNode = {
|
|
37
|
+
id: rootId,
|
|
38
|
+
label: set.name,
|
|
39
|
+
children: groupNodes,
|
|
40
|
+
metadata: {
|
|
41
|
+
type: 'root',
|
|
42
|
+
setId: set.id,
|
|
43
|
+
totalSites: set.sites.length,
|
|
44
|
+
groupCount: set.groups.length
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return [rootNode];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get site counts per group
|
|
51
|
+
*/
|
|
52
|
+
export function getGroupCounts(set) {
|
|
53
|
+
const counts = new Map();
|
|
54
|
+
for (const site of set.sites) {
|
|
55
|
+
const current = counts.get(site.customGroup) || 0;
|
|
56
|
+
counts.set(site.customGroup, current + 1);
|
|
57
|
+
}
|
|
58
|
+
return counts;
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Site Sets Store
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple custom site sets with CRUD operations,
|
|
5
|
+
* localStorage persistence, and group visibility/color controls.
|
|
6
|
+
*/
|
|
7
|
+
import type { CustomSite, CustomSiteSet, CustomSiteImportResult } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Store for managing custom site sets
|
|
10
|
+
*/
|
|
11
|
+
export declare class CustomSiteSetsStore {
|
|
12
|
+
/** All custom site sets */
|
|
13
|
+
private _sets;
|
|
14
|
+
/** Version counter for reactivity triggers */
|
|
15
|
+
private _version;
|
|
16
|
+
constructor();
|
|
17
|
+
get sets(): CustomSiteSet[];
|
|
18
|
+
get version(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Import sites from CSV content
|
|
21
|
+
*/
|
|
22
|
+
importFromCsv(csvContent: string, fileName: string): CustomSiteImportResult & {
|
|
23
|
+
setId?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Create a new custom site set
|
|
27
|
+
*/
|
|
28
|
+
createSet(name: string, sites: CustomSite[], groups: string[]): string;
|
|
29
|
+
/**
|
|
30
|
+
* Toggle visibility of entire set
|
|
31
|
+
*/
|
|
32
|
+
toggleSetVisibility(setId: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Toggle visibility of a specific group within a set
|
|
35
|
+
*/
|
|
36
|
+
toggleGroupVisibility(setId: string, groupId: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Set color for a group
|
|
39
|
+
*/
|
|
40
|
+
setGroupColor(setId: string, groupId: string, color: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Update base size for a set
|
|
43
|
+
*/
|
|
44
|
+
setBaseSize(setId: string, size: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* Update opacity for a set
|
|
47
|
+
*/
|
|
48
|
+
setOpacity(setId: string, opacity: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Rename a set
|
|
51
|
+
*/
|
|
52
|
+
renameSet(setId: string, newName: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Remove a set
|
|
55
|
+
*/
|
|
56
|
+
removeSet(setId: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Clear all sets
|
|
59
|
+
*/
|
|
60
|
+
clearAll(): void;
|
|
61
|
+
private saveToStorage;
|
|
62
|
+
private loadFromStorage;
|
|
63
|
+
/**
|
|
64
|
+
* Get a specific set by ID
|
|
65
|
+
*/
|
|
66
|
+
getSet(setId: string): CustomSiteSet | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Get visible sites for a set (filtered by visible groups)
|
|
69
|
+
*/
|
|
70
|
+
getVisibleSites(setId: string): CustomSite[];
|
|
71
|
+
/**
|
|
72
|
+
* Get all visible sites across all sets
|
|
73
|
+
*/
|
|
74
|
+
getAllVisibleSites(): Array<{
|
|
75
|
+
site: CustomSite;
|
|
76
|
+
set: CustomSiteSet;
|
|
77
|
+
}>;
|
|
78
|
+
}
|