@smartnet360/svelte-components 0.0.145 → 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/map-v3/features/custom/components/CustomCellSetManager.svelte +10 -0
- package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +48 -4
- package/dist/map-v3/features/custom/logic/csv-parser.js +18 -5
- package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.d.ts +4 -0
- package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.js +12 -0
- package/dist/map-v3/features/custom/types.d.ts +2 -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
|
@@ -494,6 +494,7 @@
|
|
|
494
494
|
{#each setsStore.sets as set (set.id)}
|
|
495
495
|
{@const isExpanded = expandedSets.has(set.id)}
|
|
496
496
|
{@const treeStore = treeStores.get(set.id)}
|
|
497
|
+
{@const hasPoints = set.cells.some(c => c.geometry === 'point')}
|
|
497
498
|
<div class="set-section" class:expanded={isExpanded}>
|
|
498
499
|
<!-- Set Header -->
|
|
499
500
|
<div class="set-header d-flex align-items-center justify-content-between px-2 py-2">
|
|
@@ -512,6 +513,15 @@
|
|
|
512
513
|
>
|
|
513
514
|
<i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
|
|
514
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>
|
|
515
525
|
<div class="set-info text-truncate" onclick={() => toggleExpanded(set.id)} style="cursor: pointer;">
|
|
516
526
|
<div class="fw-medium small text-truncate">{set.name}</div>
|
|
517
527
|
<small class="text-muted">{getSetItemCounts(set)} · {set.groups.length} groups</small>
|
|
@@ -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,7 +232,8 @@
|
|
|
203
232
|
customGroup: item.customGroup,
|
|
204
233
|
sizeFactor: item.sizeFactor,
|
|
205
234
|
setName: set.name,
|
|
206
|
-
popupInfo: { ...item.extraFields }
|
|
235
|
+
popupInfo: { ...item.extraFields },
|
|
236
|
+
labelText
|
|
207
237
|
}
|
|
208
238
|
});
|
|
209
239
|
} else if (item.geometry === 'cell' && item.resolvedCell) {
|
|
@@ -259,9 +289,23 @@
|
|
|
259
289
|
map.setPaintProperty(layers.pointCircle, 'circle-opacity', set.opacity);
|
|
260
290
|
}
|
|
261
291
|
|
|
292
|
+
// Update label visibility
|
|
293
|
+
if (map.getLayer(layers.pointLabel)) {
|
|
294
|
+
map.setLayoutProperty(layers.pointLabel, 'visibility', set.showLabels ? 'visible' : 'none');
|
|
295
|
+
}
|
|
296
|
+
|
|
262
297
|
console.log(`[CustomLayer] Rendered ${cellFeatures.length} cells, ${pointFeatures.length} points for set "${set.name}"`);
|
|
263
298
|
}
|
|
264
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
|
+
|
|
265
309
|
/**
|
|
266
310
|
* Darken a hex color by a factor
|
|
267
311
|
*/
|
|
@@ -138,14 +138,23 @@ export function parseCustomCellsCsv(csvContent, cellLookup, options = {}) {
|
|
|
138
138
|
const groupsSet = new Set();
|
|
139
139
|
let cellCount = 0;
|
|
140
140
|
let pointCount = 0;
|
|
141
|
+
let autoIdCounter = 0;
|
|
141
142
|
for (let i = 1; i < lines.length; i++) {
|
|
142
143
|
const line = lines[i].trim();
|
|
143
144
|
if (!line)
|
|
144
145
|
continue;
|
|
145
146
|
const values = parseCSVLine(line, actualDelimiter);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
}
|
|
149
158
|
// Get custom group (default to 'default')
|
|
150
159
|
const customGroup = groupIndex !== -1
|
|
151
160
|
? (values[groupIndex]?.trim() || 'default')
|
|
@@ -187,8 +196,10 @@ export function parseCustomCellsCsv(csvContent, cellLookup, options = {}) {
|
|
|
187
196
|
lat = parsedLat;
|
|
188
197
|
lon = parsedLon;
|
|
189
198
|
pointCount++;
|
|
199
|
+
// Auto-generate ID for points if not mapped
|
|
200
|
+
const pointId = itemId || `Point ${++autoIdCounter}`;
|
|
190
201
|
cells.push({
|
|
191
|
-
id:
|
|
202
|
+
id: pointId,
|
|
192
203
|
customGroup,
|
|
193
204
|
sizeFactor,
|
|
194
205
|
extraFields,
|
|
@@ -199,7 +210,9 @@ export function parseCustomCellsCsv(csvContent, cellLookup, options = {}) {
|
|
|
199
210
|
continue;
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
|
-
// 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
|
|
203
216
|
resolvedCell = cellLookup.get(itemId);
|
|
204
217
|
if (resolvedCell) {
|
|
205
218
|
cellCount++;
|
|
@@ -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
|
*/
|
|
@@ -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
|
}
|
|
@@ -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
|
]
|