@smartnet360/svelte-components 0.0.144 → 0.0.147
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/antenna-tools/components/AntennaTools.svelte +7 -3
- package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +25 -1
- package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +49 -11
- package/dist/map-v3/features/custom/logic/csv-parser.d.ts +15 -3
- package/dist/map-v3/features/custom/logic/csv-parser.js +44 -15
- package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.d.ts +7 -3
- package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.js +16 -4
- package/dist/map-v3/features/custom/types.d.ts +2 -0
- package/dist/shared/csv-import/ColumnMapper.svelte +96 -2
- package/dist/shared/csv-import/ColumnMapper.svelte.d.ts +7 -0
- package/dist/shared/csv-import/column-detector.d.ts +5 -2
- package/dist/shared/csv-import/column-detector.js +13 -6
- package/dist/shared/csv-import/types.js +2 -2
- package/package.json +1 -1
|
@@ -110,11 +110,14 @@
|
|
|
110
110
|
if (externalAntenna1) {
|
|
111
111
|
const found = findAntennaBySpec(antennas, externalAntenna1);
|
|
112
112
|
if (found) {
|
|
113
|
+
// Compute available tilts INLINE to avoid stale state issue
|
|
114
|
+
const tiltsForAntenna1 = getAvailableTiltsForAntenna(found);
|
|
115
|
+
|
|
113
116
|
// Use untrack to prevent infinite loop from state updates
|
|
114
117
|
untrack(() => {
|
|
115
118
|
selectedAntenna = found;
|
|
116
|
-
|
|
117
|
-
ant1ElectricalTilt = findTiltIndex(
|
|
119
|
+
availableElectricalTilts = tiltsForAntenna1;
|
|
120
|
+
ant1ElectricalTilt = findTiltIndex(tiltsForAntenna1, externalAntenna1.electricalTilt ?? 0);
|
|
118
121
|
ant1MechanicalTilt = externalAntenna1.mechanicalTilt ?? 0;
|
|
119
122
|
});
|
|
120
123
|
} else {
|
|
@@ -126,9 +129,10 @@
|
|
|
126
129
|
if (externalAntenna2) {
|
|
127
130
|
const found2 = findAntennaBySpec(antennas, externalAntenna2);
|
|
128
131
|
if (found2) {
|
|
132
|
+
const tiltsForAntenna2 = getAvailableTiltsForAntenna(found2);
|
|
129
133
|
untrack(() => {
|
|
130
134
|
selectedAntenna2 = found2;
|
|
131
|
-
ant2ElectricalTilt = findTiltIndex(
|
|
135
|
+
ant2ElectricalTilt = findTiltIndex(tiltsForAntenna2, externalAntenna2.electricalTilt ?? 0);
|
|
132
136
|
ant2MechanicalTilt = externalAntenna2.mechanicalTilt ?? 0;
|
|
133
137
|
if (viewMode === 'single') {
|
|
134
138
|
viewMode = 'compare';
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
let csvHeaders = $state<string[]>([]);
|
|
72
72
|
let columnMapping = $state<ColumnMapping>({});
|
|
73
73
|
let importMode = $state<ImportMode>('cell');
|
|
74
|
+
let includeAllExtras = $state(true);
|
|
75
|
+
let selectedExtras = $state<number[]>([]);
|
|
74
76
|
|
|
75
77
|
// Quick Add state
|
|
76
78
|
let showQuickAdd = $state(false);
|
|
@@ -203,7 +205,11 @@
|
|
|
203
205
|
if (!pendingCsvContent) return;
|
|
204
206
|
|
|
205
207
|
try {
|
|
206
|
-
importResult = setsStore.importFromCsv(pendingCsvContent, importFileName,
|
|
208
|
+
importResult = setsStore.importFromCsv(pendingCsvContent, importFileName, {
|
|
209
|
+
columnMapping,
|
|
210
|
+
includeAllExtras,
|
|
211
|
+
selectedExtraIndices: selectedExtras
|
|
212
|
+
});
|
|
207
213
|
showMappingModal = false;
|
|
208
214
|
showImportModal = true;
|
|
209
215
|
} catch (err) {
|
|
@@ -219,6 +225,8 @@
|
|
|
219
225
|
csvHeaders = [];
|
|
220
226
|
columnMapping = {};
|
|
221
227
|
importMode = 'cell';
|
|
228
|
+
includeAllExtras = true;
|
|
229
|
+
selectedExtras = [];
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
function confirmImport() {
|
|
@@ -486,6 +494,7 @@
|
|
|
486
494
|
{#each setsStore.sets as set (set.id)}
|
|
487
495
|
{@const isExpanded = expandedSets.has(set.id)}
|
|
488
496
|
{@const treeStore = treeStores.get(set.id)}
|
|
497
|
+
{@const hasPoints = set.cells.some(c => c.geometry === 'point')}
|
|
489
498
|
<div class="set-section" class:expanded={isExpanded}>
|
|
490
499
|
<!-- Set Header -->
|
|
491
500
|
<div class="set-header d-flex align-items-center justify-content-between px-2 py-2">
|
|
@@ -504,6 +513,15 @@
|
|
|
504
513
|
>
|
|
505
514
|
<i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
|
|
506
515
|
</button>
|
|
516
|
+
<button
|
|
517
|
+
class="btn btn-sm p-0 me-2"
|
|
518
|
+
title={!hasPoints ? 'No points to label' : set.showLabels ? 'Hide Labels' : 'Show Labels'}
|
|
519
|
+
aria-label={!hasPoints ? 'No points to label' : set.showLabels ? 'Hide Labels' : 'Show Labels'}
|
|
520
|
+
onclick={() => setsStore.toggleSetLabels(set.id)}
|
|
521
|
+
disabled={!hasPoints}
|
|
522
|
+
>
|
|
523
|
+
<i class="bi bi-tag{!hasPoints ? ' text-muted opacity-50' : set.showLabels ? '-fill text-primary' : ' text-muted'}"></i>
|
|
524
|
+
</button>
|
|
507
525
|
<div class="set-info text-truncate" onclick={() => toggleExpanded(set.id)} style="cursor: pointer;">
|
|
508
526
|
<div class="fw-medium small text-truncate">{set.name}</div>
|
|
509
527
|
<small class="text-muted">{getSetItemCounts(set)} · {set.groups.length} groups</small>
|
|
@@ -596,8 +614,14 @@
|
|
|
596
614
|
headers={csvHeaders}
|
|
597
615
|
mode={importMode}
|
|
598
616
|
mapping={columnMapping}
|
|
617
|
+
{includeAllExtras}
|
|
618
|
+
{selectedExtras}
|
|
599
619
|
onmodechange={(m) => importMode = m}
|
|
600
620
|
onchange={(m) => columnMapping = m}
|
|
621
|
+
onextraschange={(all, selected) => {
|
|
622
|
+
includeAllExtras = all;
|
|
623
|
+
selectedExtras = selected;
|
|
624
|
+
}}
|
|
601
625
|
/>
|
|
602
626
|
</div>
|
|
603
627
|
<div class="modal-footer py-2">
|
|
@@ -51,11 +51,13 @@
|
|
|
51
51
|
cellFill: string;
|
|
52
52
|
cellLine: string;
|
|
53
53
|
pointCircle: string;
|
|
54
|
+
pointLabel: string;
|
|
54
55
|
} {
|
|
55
56
|
return {
|
|
56
57
|
cellFill: `custom-cells-fill-${setId}`,
|
|
57
58
|
cellLine: `custom-cells-line-${setId}`,
|
|
58
|
-
pointCircle: `custom-points-circle-${setId}
|
|
59
|
+
pointCircle: `custom-points-circle-${setId}`,
|
|
60
|
+
pointLabel: `custom-points-label-${setId}`
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -129,6 +131,30 @@
|
|
|
129
131
|
});
|
|
130
132
|
activeLayers.add(layers.pointCircle);
|
|
131
133
|
}
|
|
134
|
+
|
|
135
|
+
// Add points label layer (symbol layer for text)
|
|
136
|
+
if (!map.getLayer(layers.pointLabel)) {
|
|
137
|
+
map.addLayer({
|
|
138
|
+
id: layers.pointLabel,
|
|
139
|
+
type: 'symbol',
|
|
140
|
+
source: sources.points,
|
|
141
|
+
layout: {
|
|
142
|
+
'text-field': ['get', 'labelText'],
|
|
143
|
+
'text-size': 11,
|
|
144
|
+
'text-anchor': 'top',
|
|
145
|
+
'text-offset': [0, 0.8],
|
|
146
|
+
'text-allow-overlap': false,
|
|
147
|
+
'text-ignore-placement': false,
|
|
148
|
+
'visibility': set.showLabels ? 'visible' : 'none'
|
|
149
|
+
},
|
|
150
|
+
paint: {
|
|
151
|
+
'text-color': '#333',
|
|
152
|
+
'text-halo-color': '#fff',
|
|
153
|
+
'text-halo-width': 1.5
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
activeLayers.add(layers.pointLabel);
|
|
157
|
+
}
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
/**
|
|
@@ -138,8 +164,8 @@
|
|
|
138
164
|
const sources = getSourceIds(setIdToRemove);
|
|
139
165
|
const layers = getLayerIds(setIdToRemove);
|
|
140
166
|
|
|
141
|
-
// Remove layers
|
|
142
|
-
for (const layerId of [layers.cellLine, layers.cellFill, layers.pointCircle]) {
|
|
167
|
+
// Remove layers (label layer must be removed first since it's on top)
|
|
168
|
+
for (const layerId of [layers.pointLabel, layers.cellLine, layers.cellFill, layers.pointCircle]) {
|
|
143
169
|
if (map.getLayer(layerId)) {
|
|
144
170
|
map.removeLayer(layerId);
|
|
145
171
|
activeLayers.delete(layerId);
|
|
@@ -189,6 +215,9 @@
|
|
|
189
215
|
// Darken color for stroke
|
|
190
216
|
const strokeColor = darkenColor(color, 0.3);
|
|
191
217
|
|
|
218
|
+
// Generate label text from first 3 extra fields
|
|
219
|
+
const labelText = buildLabelText(item.extraFields);
|
|
220
|
+
|
|
192
221
|
pointFeatures.push({
|
|
193
222
|
type: 'Feature',
|
|
194
223
|
geometry: {
|
|
@@ -203,9 +232,8 @@
|
|
|
203
232
|
customGroup: item.customGroup,
|
|
204
233
|
sizeFactor: item.sizeFactor,
|
|
205
234
|
setName: set.name,
|
|
206
|
-
...
|
|
207
|
-
|
|
208
|
-
)
|
|
235
|
+
popupInfo: { ...item.extraFields },
|
|
236
|
+
labelText
|
|
209
237
|
}
|
|
210
238
|
});
|
|
211
239
|
} else if (item.geometry === 'cell' && item.resolvedCell) {
|
|
@@ -226,11 +254,7 @@
|
|
|
226
254
|
feature.properties.customGroup = item.customGroup;
|
|
227
255
|
feature.properties.sizeFactor = item.sizeFactor;
|
|
228
256
|
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
|
-
}
|
|
257
|
+
feature.properties.popupInfo = { ...item.extraFields };
|
|
234
258
|
}
|
|
235
259
|
|
|
236
260
|
cellFeatures.push(feature);
|
|
@@ -265,9 +289,23 @@
|
|
|
265
289
|
map.setPaintProperty(layers.pointCircle, 'circle-opacity', set.opacity);
|
|
266
290
|
}
|
|
267
291
|
|
|
292
|
+
// Update label visibility
|
|
293
|
+
if (map.getLayer(layers.pointLabel)) {
|
|
294
|
+
map.setLayoutProperty(layers.pointLabel, 'visibility', set.showLabels ? 'visible' : 'none');
|
|
295
|
+
}
|
|
296
|
+
|
|
268
297
|
console.log(`[CustomLayer] Rendered ${cellFeatures.length} cells, ${pointFeatures.length} points for set "${set.name}"`);
|
|
269
298
|
}
|
|
270
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Build label text from extra fields (first 3 values, newline separated)
|
|
302
|
+
*/
|
|
303
|
+
function buildLabelText(extraFields: Record<string, string | number> | undefined): string {
|
|
304
|
+
if (!extraFields) return '';
|
|
305
|
+
const values = Object.values(extraFields).slice(0, 3);
|
|
306
|
+
return values.filter(v => v !== undefined && v !== null && v !== '').join('\n');
|
|
307
|
+
}
|
|
308
|
+
|
|
271
309
|
/**
|
|
272
310
|
* Darken a hex color by a factor
|
|
273
311
|
*/
|
|
@@ -26,15 +26,27 @@ export type CsvDelimiter = ',' | ';' | 'auto';
|
|
|
26
26
|
* @returns Array of header strings
|
|
27
27
|
*/
|
|
28
28
|
export declare function extractCsvHeaders(csvContent: string, delimiter?: CsvDelimiter): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Options for CSV parsing
|
|
31
|
+
*/
|
|
32
|
+
export interface CsvParseOptions {
|
|
33
|
+
/** Delimiter to use: ',' (comma), ';' (semicolon), or 'auto' (detect) */
|
|
34
|
+
delimiter?: CsvDelimiter;
|
|
35
|
+
/** User-defined column mapping (overrides auto-detection) */
|
|
36
|
+
columnMapping?: ColumnMapping;
|
|
37
|
+
/** Whether to include all extra columns (default: true) */
|
|
38
|
+
includeAllExtras?: boolean;
|
|
39
|
+
/** Specific extra column indices to include (when includeAllExtras is false) */
|
|
40
|
+
selectedExtraIndices?: number[];
|
|
41
|
+
}
|
|
29
42
|
/**
|
|
30
43
|
* Parse a CSV string into custom items (cells or points)
|
|
31
44
|
* @param csvContent Raw CSV content
|
|
32
45
|
* @param cellLookup Map of cellName -> Cell for resolving cell data
|
|
33
|
-
* @param
|
|
34
|
-
* @param columnMapping Optional user-defined column mapping (overrides auto-detection)
|
|
46
|
+
* @param options Parsing options
|
|
35
47
|
* @returns Import result with items, unmatched IDs, groups, and extra columns
|
|
36
48
|
*/
|
|
37
|
-
export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>,
|
|
49
|
+
export declare function parseCustomCellsCsv(csvContent: string, cellLookup: Map<string, Cell>, options?: CsvParseOptions): CustomCellImportResult;
|
|
38
50
|
/**
|
|
39
51
|
* Build a cell lookup map from an array of cells
|
|
40
52
|
* Creates lookups for both cellName and txId for flexible matching
|
|
@@ -49,11 +49,11 @@ export function extractCsvHeaders(csvContent, delimiter = 'auto') {
|
|
|
49
49
|
* Parse a CSV string into custom items (cells or points)
|
|
50
50
|
* @param csvContent Raw CSV content
|
|
51
51
|
* @param cellLookup Map of cellName -> Cell for resolving cell data
|
|
52
|
-
* @param
|
|
53
|
-
* @param columnMapping Optional user-defined column mapping (overrides auto-detection)
|
|
52
|
+
* @param options Parsing options
|
|
54
53
|
* @returns Import result with items, unmatched IDs, groups, and extra columns
|
|
55
54
|
*/
|
|
56
|
-
export function parseCustomCellsCsv(csvContent, cellLookup,
|
|
55
|
+
export function parseCustomCellsCsv(csvContent, cellLookup, options = {}) {
|
|
56
|
+
const { delimiter = 'auto', columnMapping, includeAllExtras = true, selectedExtraIndices = [] } = options;
|
|
57
57
|
const lines = csvContent.trim().split('\n');
|
|
58
58
|
if (lines.length < 2) {
|
|
59
59
|
return {
|
|
@@ -106,30 +106,55 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto',
|
|
|
106
106
|
throw new Error('CSV must contain an "id", "cellName", or "txId" column');
|
|
107
107
|
}
|
|
108
108
|
const hasLatLon = latIndex !== -1 && lonIndex !== -1;
|
|
109
|
-
// Find extra columns
|
|
109
|
+
// Find extra columns based on user selection
|
|
110
110
|
const extraColumns = [];
|
|
111
111
|
const extraIndices = [];
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
// Get indices that are already mapped to known fields
|
|
113
|
+
const mappedIndices = new Set([idIndex, groupIndex, sizeFactorIndex, latIndex, lonIndex].filter(i => i >= 0));
|
|
114
|
+
if (includeAllExtras) {
|
|
115
|
+
// Include all non-reserved, non-mapped columns
|
|
116
|
+
headers.forEach((header, idx) => {
|
|
117
|
+
if (mappedIndices.has(idx))
|
|
118
|
+
return;
|
|
119
|
+
const normalized = normalizeColumnName(header);
|
|
120
|
+
if (!RESERVED_COLUMNS.includes(normalized)) {
|
|
121
|
+
extraColumns.push(header.trim());
|
|
122
|
+
extraIndices.push(idx);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Include only specifically selected columns
|
|
128
|
+
for (const idx of selectedExtraIndices) {
|
|
129
|
+
if (idx >= 0 && idx < headers.length && !mappedIndices.has(idx)) {
|
|
130
|
+
extraColumns.push(headers[idx].trim());
|
|
131
|
+
extraIndices.push(idx);
|
|
132
|
+
}
|
|
117
133
|
}
|
|
118
|
-
}
|
|
134
|
+
}
|
|
119
135
|
// Parse data rows
|
|
120
136
|
const cells = [];
|
|
121
137
|
const unmatchedTxIds = [];
|
|
122
138
|
const groupsSet = new Set();
|
|
123
139
|
let cellCount = 0;
|
|
124
140
|
let pointCount = 0;
|
|
141
|
+
let autoIdCounter = 0;
|
|
125
142
|
for (let i = 1; i < lines.length; i++) {
|
|
126
143
|
const line = lines[i].trim();
|
|
127
144
|
if (!line)
|
|
128
145
|
continue;
|
|
129
146
|
const values = parseCSVLine(line, actualDelimiter);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
// Get item ID - auto-generate for points if no ID column mapped
|
|
148
|
+
let itemId;
|
|
149
|
+
if (idIndex >= 0) {
|
|
150
|
+
itemId = values[idIndex]?.trim() || '';
|
|
151
|
+
if (!itemId)
|
|
152
|
+
continue; // Skip rows with empty ID when ID column is mapped
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// No ID column mapped - will auto-generate for points
|
|
156
|
+
itemId = '';
|
|
157
|
+
}
|
|
133
158
|
// Get custom group (default to 'default')
|
|
134
159
|
const customGroup = groupIndex !== -1
|
|
135
160
|
? (values[groupIndex]?.trim() || 'default')
|
|
@@ -171,8 +196,10 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto',
|
|
|
171
196
|
lat = parsedLat;
|
|
172
197
|
lon = parsedLon;
|
|
173
198
|
pointCount++;
|
|
199
|
+
// Auto-generate ID for points if not mapped
|
|
200
|
+
const pointId = itemId || `Point ${++autoIdCounter}`;
|
|
174
201
|
cells.push({
|
|
175
|
-
id:
|
|
202
|
+
id: pointId,
|
|
176
203
|
customGroup,
|
|
177
204
|
sizeFactor,
|
|
178
205
|
extraFields,
|
|
@@ -183,7 +210,9 @@ export function parseCustomCellsCsv(csvContent, cellLookup, delimiter = 'auto',
|
|
|
183
210
|
continue;
|
|
184
211
|
}
|
|
185
212
|
}
|
|
186
|
-
// No valid lat/lon - try to resolve as cell
|
|
213
|
+
// No valid lat/lon - try to resolve as cell (requires ID)
|
|
214
|
+
if (!itemId)
|
|
215
|
+
continue; // Can't resolve cell without ID
|
|
187
216
|
resolvedCell = cellLookup.get(itemId);
|
|
188
217
|
if (resolvedCell) {
|
|
189
218
|
cellCount++;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { Cell } from '../../../../shared/demo';
|
|
8
8
|
import type { CellDataStore } from '../../cells/stores/cell.data.svelte';
|
|
9
9
|
import type { CustomCellSet, CustomCellImportResult } from '../types';
|
|
10
|
-
import type
|
|
10
|
+
import { type CsvParseOptions } from '../logic/csv-parser';
|
|
11
11
|
/** Function that returns the current cells array */
|
|
12
12
|
type CellsGetter = () => Cell[];
|
|
13
13
|
/**
|
|
@@ -32,9 +32,9 @@ export declare class CustomCellSetsStore {
|
|
|
32
32
|
* Import a CSV file and create a new custom cell set
|
|
33
33
|
* @param csvContent Raw CSV content
|
|
34
34
|
* @param fileName Name of the file being imported
|
|
35
|
-
* @param
|
|
35
|
+
* @param options Parsing options (column mapping, extra fields selection)
|
|
36
36
|
*/
|
|
37
|
-
importFromCsv(csvContent: string, fileName: string,
|
|
37
|
+
importFromCsv(csvContent: string, fileName: string, options?: CsvParseOptions): CustomCellImportResult;
|
|
38
38
|
/**
|
|
39
39
|
* Create a new set from import result
|
|
40
40
|
*/
|
|
@@ -51,6 +51,10 @@ export declare class CustomCellSetsStore {
|
|
|
51
51
|
* Toggle set visibility
|
|
52
52
|
*/
|
|
53
53
|
toggleSetVisibility(setId: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Toggle set labels visibility
|
|
56
|
+
*/
|
|
57
|
+
toggleSetLabels(setId: string): void;
|
|
54
58
|
/**
|
|
55
59
|
* Toggle group visibility within a set
|
|
56
60
|
*/
|
|
@@ -69,13 +69,13 @@ export class CustomCellSetsStore {
|
|
|
69
69
|
* Import a CSV file and create a new custom cell set
|
|
70
70
|
* @param csvContent Raw CSV content
|
|
71
71
|
* @param fileName Name of the file being imported
|
|
72
|
-
* @param
|
|
72
|
+
* @param options Parsing options (column mapping, extra fields selection)
|
|
73
73
|
*/
|
|
74
|
-
importFromCsv(csvContent, fileName,
|
|
74
|
+
importFromCsv(csvContent, fileName, options) {
|
|
75
75
|
// Build lookup from all cells
|
|
76
76
|
const cellLookup = buildCellLookup(this.getCells());
|
|
77
|
-
// Parse CSV with
|
|
78
|
-
const result = parseCustomCellsCsv(csvContent, cellLookup,
|
|
77
|
+
// Parse CSV with options
|
|
78
|
+
const result = parseCustomCellsCsv(csvContent, cellLookup, options || {});
|
|
79
79
|
return result;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
@@ -101,6 +101,7 @@ export class CustomCellSetsStore {
|
|
|
101
101
|
defaultColor: CUSTOM_CELL_PALETTE[0],
|
|
102
102
|
groupColors,
|
|
103
103
|
visible: true,
|
|
104
|
+
showLabels: false,
|
|
104
105
|
visibleGroups: new Set(importResult.groups)
|
|
105
106
|
};
|
|
106
107
|
this.sets.push(newSet);
|
|
@@ -136,6 +137,16 @@ export class CustomCellSetsStore {
|
|
|
136
137
|
this.version++;
|
|
137
138
|
}
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Toggle set labels visibility
|
|
142
|
+
*/
|
|
143
|
+
toggleSetLabels(setId) {
|
|
144
|
+
const set = this.getSet(setId);
|
|
145
|
+
if (set) {
|
|
146
|
+
set.showLabels = !set.showLabels;
|
|
147
|
+
this.version++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
139
150
|
/**
|
|
140
151
|
* Toggle group visibility within a set
|
|
141
152
|
*/
|
|
@@ -227,6 +238,7 @@ export class CustomCellSetsStore {
|
|
|
227
238
|
this.sets = data.map((setData) => ({
|
|
228
239
|
...setData,
|
|
229
240
|
visibleGroups: new Set(setData.visibleGroups || []),
|
|
241
|
+
showLabels: setData.showLabels ?? false,
|
|
230
242
|
cells: setData.cells || []
|
|
231
243
|
}));
|
|
232
244
|
// Re-resolve cells
|
|
@@ -81,6 +81,8 @@ export interface CustomCellSet {
|
|
|
81
81
|
groupColors: Record<string, string>;
|
|
82
82
|
/** Whether this set's layer is visible */
|
|
83
83
|
visible: boolean;
|
|
84
|
+
/** Whether to show labels for points */
|
|
85
|
+
showLabels: boolean;
|
|
84
86
|
/** Which groups are currently visible */
|
|
85
87
|
visibleGroups: Set<string>;
|
|
86
88
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Compact UI for mapping CSV columns to expected fields.
|
|
6
6
|
* Includes import mode selection (Cell/Point/Site) with auto-detection.
|
|
7
|
+
* Supports extra field selection for tooltips.
|
|
7
8
|
*/
|
|
8
9
|
import type { ColumnMapping, ImportFieldsConfig, ImportMode } from './types';
|
|
9
10
|
import { getFieldsForMode } from './types';
|
|
@@ -16,18 +17,27 @@
|
|
|
16
17
|
mode: ImportMode;
|
|
17
18
|
/** Current mapping (can be auto-detected or user-modified) */
|
|
18
19
|
mapping: ColumnMapping;
|
|
20
|
+
/** Whether to include all extra columns */
|
|
21
|
+
includeAllExtras?: boolean;
|
|
22
|
+
/** Selected extra column indices (when not including all) */
|
|
23
|
+
selectedExtras?: number[];
|
|
19
24
|
/** Called when mode changes */
|
|
20
25
|
onmodechange?: (mode: ImportMode) => void;
|
|
21
26
|
/** Called when mapping changes */
|
|
22
27
|
onchange?: (mapping: ColumnMapping) => void;
|
|
28
|
+
/** Called when extra fields settings change */
|
|
29
|
+
onextraschange?: (includeAll: boolean, selected: number[]) => void;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
let {
|
|
26
33
|
headers,
|
|
27
34
|
mode,
|
|
28
35
|
mapping,
|
|
36
|
+
includeAllExtras = true,
|
|
37
|
+
selectedExtras = [],
|
|
29
38
|
onmodechange,
|
|
30
|
-
onchange
|
|
39
|
+
onchange,
|
|
40
|
+
onextraschange
|
|
31
41
|
}: Props = $props();
|
|
32
42
|
|
|
33
43
|
// Get fields config based on current mode
|
|
@@ -36,6 +46,18 @@
|
|
|
36
46
|
// All fields to display
|
|
37
47
|
let allFields = $derived([...fieldsConfig.required, ...fieldsConfig.optional]);
|
|
38
48
|
|
|
49
|
+
// Get mapped column indices (to exclude from extra fields options)
|
|
50
|
+
let mappedIndices = $derived(new Set(
|
|
51
|
+
Object.values(mapping).filter((v): v is number => v !== null && v !== undefined)
|
|
52
|
+
));
|
|
53
|
+
|
|
54
|
+
// Available columns for extra fields (not already mapped)
|
|
55
|
+
let availableExtras = $derived(
|
|
56
|
+
headers
|
|
57
|
+
.map((header, idx) => ({ header, idx }))
|
|
58
|
+
.filter(({ idx }) => !mappedIndices.has(idx))
|
|
59
|
+
);
|
|
60
|
+
|
|
39
61
|
// Validation state
|
|
40
62
|
let missingRequired = $derived(
|
|
41
63
|
fieldsConfig.required.filter(f =>
|
|
@@ -78,6 +100,35 @@
|
|
|
78
100
|
const idx = mapping[fieldType];
|
|
79
101
|
return idx !== null && idx !== undefined ? headers[idx] : null;
|
|
80
102
|
}
|
|
103
|
+
|
|
104
|
+
// Extra fields handlers
|
|
105
|
+
function handleIncludeAllChange(event: Event) {
|
|
106
|
+
const checked = (event.target as HTMLInputElement).checked;
|
|
107
|
+
onextraschange?.(checked, selectedExtras);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleExtraFieldChange(slotIndex: number, event: Event) {
|
|
111
|
+
const select = event.target as HTMLSelectElement;
|
|
112
|
+
const value = select.value;
|
|
113
|
+
|
|
114
|
+
const newSelected = [...selectedExtras];
|
|
115
|
+
|
|
116
|
+
if (value === '') {
|
|
117
|
+
// Remove this slot
|
|
118
|
+
newSelected[slotIndex] = -1;
|
|
119
|
+
} else {
|
|
120
|
+
newSelected[slotIndex] = parseInt(value, 10);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Filter out -1 values and duplicates
|
|
124
|
+
const filtered = newSelected.filter((v, i) => v >= 0 && newSelected.indexOf(v) === i);
|
|
125
|
+
onextraschange?.(includeAllExtras, filtered);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getExtraSlotValue(slotIndex: number): string {
|
|
129
|
+
const value = selectedExtras[slotIndex];
|
|
130
|
+
return value === undefined || value < 0 ? '' : String(value);
|
|
131
|
+
}
|
|
81
132
|
</script>
|
|
82
133
|
|
|
83
134
|
<div class="column-mapper">
|
|
@@ -150,13 +201,56 @@
|
|
|
150
201
|
>
|
|
151
202
|
<option value="">{field.description || '-- Select --'}</option>
|
|
152
203
|
{#each headers as header, idx}
|
|
153
|
-
<option value={idx}>{header}</option>
|
|
204
|
+
<option value={String(idx)}>{header}</option>
|
|
154
205
|
{/each}
|
|
155
206
|
</select>
|
|
156
207
|
</div>
|
|
157
208
|
{/each}
|
|
158
209
|
</div>
|
|
159
210
|
|
|
211
|
+
<!-- Extra Fields Section -->
|
|
212
|
+
{#if availableExtras.length > 0}
|
|
213
|
+
<div class="extra-fields mt-3 pt-2 border-top">
|
|
214
|
+
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
215
|
+
<span class="small fw-medium">Extra columns for popups</span>
|
|
216
|
+
<div class="form-check form-check-inline mb-0">
|
|
217
|
+
<input
|
|
218
|
+
class="form-check-input"
|
|
219
|
+
type="checkbox"
|
|
220
|
+
id="include-all-extras"
|
|
221
|
+
checked={includeAllExtras}
|
|
222
|
+
onchange={handleIncludeAllChange}
|
|
223
|
+
/>
|
|
224
|
+
<label class="form-check-label small" for="include-all-extras">
|
|
225
|
+
Include all ({availableExtras.length})
|
|
226
|
+
</label>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{#if !includeAllExtras}
|
|
231
|
+
<div class="extra-slots">
|
|
232
|
+
{#each [0, 1, 2] as slotIndex}
|
|
233
|
+
{@const slotId = `extra-slot-${slotIndex}`}
|
|
234
|
+
<div class="d-flex align-items-center gap-2 mb-1">
|
|
235
|
+
<select
|
|
236
|
+
id={slotId}
|
|
237
|
+
class="form-select form-select-sm"
|
|
238
|
+
value={getExtraSlotValue(slotIndex)}
|
|
239
|
+
onchange={(e) => handleExtraFieldChange(slotIndex, e)}
|
|
240
|
+
aria-label={`Extra field ${slotIndex + 1}`}
|
|
241
|
+
>
|
|
242
|
+
<option value="">-- Extra {slotIndex + 1} --</option>
|
|
243
|
+
{#each availableExtras as { header, idx }}
|
|
244
|
+
<option value={String(idx)}>{header}</option>
|
|
245
|
+
{/each}
|
|
246
|
+
</select>
|
|
247
|
+
</div>
|
|
248
|
+
{/each}
|
|
249
|
+
</div>
|
|
250
|
+
{/if}
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|
|
253
|
+
|
|
160
254
|
<!-- Validation Warning -->
|
|
161
255
|
{#if missingRequired.length > 0}
|
|
162
256
|
<div class="alert alert-warning py-1 px-2 mt-2 mb-0 small">
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Compact UI for mapping CSV columns to expected fields.
|
|
5
5
|
* Includes import mode selection (Cell/Point/Site) with auto-detection.
|
|
6
|
+
* Supports extra field selection for tooltips.
|
|
6
7
|
*/
|
|
7
8
|
import type { ColumnMapping, ImportMode } from './types';
|
|
8
9
|
interface Props {
|
|
@@ -12,10 +13,16 @@ interface Props {
|
|
|
12
13
|
mode: ImportMode;
|
|
13
14
|
/** Current mapping (can be auto-detected or user-modified) */
|
|
14
15
|
mapping: ColumnMapping;
|
|
16
|
+
/** Whether to include all extra columns */
|
|
17
|
+
includeAllExtras?: boolean;
|
|
18
|
+
/** Selected extra column indices (when not including all) */
|
|
19
|
+
selectedExtras?: number[];
|
|
15
20
|
/** Called when mode changes */
|
|
16
21
|
onmodechange?: (mode: ImportMode) => void;
|
|
17
22
|
/** Called when mapping changes */
|
|
18
23
|
onchange?: (mapping: ColumnMapping) => void;
|
|
24
|
+
/** Called when extra fields settings change */
|
|
25
|
+
onextraschange?: (includeAll: boolean, selected: number[]) => void;
|
|
19
26
|
}
|
|
20
27
|
declare const ColumnMapper: import("svelte").Component<Props, {}, "">;
|
|
21
28
|
type ColumnMapper = ReturnType<typeof ColumnMapper>;
|
|
@@ -17,8 +17,11 @@ export declare function detectFieldType(header: string): StandardFieldType | nul
|
|
|
17
17
|
/**
|
|
18
18
|
* Auto-detect column mapping from headers
|
|
19
19
|
* Returns suggested mapping based on alias matching
|
|
20
|
+
* @param headers - CSV column headers
|
|
21
|
+
* @param fieldsConfig - Field configuration for the import mode
|
|
22
|
+
* @param mode - Import mode (used to customize detection behavior)
|
|
20
23
|
*/
|
|
21
|
-
export declare function detectColumnMapping(headers: string[], fieldsConfig: ImportFieldsConfig): ColumnMapping;
|
|
24
|
+
export declare function detectColumnMapping(headers: string[], fieldsConfig: ImportFieldsConfig, mode?: ImportMode): ColumnMapping;
|
|
22
25
|
/**
|
|
23
26
|
* Validate that all required fields are mapped
|
|
24
27
|
*/
|
|
@@ -50,7 +53,7 @@ export declare function parseHeaderLine(line: string, delimiter: ',' | ';'): str
|
|
|
50
53
|
/**
|
|
51
54
|
* Get CSV headers and suggested mapping
|
|
52
55
|
*/
|
|
53
|
-
export declare function analyzeCSVHeaders(csvContent: string, fieldsConfig: ImportFieldsConfig): {
|
|
56
|
+
export declare function analyzeCSVHeaders(csvContent: string, fieldsConfig: ImportFieldsConfig, mode?: ImportMode): {
|
|
54
57
|
headers: string[];
|
|
55
58
|
delimiter: ',' | ';';
|
|
56
59
|
suggestedMapping: ColumnMapping;
|
|
@@ -96,14 +96,21 @@ export function detectFieldType(header) {
|
|
|
96
96
|
/**
|
|
97
97
|
* Auto-detect column mapping from headers
|
|
98
98
|
* Returns suggested mapping based on alias matching
|
|
99
|
+
* @param headers - CSV column headers
|
|
100
|
+
* @param fieldsConfig - Field configuration for the import mode
|
|
101
|
+
* @param mode - Import mode (used to customize detection behavior)
|
|
99
102
|
*/
|
|
100
|
-
export function detectColumnMapping(headers, fieldsConfig) {
|
|
103
|
+
export function detectColumnMapping(headers, fieldsConfig, mode) {
|
|
101
104
|
const mapping = {};
|
|
102
105
|
const usedIndices = new Set();
|
|
103
106
|
// Get all field types we're looking for
|
|
104
107
|
const allFields = [...fieldsConfig.required, ...fieldsConfig.optional];
|
|
108
|
+
// For point mode, skip auto-detecting ID - default to auto-generated
|
|
109
|
+
const fieldsToDetect = mode === 'point'
|
|
110
|
+
? allFields.filter(f => f.type !== 'id')
|
|
111
|
+
: allFields;
|
|
105
112
|
// First pass: exact matches and strong matches
|
|
106
|
-
for (const field of
|
|
113
|
+
for (const field of fieldsToDetect) {
|
|
107
114
|
const aliases = COLUMN_ALIASES[field.type] || [];
|
|
108
115
|
for (let i = 0; i < headers.length; i++) {
|
|
109
116
|
if (usedIndices.has(i))
|
|
@@ -118,7 +125,7 @@ export function detectColumnMapping(headers, fieldsConfig) {
|
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
// Second pass: partial matches for unmapped fields
|
|
121
|
-
for (const field of
|
|
128
|
+
for (const field of fieldsToDetect) {
|
|
122
129
|
if (mapping[field.type] !== undefined)
|
|
123
130
|
continue;
|
|
124
131
|
const aliases = COLUMN_ALIASES[field.type] || [];
|
|
@@ -190,7 +197,7 @@ export function detectImportMode(headers) {
|
|
|
190
197
|
export function autoDetectImport(headers) {
|
|
191
198
|
const mode = detectImportMode(headers);
|
|
192
199
|
const fieldsConfig = getFieldsForMode(mode);
|
|
193
|
-
const mapping = detectColumnMapping(headers, fieldsConfig);
|
|
200
|
+
const mapping = detectColumnMapping(headers, fieldsConfig, mode);
|
|
194
201
|
return { mode, mapping, fieldsConfig };
|
|
195
202
|
}
|
|
196
203
|
/**
|
|
@@ -211,14 +218,14 @@ export function parseHeaderLine(line, delimiter) {
|
|
|
211
218
|
/**
|
|
212
219
|
* Get CSV headers and suggested mapping
|
|
213
220
|
*/
|
|
214
|
-
export function analyzeCSVHeaders(csvContent, fieldsConfig) {
|
|
221
|
+
export function analyzeCSVHeaders(csvContent, fieldsConfig, mode) {
|
|
215
222
|
const lines = csvContent.trim().split('\n');
|
|
216
223
|
if (lines.length === 0) {
|
|
217
224
|
return { headers: [], delimiter: ',', suggestedMapping: {}, rowCount: 0 };
|
|
218
225
|
}
|
|
219
226
|
const delimiter = detectDelimiter(lines[0]);
|
|
220
227
|
const headers = parseHeaderLine(lines[0], delimiter);
|
|
221
|
-
const suggestedMapping = detectColumnMapping(headers, fieldsConfig);
|
|
228
|
+
const suggestedMapping = detectColumnMapping(headers, fieldsConfig, mode);
|
|
222
229
|
return {
|
|
223
230
|
headers,
|
|
224
231
|
delimiter,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
/** Cell mode - ID resolves against existing cell data */
|
|
10
10
|
export const CELL_MODE_FIELDS = {
|
|
11
11
|
required: [
|
|
12
|
-
{ type: 'id', label: 'Cell
|
|
12
|
+
{ type: 'id', label: 'txId / (Cell Name) ', required: true, description: 'Column to match against cell data' }
|
|
13
13
|
],
|
|
14
14
|
optional: [
|
|
15
15
|
{ type: 'group', label: 'Group', required: false, description: 'e.g. customGroup, category' },
|
|
@@ -19,11 +19,11 @@ export const CELL_MODE_FIELDS = {
|
|
|
19
19
|
/** Point mode - creates markers at lat/lon coordinates */
|
|
20
20
|
export const POINT_MODE_FIELDS = {
|
|
21
21
|
required: [
|
|
22
|
-
{ type: 'id', label: 'ID / Name', required: true, description: 'e.g. name, label, id' },
|
|
23
22
|
{ type: 'lat', label: 'Latitude', required: true, description: 'e.g. lat, latitude, y' },
|
|
24
23
|
{ type: 'lon', label: 'Longitude', required: true, description: 'e.g. lon, lng, longitude, x' }
|
|
25
24
|
],
|
|
26
25
|
optional: [
|
|
26
|
+
{ type: 'id', label: 'Name / ID', required: false, description: 'Optional - auto-generated if not mapped' },
|
|
27
27
|
{ type: 'group', label: 'Group', required: false, description: 'e.g. category, type' },
|
|
28
28
|
{ type: 'sizeFactor', label: 'Size', required: false, description: 'e.g. size, weight' }
|
|
29
29
|
]
|