@smartnet360/svelte-components 0.0.129 → 0.0.131

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.
Files changed (33) hide show
  1. package/dist/map-v3/demo/DemoMap.svelte +1 -6
  2. package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte +11 -4
  3. package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +692 -0
  4. package/dist/map-v3/features/{cells/custom → custom}/index.d.ts +1 -1
  5. package/dist/map-v3/features/{cells/custom → custom}/index.js +1 -1
  6. package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +399 -0
  7. package/dist/map-v3/features/custom/logic/csv-parser.d.ts +31 -0
  8. package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.js +85 -26
  9. package/dist/map-v3/features/{cells/custom → custom}/logic/index.d.ts +1 -1
  10. package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.d.ts +1 -1
  11. package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.d.ts +4 -3
  12. package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.js +30 -10
  13. package/dist/map-v3/features/{cells/custom → custom}/types.d.ts +32 -12
  14. package/dist/map-v3/features/{cells/custom → custom}/types.js +5 -3
  15. package/dist/map-v3/index.d.ts +1 -1
  16. package/dist/map-v3/index.js +1 -1
  17. package/dist/map-v3/shared/controls/MapControl.svelte +43 -15
  18. package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +3 -1
  19. package/package.json +1 -1
  20. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +0 -306
  21. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +0 -262
  22. package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +0 -21
  23. /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte.d.ts +0 -0
  24. /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellSetManager.svelte.d.ts +0 -0
  25. /package/dist/map-v3/features/{cells/custom → custom}/components/index.d.ts +0 -0
  26. /package/dist/map-v3/features/{cells/custom → custom}/components/index.js +0 -0
  27. /package/dist/map-v3/features/{cells/custom → custom}/layers/CustomCellsLayer.svelte.d.ts +0 -0
  28. /package/dist/map-v3/features/{cells/custom → custom}/layers/index.d.ts +0 -0
  29. /package/dist/map-v3/features/{cells/custom → custom}/layers/index.js +0 -0
  30. /package/dist/map-v3/features/{cells/custom → custom}/logic/index.js +0 -0
  31. /package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.js +0 -0
  32. /package/dist/map-v3/features/{cells/custom → custom}/stores/index.d.ts +0 -0
  33. /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>
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Custom Feature - CSV Parser
3
+ *
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
11
+ * Supports both comma (,) and semicolon (;) delimiters
12
+ */
13
+ import type { CustomCellImportResult } from '../types';
14
+ import type { Cell } from '../../../../shared/demo';
15
+ /**
16
+ * Supported delimiters
17
+ */
18
+ export type CsvDelimiter = ',' | ';' | 'auto';
19
+ /**
20
+ * Parse a CSV string into custom items (cells or points)
21
+ * @param csvContent Raw CSV content
22
+ * @param cellLookup Map of cellName -> Cell for resolving cell data
23
+ * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
24
+ * @returns Import result with items, unmatched IDs, groups, and extra columns
25
+ */
26
+ export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, delimiter?: CsvDelimiter): CustomCellImportResult;
27
+ /**
28
+ * Build a cell lookup map from an array of cells
29
+ * Creates lookups for both cellName and txId for flexible matching
30
+ */
31
+ export declare function buildCellLookup(cells: Cell[]): Map<string, Cell>;
@@ -1,14 +1,19 @@
1
1
  /**
2
- * Custom Cells - CSV Parser
2
+ * Custom Feature - CSV Parser
3
3
  *
4
- * Parses CSV files for custom cell sets.
5
- * Required column: cellName (or txId for backwards compatibility)
6
- * Optional columns: customGroup, sizeFactor, + any extras for tooltips
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
11
+ * Supports both comma (,) and semicolon (;) delimiters
7
12
  */
8
13
  /**
9
14
  * Known/reserved column names
10
15
  */
11
- const RESERVED_COLUMNS = ['cellname', 'txid', 'customgroup', 'sizefactor'];
16
+ const RESERVED_COLUMNS = ['id', 'cellname', 'txid', 'customgroup', 'sizefactor', 'lat', 'latitude', 'lon', 'lng', 'longitude'];
12
17
  /**
13
18
  * Normalize column name for matching
14
19
  */
@@ -16,12 +21,22 @@ function normalizeColumnName(name) {
16
21
  return name.trim().toLowerCase();
17
22
  }
18
23
  /**
19
- * Parse a CSV string into custom cells
24
+ * Detect the delimiter used in CSV content
25
+ * Counts occurrences in the header line and picks the most common
26
+ */
27
+ function detectDelimiter(headerLine) {
28
+ const commaCount = (headerLine.match(/,/g) || []).length;
29
+ const semicolonCount = (headerLine.match(/;/g) || []).length;
30
+ return semicolonCount > commaCount ? ';' : ',';
31
+ }
32
+ /**
33
+ * Parse a CSV string into custom items (cells or points)
20
34
  * @param csvContent Raw CSV content
21
35
  * @param cellLookup Map of cellName -> Cell for resolving cell data
22
- * @returns Import result with cells, unmatched IDs, groups, and extra columns
36
+ * @param delimiter Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect)
37
+ * @returns Import result with items, unmatched IDs, groups, and extra columns
23
38
  */
24
- export function parseCustomCellsCsv(csvContent, cellLookup) {
39
+ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto') {
25
40
  const lines = csvContent.trim().split('\n');
26
41
  if (lines.length < 2) {
27
42
  return {
@@ -29,26 +44,35 @@ export function parseCustomCellsCsv(csvContent, cellLookup) {
29
44
  unmatchedTxIds: [],
30
45
  groups: [],
31
46
  extraColumns: [],
32
- totalRows: 0
47
+ totalRows: 0,
48
+ cellCount: 0,
49
+ pointCount: 0
33
50
  };
34
51
  }
35
- // Parse header
52
+ // Detect or use specified delimiter
36
53
  const headerLine = lines[0];
37
- const headers = parseCSVLine(headerLine);
54
+ const actualDelimiter = delimiter === 'auto' ? detectDelimiter(headerLine) : delimiter;
55
+ // Parse header
56
+ const headers = parseCSVLine(headerLine, actualDelimiter);
38
57
  const normalizedHeaders = headers.map(normalizeColumnName);
39
- // Find cell identifier column - prefer cellName, fallback to txId
40
- let idIndex = normalizedHeaders.findIndex(h => h === 'cellname');
41
- let usesCellName = true;
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
+ }
42
63
  if (idIndex === -1) {
43
64
  idIndex = normalizedHeaders.findIndex(h => h === 'txid');
44
- usesCellName = false;
45
65
  }
46
66
  if (idIndex === -1) {
47
- throw new Error('CSV must contain a "cellName" or "txId" column');
67
+ throw new Error('CSV must contain an "id", "cellName", or "txId" column');
48
68
  }
49
69
  // Find optional columns
50
70
  const groupIndex = normalizedHeaders.findIndex(h => h === 'customgroup');
51
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;
52
76
  // Find extra columns (not reserved)
53
77
  const extraColumns = [];
54
78
  const extraIndices = [];
@@ -63,13 +87,15 @@ export function parseCustomCellsCsv(csvContent, cellLookup) {
63
87
  const cells = [];
64
88
  const unmatchedTxIds = [];
65
89
  const groupsSet = new Set();
90
+ let cellCount = 0;
91
+ let pointCount = 0;
66
92
  for (let i = 1; i < lines.length; i++) {
67
93
  const line = lines[i].trim();
68
94
  if (!line)
69
95
  continue;
70
- const values = parseCSVLine(line);
71
- const cellIdentifier = values[idIndex]?.trim();
72
- if (!cellIdentifier)
96
+ const values = parseCSVLine(line, actualDelimiter);
97
+ const itemId = values[idIndex]?.trim();
98
+ if (!itemId)
73
99
  continue;
74
100
  // Get custom group (default to 'default')
75
101
  const customGroup = groupIndex !== -1
@@ -92,19 +118,48 @@ export function parseCustomCellsCsv(csvContent, cellLookup) {
92
118
  const numValue = parseFloat(value);
93
119
  extraFields[extraColumns[i]] = isNaN(numValue) ? value : numValue;
94
120
  });
95
- // Resolve cell from lookup
96
- const resolvedCell = cellLookup.get(cellIdentifier);
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);
97
150
  if (resolvedCell) {
151
+ cellCount++;
98
152
  cells.push({
99
- txId: resolvedCell.txId, // Always store the txId from resolved cell
153
+ id: itemId,
100
154
  customGroup,
101
155
  sizeFactor,
102
156
  extraFields,
157
+ geometry: 'cell',
103
158
  resolvedCell
104
159
  });
105
160
  }
106
161
  else {
107
- unmatchedTxIds.push(cellIdentifier);
162
+ unmatchedTxIds.push(itemId);
108
163
  }
109
164
  }
110
165
  return {
@@ -112,13 +167,17 @@ export function parseCustomCellsCsv(csvContent, cellLookup) {
112
167
  unmatchedTxIds,
113
168
  groups: Array.from(groupsSet).sort(),
114
169
  extraColumns,
115
- totalRows: lines.length - 1
170
+ totalRows: lines.length - 1,
171
+ cellCount,
172
+ pointCount
116
173
  };
117
174
  }
118
175
  /**
119
176
  * Parse a single CSV line, handling quoted fields
177
+ * @param line The CSV line to parse
178
+ * @param delimiter The field delimiter (',' or ';')
120
179
  */
121
- function parseCSVLine(line) {
180
+ function parseCSVLine(line, delimiter = ',') {
122
181
  const result = [];
123
182
  let current = '';
124
183
  let inQuotes = false;
@@ -143,7 +202,7 @@ function parseCSVLine(line) {
143
202
  if (char === '"') {
144
203
  inQuotes = true;
145
204
  }
146
- else if (char === ',') {
205
+ else if (char === delimiter) {
147
206
  result.push(current);
148
207
  current = '';
149
208
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Custom Cells Logic - Barrel Export
3
3
  */
4
- export { parseCustomCellsCsv, buildCellLookup } from './csv-parser';
4
+ export { parseCustomCellsCsv, buildCellLookup, type CsvDelimiter } from './csv-parser';
5
5
  export { buildCustomCellTree, getGroupCounts } from './tree-adapter';
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Builds tree structure for TreeView component, grouped by customGroup
5
5
  */
6
- import type { TreeNode } from '../../../../../core/TreeView/tree.model';
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 '../../types';
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
  /**