@smartnet360/svelte-components 0.0.130 → 0.0.132
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/core/CellTable/CellHistoryDemo.svelte +106 -65
- package/dist/core/CellTable/column-config.d.ts +12 -0
- package/dist/core/CellTable/column-config.js +79 -2
- package/dist/core/CellTable/history-api-helper.d.ts +79 -0
- package/dist/core/CellTable/history-api-helper.js +83 -0
- package/dist/core/CellTable/index.d.ts +3 -2
- package/dist/core/CellTable/index.js +3 -1
- package/dist/core/CellTable/types.d.ts +26 -0
- package/dist/map-v3/demo/DemoMap.svelte +1 -6
- package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte +11 -4
- package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +692 -0
- package/dist/map-v3/features/{cells/custom → custom}/index.d.ts +1 -1
- package/dist/map-v3/features/{cells/custom → custom}/index.js +1 -1
- package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +399 -0
- package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.d.ts +11 -7
- package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.js +64 -20
- package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.d.ts +1 -1
- package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.d.ts +4 -3
- package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.js +30 -10
- package/dist/map-v3/features/{cells/custom → custom}/types.d.ts +32 -12
- package/dist/map-v3/features/{cells/custom → custom}/types.js +5 -3
- package/dist/map-v3/index.d.ts +1 -1
- package/dist/map-v3/index.js +1 -1
- package/dist/map-v3/shared/controls/MapControl.svelte +43 -15
- package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +3 -1
- package/package.json +1 -1
- package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +0 -306
- package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +0 -262
- /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellSetManager.svelte.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/components/index.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/components/index.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/layers/CustomCellsLayer.svelte.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/layers/index.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/layers/index.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/logic/index.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/logic/index.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/stores/index.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/stores/index.js +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Custom Layer
|
|
4
|
+
*
|
|
5
|
+
* Renders custom sets on the map with sizeFactor support.
|
|
6
|
+
* Supports two geometry types:
|
|
7
|
+
* - 'cell': Sector arcs using resolved cell's azimuth/beamwidth
|
|
8
|
+
* - 'point': Native Mapbox circles (lightweight)
|
|
9
|
+
*
|
|
10
|
+
* Each set is rendered as separate layers for independent styling.
|
|
11
|
+
*/
|
|
12
|
+
import { getContext, onMount, onDestroy } from 'svelte';
|
|
13
|
+
import type { MapStore } from '../../../core/stores/map.store.svelte';
|
|
14
|
+
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
15
|
+
import type { CustomCellSet, CustomCell } from '../types';
|
|
16
|
+
import { generateCellArc, calculateRadiusInMeters } from '../../cells/logic/geometry';
|
|
17
|
+
import type mapboxgl from 'mapbox-gl';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** The custom cell sets store */
|
|
21
|
+
setsStore: CustomCellSetsStore;
|
|
22
|
+
/** Optional: specific set ID to render (if not provided, renders all) */
|
|
23
|
+
setId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { setsStore, setId }: Props = $props();
|
|
27
|
+
|
|
28
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
29
|
+
|
|
30
|
+
// Track active layer/source IDs for cleanup
|
|
31
|
+
let activeSources = new Set<string>();
|
|
32
|
+
let activeLayers = new Set<string>();
|
|
33
|
+
|
|
34
|
+
// Debounce timer
|
|
35
|
+
let updateTimeout: ReturnType<typeof setTimeout>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get source IDs for a set (one for cells, one for points)
|
|
39
|
+
*/
|
|
40
|
+
function getSourceIds(setId: string): { cells: string; points: string } {
|
|
41
|
+
return {
|
|
42
|
+
cells: `custom-cells-${setId}`,
|
|
43
|
+
points: `custom-points-${setId}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get layer IDs for a set
|
|
49
|
+
*/
|
|
50
|
+
function getLayerIds(setId: string): {
|
|
51
|
+
cellFill: string;
|
|
52
|
+
cellLine: string;
|
|
53
|
+
pointCircle: string;
|
|
54
|
+
} {
|
|
55
|
+
return {
|
|
56
|
+
cellFill: `custom-cells-fill-${setId}`,
|
|
57
|
+
cellLine: `custom-cells-line-${setId}`,
|
|
58
|
+
pointCircle: `custom-points-circle-${setId}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add source and layers for a set
|
|
64
|
+
*/
|
|
65
|
+
function addSetLayers(map: mapboxgl.Map, set: CustomCellSet) {
|
|
66
|
+
const sources = getSourceIds(set.id);
|
|
67
|
+
const layers = getLayerIds(set.id);
|
|
68
|
+
|
|
69
|
+
// Add cells source if not exists
|
|
70
|
+
if (!map.getSource(sources.cells)) {
|
|
71
|
+
map.addSource(sources.cells, {
|
|
72
|
+
type: 'geojson',
|
|
73
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
74
|
+
});
|
|
75
|
+
activeSources.add(sources.cells);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add points source if not exists
|
|
79
|
+
if (!map.getSource(sources.points)) {
|
|
80
|
+
map.addSource(sources.points, {
|
|
81
|
+
type: 'geojson',
|
|
82
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
83
|
+
});
|
|
84
|
+
activeSources.add(sources.points);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add cell fill layer
|
|
88
|
+
if (!map.getLayer(layers.cellFill)) {
|
|
89
|
+
map.addLayer({
|
|
90
|
+
id: layers.cellFill,
|
|
91
|
+
type: 'fill',
|
|
92
|
+
source: sources.cells,
|
|
93
|
+
paint: {
|
|
94
|
+
'fill-color': ['get', 'color'],
|
|
95
|
+
'fill-opacity': set.opacity
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
activeLayers.add(layers.cellFill);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add cell line layer
|
|
102
|
+
if (!map.getLayer(layers.cellLine)) {
|
|
103
|
+
map.addLayer({
|
|
104
|
+
id: layers.cellLine,
|
|
105
|
+
type: 'line',
|
|
106
|
+
source: sources.cells,
|
|
107
|
+
paint: {
|
|
108
|
+
'line-color': ['get', 'lineColor'],
|
|
109
|
+
'line-width': ['get', 'lineWidth'],
|
|
110
|
+
'line-opacity': ['get', 'lineOpacity']
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
activeLayers.add(layers.cellLine);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add points circle layer (native Mapbox - very lightweight)
|
|
117
|
+
if (!map.getLayer(layers.pointCircle)) {
|
|
118
|
+
map.addLayer({
|
|
119
|
+
id: layers.pointCircle,
|
|
120
|
+
type: 'circle',
|
|
121
|
+
source: sources.points,
|
|
122
|
+
paint: {
|
|
123
|
+
'circle-radius': ['get', 'radius'],
|
|
124
|
+
'circle-color': ['get', 'color'],
|
|
125
|
+
'circle-opacity': set.opacity,
|
|
126
|
+
'circle-stroke-width': 1,
|
|
127
|
+
'circle-stroke-color': ['get', 'strokeColor']
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
activeLayers.add(layers.pointCircle);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Remove layers and source for a set
|
|
136
|
+
*/
|
|
137
|
+
function removeSetLayers(map: mapboxgl.Map, setIdToRemove: string) {
|
|
138
|
+
const sources = getSourceIds(setIdToRemove);
|
|
139
|
+
const layers = getLayerIds(setIdToRemove);
|
|
140
|
+
|
|
141
|
+
// Remove layers
|
|
142
|
+
for (const layerId of [layers.cellLine, layers.cellFill, layers.pointCircle]) {
|
|
143
|
+
if (map.getLayer(layerId)) {
|
|
144
|
+
map.removeLayer(layerId);
|
|
145
|
+
activeLayers.delete(layerId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Remove sources
|
|
150
|
+
for (const sourceId of [sources.cells, sources.points]) {
|
|
151
|
+
if (map.getSource(sourceId)) {
|
|
152
|
+
map.removeSource(sourceId);
|
|
153
|
+
activeSources.delete(sourceId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render items for a specific set
|
|
160
|
+
*/
|
|
161
|
+
function renderSet(map: mapboxgl.Map, set: CustomCellSet) {
|
|
162
|
+
const bounds = map.getBounds();
|
|
163
|
+
if (!bounds) return;
|
|
164
|
+
|
|
165
|
+
const zoom = map.getZoom();
|
|
166
|
+
const centerLat = map.getCenter().lat;
|
|
167
|
+
|
|
168
|
+
// Calculate base radius from pixel size (for cell arcs)
|
|
169
|
+
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, set.baseSize);
|
|
170
|
+
|
|
171
|
+
const cellFeatures: GeoJSON.Feature[] = [];
|
|
172
|
+
const pointFeatures: GeoJSON.Feature[] = [];
|
|
173
|
+
|
|
174
|
+
for (const item of set.cells) {
|
|
175
|
+
// Check group visibility
|
|
176
|
+
if (!set.visibleGroups.has(item.customGroup)) continue;
|
|
177
|
+
|
|
178
|
+
// Get color for this group
|
|
179
|
+
const color = set.groupColors[item.customGroup] || set.defaultColor;
|
|
180
|
+
|
|
181
|
+
if (item.geometry === 'point' && item.lat !== undefined && item.lon !== undefined) {
|
|
182
|
+
// Point geometry - render as circle
|
|
183
|
+
// Viewport filter
|
|
184
|
+
if (!bounds.contains([item.lon, item.lat])) continue;
|
|
185
|
+
|
|
186
|
+
// Point radius in pixels (pointSize is in pixels)
|
|
187
|
+
const radius = set.pointSize * item.sizeFactor;
|
|
188
|
+
|
|
189
|
+
// Darken color for stroke
|
|
190
|
+
const strokeColor = darkenColor(color, 0.3);
|
|
191
|
+
|
|
192
|
+
pointFeatures.push({
|
|
193
|
+
type: 'Feature',
|
|
194
|
+
geometry: {
|
|
195
|
+
type: 'Point',
|
|
196
|
+
coordinates: [item.lon, item.lat]
|
|
197
|
+
},
|
|
198
|
+
properties: {
|
|
199
|
+
id: item.id,
|
|
200
|
+
color,
|
|
201
|
+
strokeColor,
|
|
202
|
+
radius,
|
|
203
|
+
customGroup: item.customGroup,
|
|
204
|
+
sizeFactor: item.sizeFactor,
|
|
205
|
+
setName: set.name,
|
|
206
|
+
...Object.fromEntries(
|
|
207
|
+
Object.entries(item.extraFields).map(([k, v]) => [`extra_${k}`, v])
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} else if (item.geometry === 'cell' && item.resolvedCell) {
|
|
212
|
+
// Cell geometry - render as sector arc
|
|
213
|
+
const cell = item.resolvedCell;
|
|
214
|
+
|
|
215
|
+
// Viewport filter
|
|
216
|
+
if (!bounds.contains([cell.longitude, cell.latitude])) continue;
|
|
217
|
+
|
|
218
|
+
// Apply size factor
|
|
219
|
+
const radiusMeters = baseRadiusMeters * item.sizeFactor;
|
|
220
|
+
|
|
221
|
+
// Generate arc feature
|
|
222
|
+
const feature = generateCellArc(cell, radiusMeters, 50, color);
|
|
223
|
+
|
|
224
|
+
// Add custom properties for tooltips
|
|
225
|
+
if (feature.properties) {
|
|
226
|
+
feature.properties.customGroup = item.customGroup;
|
|
227
|
+
feature.properties.sizeFactor = item.sizeFactor;
|
|
228
|
+
feature.properties.setName = set.name;
|
|
229
|
+
|
|
230
|
+
// Add extra fields
|
|
231
|
+
for (const [key, value] of Object.entries(item.extraFields)) {
|
|
232
|
+
feature.properties[`extra_${key}`] = value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
cellFeatures.push(feature);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update sources
|
|
241
|
+
const sources = getSourceIds(set.id);
|
|
242
|
+
const layers = getLayerIds(set.id);
|
|
243
|
+
|
|
244
|
+
const cellsSource = map.getSource(sources.cells) as mapboxgl.GeoJSONSource;
|
|
245
|
+
if (cellsSource) {
|
|
246
|
+
cellsSource.setData({
|
|
247
|
+
type: 'FeatureCollection',
|
|
248
|
+
features: cellFeatures as any
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const pointsSource = map.getSource(sources.points) as mapboxgl.GeoJSONSource;
|
|
253
|
+
if (pointsSource) {
|
|
254
|
+
pointsSource.setData({
|
|
255
|
+
type: 'FeatureCollection',
|
|
256
|
+
features: pointFeatures
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Update layer opacity
|
|
261
|
+
if (map.getLayer(layers.cellFill)) {
|
|
262
|
+
map.setPaintProperty(layers.cellFill, 'fill-opacity', set.opacity);
|
|
263
|
+
}
|
|
264
|
+
if (map.getLayer(layers.pointCircle)) {
|
|
265
|
+
map.setPaintProperty(layers.pointCircle, 'circle-opacity', set.opacity);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(`[CustomLayer] Rendered ${cellFeatures.length} cells, ${pointFeatures.length} points for set "${set.name}"`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Darken a hex color by a factor
|
|
273
|
+
*/
|
|
274
|
+
function darkenColor(hex: string, factor: number): string {
|
|
275
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
276
|
+
const r = Math.floor((num >> 16) * (1 - factor));
|
|
277
|
+
const g = Math.floor(((num >> 8) & 0x00FF) * (1 - factor));
|
|
278
|
+
const b = Math.floor((num & 0x0000FF) * (1 - factor));
|
|
279
|
+
return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Ensure all sources and layers exist for current sets (synchronous)
|
|
284
|
+
*/
|
|
285
|
+
function ensureLayers() {
|
|
286
|
+
const map = mapStore.map;
|
|
287
|
+
if (!map) return;
|
|
288
|
+
|
|
289
|
+
const setsToRender = setId
|
|
290
|
+
? setsStore.sets.filter(s => s.id === setId)
|
|
291
|
+
: setsStore.sets;
|
|
292
|
+
|
|
293
|
+
// Track which sets we're rendering
|
|
294
|
+
const activeSetIds = new Set(setsToRender.map(s => s.id));
|
|
295
|
+
|
|
296
|
+
// Remove layers for sets that no longer exist
|
|
297
|
+
for (const sourceId of [...activeSources]) {
|
|
298
|
+
const match = sourceId.match(/^custom-(?:cells|points)-(.+)$/);
|
|
299
|
+
if (match) {
|
|
300
|
+
const setIdFromSource = match[1];
|
|
301
|
+
if (!activeSetIds.has(setIdFromSource)) {
|
|
302
|
+
removeSetLayers(map, setIdFromSource);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Add layers for each visible set
|
|
308
|
+
for (const set of setsToRender) {
|
|
309
|
+
if (set.visible) {
|
|
310
|
+
addSetLayers(map, set);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Update layer data (debounced)
|
|
317
|
+
*/
|
|
318
|
+
function updateLayerData() {
|
|
319
|
+
const map = mapStore.map;
|
|
320
|
+
if (!map) return;
|
|
321
|
+
|
|
322
|
+
clearTimeout(updateTimeout);
|
|
323
|
+
updateTimeout = setTimeout(() => {
|
|
324
|
+
const setsToRender = setId
|
|
325
|
+
? setsStore.sets.filter(s => s.id === setId)
|
|
326
|
+
: setsStore.sets;
|
|
327
|
+
|
|
328
|
+
// Render each set
|
|
329
|
+
for (const set of setsToRender) {
|
|
330
|
+
if (set.visible) {
|
|
331
|
+
renderSet(map, set);
|
|
332
|
+
} else {
|
|
333
|
+
// Hide by clearing data
|
|
334
|
+
const sources = getSourceIds(set.id);
|
|
335
|
+
const cellsSource = map.getSource(sources.cells) as mapboxgl.GeoJSONSource;
|
|
336
|
+
const pointsSource = map.getSource(sources.points) as mapboxgl.GeoJSONSource;
|
|
337
|
+
if (cellsSource) {
|
|
338
|
+
cellsSource.setData({ type: 'FeatureCollection', features: [] });
|
|
339
|
+
}
|
|
340
|
+
if (pointsSource) {
|
|
341
|
+
pointsSource.setData({ type: 'FeatureCollection', features: [] });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}, 100);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Setup and reactive updates
|
|
349
|
+
$effect(() => {
|
|
350
|
+
const map = mapStore.map;
|
|
351
|
+
if (!map) return;
|
|
352
|
+
|
|
353
|
+
// When style changes, all sources/layers are removed by Mapbox
|
|
354
|
+
// We need to clear our tracking and re-add layers
|
|
355
|
+
const onStyleLoad = () => {
|
|
356
|
+
activeSources.clear();
|
|
357
|
+
activeLayers.clear();
|
|
358
|
+
ensureLayers();
|
|
359
|
+
updateLayerData();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Initial setup
|
|
363
|
+
ensureLayers();
|
|
364
|
+
updateLayerData();
|
|
365
|
+
|
|
366
|
+
// Events
|
|
367
|
+
map.on('style.load', onStyleLoad);
|
|
368
|
+
map.on('moveend', updateLayerData);
|
|
369
|
+
map.on('zoomend', updateLayerData);
|
|
370
|
+
|
|
371
|
+
return () => {
|
|
372
|
+
map.off('style.load', onStyleLoad);
|
|
373
|
+
map.off('moveend', updateLayerData);
|
|
374
|
+
map.off('zoomend', updateLayerData);
|
|
375
|
+
|
|
376
|
+
// Cleanup all layers - extract unique set IDs
|
|
377
|
+
const setIds = new Set<string>();
|
|
378
|
+
for (const sourceId of activeSources) {
|
|
379
|
+
const match = sourceId.match(/^custom-(?:cells|points)-(.+)$/);
|
|
380
|
+
if (match) {
|
|
381
|
+
setIds.add(match[1]);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const setIdToRemove of setIds) {
|
|
385
|
+
removeSetLayers(map, setIdToRemove);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// React to store changes
|
|
391
|
+
$effect(() => {
|
|
392
|
+
// Read version to trigger on changes
|
|
393
|
+
const _version = setsStore.version;
|
|
394
|
+
const _sets = setsStore.sets;
|
|
395
|
+
|
|
396
|
+
ensureLayers();
|
|
397
|
+
updateLayerData();
|
|
398
|
+
});
|
|
399
|
+
</script>
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom
|
|
2
|
+
* Custom Feature - CSV Parser
|
|
3
3
|
*
|
|
4
|
-
* Parses CSV files for custom
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Parses CSV files for custom sets.
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* - Cell mode: id column resolves against cell data → sector arcs
|
|
7
|
+
* - Point mode: lat/lon columns → circles
|
|
8
|
+
*
|
|
9
|
+
* Required column: id (or cellName/txId for backwards compat)
|
|
10
|
+
* Optional columns: customGroup, sizeFactor, lat, lon, + any extras for tooltips
|
|
7
11
|
* Supports both comma (,) and semicolon (;) delimiters
|
|
8
12
|
*/
|
|
9
13
|
import type { CustomCellImportResult } from '../types';
|
|
10
|
-
import type { Cell } from '
|
|
14
|
+
import type { Cell } from '../../../../shared/demo';
|
|
11
15
|
/**
|
|
12
16
|
* Supported delimiters
|
|
13
17
|
*/
|
|
14
18
|
export type CsvDelimiter = ',' | ';' | 'auto';
|
|
15
19
|
/**
|
|
16
|
-
* Parse a CSV string into custom cells
|
|
20
|
+
* Parse a CSV string into custom items (cells or points)
|
|
17
21
|
* @param csvContent Raw CSV content
|
|
18
22
|
* @param cellLookup Map of cellName -> Cell for resolving cell data
|
|
19
23
|
* @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
|
|
20
|
-
* @returns Import result with
|
|
24
|
+
* @returns Import result with items, unmatched IDs, groups, and extra columns
|
|
21
25
|
*/
|
|
22
26
|
export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, delimiter?: CsvDelimiter): CustomCellImportResult;
|
|
23
27
|
/**
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom
|
|
2
|
+
* Custom Feature - CSV Parser
|
|
3
3
|
*
|
|
4
|
-
* Parses CSV files for custom
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Parses CSV files for custom sets.
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* - Cell mode: id column resolves against cell data → sector arcs
|
|
7
|
+
* - Point mode: lat/lon columns → circles
|
|
8
|
+
*
|
|
9
|
+
* Required column: id (or cellName/txId for backwards compat)
|
|
10
|
+
* Optional columns: customGroup, sizeFactor, lat, lon, + any extras for tooltips
|
|
7
11
|
* Supports both comma (,) and semicolon (;) delimiters
|
|
8
12
|
*/
|
|
9
13
|
/**
|
|
10
14
|
* Known/reserved column names
|
|
11
15
|
*/
|
|
12
|
-
const RESERVED_COLUMNS = ['cellname', 'txid', 'customgroup', 'sizefactor'];
|
|
16
|
+
const RESERVED_COLUMNS = ['id', 'cellname', 'txid', 'customgroup', 'sizefactor', 'lat', 'latitude', 'lon', 'lng', 'longitude'];
|
|
13
17
|
/**
|
|
14
18
|
* Normalize column name for matching
|
|
15
19
|
*/
|
|
@@ -26,11 +30,11 @@ function detectDelimiter(headerLine) {
|
|
|
26
30
|
return semicolonCount > commaCount ? ';' : ',';
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
|
-
* Parse a CSV string into custom cells
|
|
33
|
+
* Parse a CSV string into custom items (cells or points)
|
|
30
34
|
* @param csvContent Raw CSV content
|
|
31
35
|
* @param cellLookup Map of cellName -> Cell for resolving cell data
|
|
32
36
|
* @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
|
|
33
|
-
* @returns Import result with
|
|
37
|
+
* @returns Import result with items, unmatched IDs, groups, and extra columns
|
|
34
38
|
*/
|
|
35
39
|
export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto') {
|
|
36
40
|
const lines = csvContent.trim().split('\n');
|
|
@@ -40,7 +44,9 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto')
|
|
|
40
44
|
unmatchedTxIds: [],
|
|
41
45
|
groups: [],
|
|
42
46
|
extraColumns: [],
|
|
43
|
-
totalRows: 0
|
|
47
|
+
totalRows: 0,
|
|
48
|
+
cellCount: 0,
|
|
49
|
+
pointCount: 0
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
// Detect or use specified delimiter
|
|
@@ -49,19 +55,24 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto')
|
|
|
49
55
|
// Parse header
|
|
50
56
|
const headers = parseCSVLine(headerLine, actualDelimiter);
|
|
51
57
|
const normalizedHeaders = headers.map(normalizeColumnName);
|
|
52
|
-
// Find
|
|
53
|
-
let idIndex = normalizedHeaders.findIndex(h => h === '
|
|
54
|
-
|
|
58
|
+
// Find ID column - prefer 'id', then 'cellname', then 'txid'
|
|
59
|
+
let idIndex = normalizedHeaders.findIndex(h => h === 'id');
|
|
60
|
+
if (idIndex === -1) {
|
|
61
|
+
idIndex = normalizedHeaders.findIndex(h => h === 'cellname');
|
|
62
|
+
}
|
|
55
63
|
if (idIndex === -1) {
|
|
56
64
|
idIndex = normalizedHeaders.findIndex(h => h === 'txid');
|
|
57
|
-
usesCellName = false;
|
|
58
65
|
}
|
|
59
66
|
if (idIndex === -1) {
|
|
60
|
-
throw new Error('CSV must contain
|
|
67
|
+
throw new Error('CSV must contain an "id", "cellName", or "txId" column');
|
|
61
68
|
}
|
|
62
69
|
// Find optional columns
|
|
63
70
|
const groupIndex = normalizedHeaders.findIndex(h => h === 'customgroup');
|
|
64
71
|
const sizeFactorIndex = normalizedHeaders.findIndex(h => h === 'sizefactor');
|
|
72
|
+
// Find lat/lon columns for point geometry
|
|
73
|
+
let latIndex = normalizedHeaders.findIndex(h => h === 'lat' || h === 'latitude');
|
|
74
|
+
let lonIndex = normalizedHeaders.findIndex(h => h === 'lon' || h === 'lng' || h === 'longitude');
|
|
75
|
+
const hasLatLon = latIndex !== -1 && lonIndex !== -1;
|
|
65
76
|
// Find extra columns (not reserved)
|
|
66
77
|
const extraColumns = [];
|
|
67
78
|
const extraIndices = [];
|
|
@@ -76,13 +87,15 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto')
|
|
|
76
87
|
const cells = [];
|
|
77
88
|
const unmatchedTxIds = [];
|
|
78
89
|
const groupsSet = new Set();
|
|
90
|
+
let cellCount = 0;
|
|
91
|
+
let pointCount = 0;
|
|
79
92
|
for (let i = 1; i < lines.length; i++) {
|
|
80
93
|
const line = lines[i].trim();
|
|
81
94
|
if (!line)
|
|
82
95
|
continue;
|
|
83
96
|
const values = parseCSVLine(line, actualDelimiter);
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
97
|
+
const itemId = values[idIndex]?.trim();
|
|
98
|
+
if (!itemId)
|
|
86
99
|
continue;
|
|
87
100
|
// Get custom group (default to 'default')
|
|
88
101
|
const customGroup = groupIndex !== -1
|
|
@@ -105,19 +118,48 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto')
|
|
|
105
118
|
const numValue = parseFloat(value);
|
|
106
119
|
extraFields[extraColumns[i]] = isNaN(numValue) ? value : numValue;
|
|
107
120
|
});
|
|
108
|
-
//
|
|
109
|
-
|
|
121
|
+
// Determine geometry type and create item
|
|
122
|
+
let geometry = 'cell';
|
|
123
|
+
let resolvedCell;
|
|
124
|
+
let lat;
|
|
125
|
+
let lon;
|
|
126
|
+
if (hasLatLon) {
|
|
127
|
+
// Try to parse lat/lon for point geometry
|
|
128
|
+
const parsedLat = parseFloat(values[latIndex]);
|
|
129
|
+
const parsedLon = parseFloat(values[lonIndex]);
|
|
130
|
+
if (!isNaN(parsedLat) && !isNaN(parsedLon)) {
|
|
131
|
+
// Valid coordinates - use point geometry
|
|
132
|
+
geometry = 'point';
|
|
133
|
+
lat = parsedLat;
|
|
134
|
+
lon = parsedLon;
|
|
135
|
+
pointCount++;
|
|
136
|
+
cells.push({
|
|
137
|
+
id: itemId,
|
|
138
|
+
customGroup,
|
|
139
|
+
sizeFactor,
|
|
140
|
+
extraFields,
|
|
141
|
+
geometry,
|
|
142
|
+
lat,
|
|
143
|
+
lon
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// No valid lat/lon - try to resolve as cell
|
|
149
|
+
resolvedCell = cellLookup.get(itemId);
|
|
110
150
|
if (resolvedCell) {
|
|
151
|
+
cellCount++;
|
|
111
152
|
cells.push({
|
|
112
|
-
|
|
153
|
+
id: itemId,
|
|
113
154
|
customGroup,
|
|
114
155
|
sizeFactor,
|
|
115
156
|
extraFields,
|
|
157
|
+
geometry: 'cell',
|
|
116
158
|
resolvedCell
|
|
117
159
|
});
|
|
118
160
|
}
|
|
119
161
|
else {
|
|
120
|
-
unmatchedTxIds.push(
|
|
162
|
+
unmatchedTxIds.push(itemId);
|
|
121
163
|
}
|
|
122
164
|
}
|
|
123
165
|
return {
|
|
@@ -125,7 +167,9 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto')
|
|
|
125
167
|
unmatchedTxIds,
|
|
126
168
|
groups: Array.from(groupsSet).sort(),
|
|
127
169
|
extraColumns,
|
|
128
|
-
totalRows: lines.length - 1
|
|
170
|
+
totalRows: lines.length - 1,
|
|
171
|
+
cellCount,
|
|
172
|
+
pointCount
|
|
129
173
|
};
|
|
130
174
|
}
|
|
131
175
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Builds tree structure for TreeView component, grouped by customGroup
|
|
5
5
|
*/
|
|
6
|
-
import type { TreeNode } from '
|
|
6
|
+
import type { TreeNode } from '../../../../core/TreeView/tree.model';
|
|
7
7
|
import type { CustomCellSet } from '../types';
|
|
8
8
|
/**
|
|
9
9
|
* Metadata for tree nodes
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Manages multiple custom cell sets, each loaded from a CSV file.
|
|
5
5
|
* Resolves cell data from a provided cell array.
|
|
6
6
|
*/
|
|
7
|
-
import type { Cell } from '
|
|
8
|
-
import type { CellDataStore } from '../../stores/cell.data.svelte';
|
|
7
|
+
import type { Cell } from '../../../../shared/demo';
|
|
8
|
+
import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
|
|
9
9
|
import type { CustomCellSet, CustomCellImportResult } from '../types';
|
|
10
10
|
/** Function that returns the current cells array */
|
|
11
11
|
type CellsGetter = () => Cell[];
|
|
@@ -58,7 +58,7 @@ export declare class CustomCellSetsStore {
|
|
|
58
58
|
/**
|
|
59
59
|
* Update set display settings
|
|
60
60
|
*/
|
|
61
|
-
updateSetSettings(setId: string, settings: Partial<Pick<CustomCellSet, 'baseSize' | 'opacity' | 'defaultColor'>>): void;
|
|
61
|
+
updateSetSettings(setId: string, settings: Partial<Pick<CustomCellSet, 'baseSize' | 'pointSize' | 'opacity' | 'defaultColor'>>): void;
|
|
62
62
|
/**
|
|
63
63
|
* Rename a set
|
|
64
64
|
*/
|
|
@@ -69,6 +69,7 @@ export declare class CustomCellSetsStore {
|
|
|
69
69
|
getVisibleCells(setId: string): import("..").CustomCell[];
|
|
70
70
|
/**
|
|
71
71
|
* Re-resolve cells after main cell data changes
|
|
72
|
+
* Only affects items with 'cell' geometry
|
|
72
73
|
*/
|
|
73
74
|
refreshResolutions(): void;
|
|
74
75
|
/**
|