@smartnet360/svelte-components 0.0.125 → 0.0.127
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/Auth/auth.svelte.js +47 -2
- package/dist/map-v3/demo/DemoMap.svelte +36 -0
- package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
- package/dist/map-v3/demo/demo-custom-cells.js +48 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
- package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
- package/dist/map-v3/features/cells/custom/components/index.js +5 -0
- package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
- package/dist/map-v3/features/cells/custom/index.js +35 -0
- package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
- package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
- package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
- package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
- package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
- package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
- package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
- package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
- package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
- package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
- package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
- package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
- package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
- package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
- package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
- package/dist/map-v3/features/cells/custom/types.js +23 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte +203 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte.d.ts +15 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte +261 -0
- package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte.d.ts +10 -0
- package/dist/map-v3/features/sites/custom/index.d.ts +13 -0
- package/dist/map-v3/features/sites/custom/index.js +16 -0
- package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte +201 -0
- package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte.d.ts +8 -0
- package/dist/map-v3/features/sites/custom/logic/csv-parser.d.ts +12 -0
- package/dist/map-v3/features/sites/custom/logic/csv-parser.js +182 -0
- package/dist/map-v3/features/sites/custom/logic/tree-adapter.d.ts +16 -0
- package/dist/map-v3/features/sites/custom/logic/tree-adapter.js +59 -0
- package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.d.ts +78 -0
- package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.js +248 -0
- package/dist/map-v3/features/sites/custom/types.d.ts +74 -0
- package/dist/map-v3/features/sites/custom/types.js +8 -0
- package/dist/map-v3/index.d.ts +2 -0
- package/dist/map-v3/index.js +4 -0
- package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
- package/package.json +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Custom Cell Set Manager
|
|
4
|
+
*
|
|
5
|
+
* Upload CSV files, view import results, and manage custom cell sets.
|
|
6
|
+
* Acts as the main control panel for the custom cells feature.
|
|
7
|
+
* Includes filter controls for each loaded set.
|
|
8
|
+
*/
|
|
9
|
+
import { MapControl } from '../../../../shared';
|
|
10
|
+
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
11
|
+
import type { CustomCellImportResult } from '../types';
|
|
12
|
+
import CustomCellFilterControl from './CustomCellFilterControl.svelte';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** The custom cell sets store */
|
|
16
|
+
setsStore: CustomCellSetsStore;
|
|
17
|
+
/** Control position on map */
|
|
18
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
setsStore,
|
|
23
|
+
position = 'top-left'
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
// Derived for reactivity
|
|
27
|
+
let setsArray = $derived(setsStore.sets);
|
|
28
|
+
|
|
29
|
+
// State
|
|
30
|
+
let showImportModal = $state(false);
|
|
31
|
+
let importResult = $state<CustomCellImportResult | null>(null);
|
|
32
|
+
let importFileName = $state('');
|
|
33
|
+
let importError = $state('');
|
|
34
|
+
let fileInput: HTMLInputElement;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle file selection
|
|
38
|
+
*/
|
|
39
|
+
function handleFileSelect(event: Event) {
|
|
40
|
+
const input = event.target as HTMLInputElement;
|
|
41
|
+
const file = input.files?.[0];
|
|
42
|
+
|
|
43
|
+
if (!file) return;
|
|
44
|
+
|
|
45
|
+
importFileName = file.name.replace(/\.csv$/i, '');
|
|
46
|
+
importError = '';
|
|
47
|
+
|
|
48
|
+
const reader = new FileReader();
|
|
49
|
+
reader.onload = (e) => {
|
|
50
|
+
try {
|
|
51
|
+
const content = e.target?.result as string;
|
|
52
|
+
importResult = setsStore.importFromCsv(content, file.name);
|
|
53
|
+
showImportModal = true;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
importError = err instanceof Error ? err.message : 'Failed to parse CSV';
|
|
56
|
+
importResult = null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
reader.onerror = () => {
|
|
60
|
+
importError = 'Failed to read file';
|
|
61
|
+
importResult = null;
|
|
62
|
+
};
|
|
63
|
+
reader.readAsText(file);
|
|
64
|
+
|
|
65
|
+
// Reset input so same file can be selected again
|
|
66
|
+
input.value = '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Confirm import and create set
|
|
71
|
+
*/
|
|
72
|
+
function confirmImport() {
|
|
73
|
+
if (!importResult) return;
|
|
74
|
+
|
|
75
|
+
setsStore.createSet(importFileName, importResult);
|
|
76
|
+
closeModal();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Close the modal
|
|
81
|
+
*/
|
|
82
|
+
function closeModal() {
|
|
83
|
+
showImportModal = false;
|
|
84
|
+
importResult = null;
|
|
85
|
+
importFileName = '';
|
|
86
|
+
importError = '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Trigger file input click
|
|
91
|
+
*/
|
|
92
|
+
function triggerFileInput() {
|
|
93
|
+
fileInput?.click();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove a set
|
|
98
|
+
*/
|
|
99
|
+
function removeSet(setId: string) {
|
|
100
|
+
if (confirm('Remove this custom cell set?')) {
|
|
101
|
+
setsStore.removeSet(setId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<MapControl {position} title="Custom Cells" icon="plus-circle" controlWidth="300px">
|
|
107
|
+
{#snippet actions()}
|
|
108
|
+
<button
|
|
109
|
+
class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
|
|
110
|
+
title="Import CSV"
|
|
111
|
+
onclick={triggerFileInput}
|
|
112
|
+
>
|
|
113
|
+
<i class="bi bi-upload"></i>
|
|
114
|
+
</button>
|
|
115
|
+
{/snippet}
|
|
116
|
+
|
|
117
|
+
<div class="custom-cell-manager">
|
|
118
|
+
<!-- Hidden file input -->
|
|
119
|
+
<input
|
|
120
|
+
type="file"
|
|
121
|
+
accept=".csv"
|
|
122
|
+
class="d-none"
|
|
123
|
+
bind:this={fileInput}
|
|
124
|
+
onchange={handleFileSelect}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<!-- Error message -->
|
|
128
|
+
{#if importError}
|
|
129
|
+
<div class="alert alert-danger py-1 px-2 mb-2 small">
|
|
130
|
+
<i class="bi bi-exclamation-triangle me-1"></i>
|
|
131
|
+
{importError}
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
<!-- Empty state -->
|
|
136
|
+
{#if setsStore.sets.length === 0}
|
|
137
|
+
<div class="text-center p-3">
|
|
138
|
+
<i class="bi bi-layers fs-1 text-muted"></i>
|
|
139
|
+
<p class="text-muted small mt-2 mb-0">
|
|
140
|
+
No custom cell sets loaded.
|
|
141
|
+
</p>
|
|
142
|
+
<button
|
|
143
|
+
class="btn btn-sm btn-outline-primary mt-2"
|
|
144
|
+
onclick={triggerFileInput}
|
|
145
|
+
>
|
|
146
|
+
<i class="bi bi-upload me-1"></i> Import CSV
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
{:else}
|
|
150
|
+
<!-- List of sets -->
|
|
151
|
+
<ul class="list-group list-group-flush">
|
|
152
|
+
{#each setsStore.sets as set (set.id)}
|
|
153
|
+
<li class="list-group-item d-flex justify-content-between align-items-center py-2 px-2">
|
|
154
|
+
<div class="d-flex align-items-center">
|
|
155
|
+
<button
|
|
156
|
+
class="btn btn-sm p-0 me-2"
|
|
157
|
+
title={set.visible ? 'Hide' : 'Show'}
|
|
158
|
+
onclick={() => setsStore.toggleSetVisibility(set.id)}
|
|
159
|
+
>
|
|
160
|
+
<i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
|
|
161
|
+
</button>
|
|
162
|
+
<div>
|
|
163
|
+
<div class="fw-medium small">{set.name}</div>
|
|
164
|
+
<small class="text-muted">
|
|
165
|
+
{set.cells.length} cells · {set.groups.length} groups
|
|
166
|
+
</small>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<button
|
|
170
|
+
class="btn btn-sm btn-outline-danger border-0 p-1"
|
|
171
|
+
title="Remove"
|
|
172
|
+
onclick={() => removeSet(set.id)}
|
|
173
|
+
>
|
|
174
|
+
<i class="bi bi-trash"></i>
|
|
175
|
+
</button>
|
|
176
|
+
</li>
|
|
177
|
+
{/each}
|
|
178
|
+
</ul>
|
|
179
|
+
{/if}
|
|
180
|
+
</div>
|
|
181
|
+
</MapControl>
|
|
182
|
+
|
|
183
|
+
<!-- Import Result Modal -->
|
|
184
|
+
{#if showImportModal && importResult}
|
|
185
|
+
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
|
|
186
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
187
|
+
<div class="modal-content">
|
|
188
|
+
<div class="modal-header py-2">
|
|
189
|
+
<h6 class="modal-title">
|
|
190
|
+
<i class="bi bi-file-earmark-spreadsheet me-2"></i>
|
|
191
|
+
Import: {importFileName}
|
|
192
|
+
</h6>
|
|
193
|
+
<button type="button" class="btn-close" onclick={closeModal}></button>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="modal-body">
|
|
196
|
+
<!-- Import Summary -->
|
|
197
|
+
<div class="mb-3">
|
|
198
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
199
|
+
<span class="text-success">
|
|
200
|
+
<i class="bi bi-check-circle me-1"></i>
|
|
201
|
+
{importResult.cells.length} cells matched
|
|
202
|
+
</span>
|
|
203
|
+
<span class="badge bg-success">{importResult.totalRows} rows</span>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{#if importResult.unmatchedTxIds.length > 0}
|
|
207
|
+
<div class="text-warning small mb-2">
|
|
208
|
+
<i class="bi bi-exclamation-triangle me-1"></i>
|
|
209
|
+
{importResult.unmatchedTxIds.length} cells not found
|
|
210
|
+
<button
|
|
211
|
+
class="btn btn-link btn-sm p-0 ms-1"
|
|
212
|
+
type="button"
|
|
213
|
+
data-bs-toggle="collapse"
|
|
214
|
+
data-bs-target="#unmatchedList"
|
|
215
|
+
>
|
|
216
|
+
(show)
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="collapse" id="unmatchedList">
|
|
220
|
+
<div class="small bg-light p-2 rounded" style="max-height: 100px; overflow-y: auto;">
|
|
221
|
+
{importResult.unmatchedTxIds.slice(0, 20).join(', ')}
|
|
222
|
+
{#if importResult.unmatchedTxIds.length > 20}
|
|
223
|
+
<span class="text-muted">... and {importResult.unmatchedTxIds.length - 20} more</span>
|
|
224
|
+
{/if}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<!-- Groups found -->
|
|
231
|
+
<div class="mb-3">
|
|
232
|
+
<label class="form-label small fw-medium">Groups Found ({importResult.groups.length})</label>
|
|
233
|
+
<div class="d-flex flex-wrap gap-1">
|
|
234
|
+
{#each importResult.groups as group}
|
|
235
|
+
<span class="badge bg-secondary">{group}</span>
|
|
236
|
+
{/each}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Extra columns -->
|
|
241
|
+
{#if importResult.extraColumns.length > 0}
|
|
242
|
+
<div class="mb-3">
|
|
243
|
+
<label class="form-label small fw-medium">Extra Columns (for tooltips)</label>
|
|
244
|
+
<div class="d-flex flex-wrap gap-1">
|
|
245
|
+
{#each importResult.extraColumns as col}
|
|
246
|
+
<span class="badge bg-info">{col}</span>
|
|
247
|
+
{/each}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
{/if}
|
|
251
|
+
|
|
252
|
+
<!-- Set Name -->
|
|
253
|
+
<div class="mb-2">
|
|
254
|
+
<label for="setName" class="form-label small fw-medium">Set Name</label>
|
|
255
|
+
<input
|
|
256
|
+
type="text"
|
|
257
|
+
class="form-control form-control-sm"
|
|
258
|
+
id="setName"
|
|
259
|
+
bind:value={importFileName}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="modal-footer py-2">
|
|
264
|
+
<button type="button" class="btn btn-secondary btn-sm" onclick={closeModal}>
|
|
265
|
+
Cancel
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="btn btn-primary btn-sm"
|
|
270
|
+
onclick={confirmImport}
|
|
271
|
+
disabled={importResult.cells.length === 0}
|
|
272
|
+
>
|
|
273
|
+
<i class="bi bi-check-lg me-1"></i>
|
|
274
|
+
Import {importResult.cells.length} cells
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
281
|
+
|
|
282
|
+
<style>
|
|
283
|
+
.custom-cell-manager {
|
|
284
|
+
font-size: 0.875rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.list-group-item {
|
|
288
|
+
background: transparent;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.modal {
|
|
292
|
+
z-index: 2000;
|
|
293
|
+
}
|
|
294
|
+
</style>
|
|
295
|
+
|
|
296
|
+
<!-- Filter Controls for each set (rendered outside the manager control) -->
|
|
297
|
+
{#each setsArray as set (set.id)}
|
|
298
|
+
{#key set.id}
|
|
299
|
+
<CustomCellFilterControl
|
|
300
|
+
{position}
|
|
301
|
+
{setsStore}
|
|
302
|
+
{set}
|
|
303
|
+
onremove={(id) => setsStore.removeSet(id)}
|
|
304
|
+
/>
|
|
305
|
+
{/key}
|
|
306
|
+
{/each}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The custom cell sets store */
|
|
4
|
+
setsStore: CustomCellSetsStore;
|
|
5
|
+
/** Control position on map */
|
|
6
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
|
+
}
|
|
8
|
+
declare const CustomCellSetManager: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type CustomCellSetManager = ReturnType<typeof CustomCellSetManager>;
|
|
10
|
+
export default CustomCellSetManager;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Cells Feature - Main Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Ad-hoc cell layers loaded from CSV files.
|
|
5
|
+
* Reference existing cells by txId with custom grouping and sizing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import {
|
|
11
|
+
* CustomCellsLayer,
|
|
12
|
+
* CustomCellSetManager,
|
|
13
|
+
* CustomCellFilterControl,
|
|
14
|
+
* createCustomCellSetsStore
|
|
15
|
+
* } from './';
|
|
16
|
+
*
|
|
17
|
+
* const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
|
|
18
|
+
* </script>
|
|
19
|
+
*
|
|
20
|
+
* <CustomCellSetManager setsStore={customSetsStore} position="top-left" />
|
|
21
|
+
* <CustomCellsLayer setsStore={customSetsStore} />
|
|
22
|
+
* {#each customSetsStore.sets as set (set.id)}
|
|
23
|
+
* <CustomCellFilterControl setsStore={customSetsStore} {set} position="top-left" />
|
|
24
|
+
* {/each}
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export type { CustomCell, CustomCellSet, CustomCellSetConfig, CustomCellImportResult } from './types';
|
|
28
|
+
export { CUSTOM_CELL_PALETTE } from './types';
|
|
29
|
+
export { CustomCellSetsStore, createCustomCellSetsStore } from './stores';
|
|
30
|
+
export { CustomCellFilterControl, CustomCellSetManager } from './components';
|
|
31
|
+
export { CustomCellsLayer } from './layers';
|
|
32
|
+
export { parseCustomCellsCsv, buildCellLookup, buildCustomCellTree, getGroupCounts } from './logic';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Cells Feature - Main Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Ad-hoc cell layers loaded from CSV files.
|
|
5
|
+
* Reference existing cells by txId with custom grouping and sizing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import {
|
|
11
|
+
* CustomCellsLayer,
|
|
12
|
+
* CustomCellSetManager,
|
|
13
|
+
* CustomCellFilterControl,
|
|
14
|
+
* createCustomCellSetsStore
|
|
15
|
+
* } from './';
|
|
16
|
+
*
|
|
17
|
+
* const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
|
|
18
|
+
* </script>
|
|
19
|
+
*
|
|
20
|
+
* <CustomCellSetManager setsStore={customSetsStore} position="top-left" />
|
|
21
|
+
* <CustomCellsLayer setsStore={customSetsStore} />
|
|
22
|
+
* {#each customSetsStore.sets as set (set.id)}
|
|
23
|
+
* <CustomCellFilterControl setsStore={customSetsStore} {set} position="top-left" />
|
|
24
|
+
* {/each}
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export { CUSTOM_CELL_PALETTE } from './types';
|
|
28
|
+
// Stores
|
|
29
|
+
export { CustomCellSetsStore, createCustomCellSetsStore } from './stores';
|
|
30
|
+
// Components
|
|
31
|
+
export { CustomCellFilterControl, CustomCellSetManager } from './components';
|
|
32
|
+
// Layers
|
|
33
|
+
export { CustomCellsLayer } from './layers';
|
|
34
|
+
// Logic (for advanced usage)
|
|
35
|
+
export { parseCustomCellsCsv, buildCellLookup, buildCustomCellTree, getGroupCounts } from './logic';
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Custom Cells Layer
|
|
4
|
+
*
|
|
5
|
+
* Renders custom cell sets on the map with sizeFactor support.
|
|
6
|
+
* Each set is rendered as a separate layer for independent styling.
|
|
7
|
+
*/
|
|
8
|
+
import { getContext, onMount, onDestroy } from 'svelte';
|
|
9
|
+
import type { MapStore } from '../../../../core/stores/map.store.svelte';
|
|
10
|
+
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
11
|
+
import type { CustomCellSet, CustomCell } from '../types';
|
|
12
|
+
import { generateCellArc, calculateRadiusInMeters } from '../../logic/geometry';
|
|
13
|
+
import type mapboxgl from 'mapbox-gl';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** The custom cell sets store */
|
|
17
|
+
setsStore: CustomCellSetsStore;
|
|
18
|
+
/** Optional: specific set ID to render (if not provided, renders all) */
|
|
19
|
+
setId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { setsStore, setId }: Props = $props();
|
|
23
|
+
|
|
24
|
+
const mapStore = getContext<MapStore>('MAP_CONTEXT');
|
|
25
|
+
|
|
26
|
+
// Track active layer/source IDs for cleanup
|
|
27
|
+
let activeSources = new Set<string>();
|
|
28
|
+
let activeLayers = new Set<string>();
|
|
29
|
+
|
|
30
|
+
// Debounce timer
|
|
31
|
+
let updateTimeout: ReturnType<typeof setTimeout>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get source ID for a set
|
|
35
|
+
*/
|
|
36
|
+
function getSourceId(setId: string): string {
|
|
37
|
+
return `custom-cells-${setId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get layer IDs for a set
|
|
42
|
+
*/
|
|
43
|
+
function getLayerIds(setId: string): { fill: string; line: string } {
|
|
44
|
+
return {
|
|
45
|
+
fill: `custom-cells-fill-${setId}`,
|
|
46
|
+
line: `custom-cells-line-${setId}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add source and layers for a set
|
|
52
|
+
*/
|
|
53
|
+
function addSetLayers(map: mapboxgl.Map, set: CustomCellSet) {
|
|
54
|
+
const sourceId = getSourceId(set.id);
|
|
55
|
+
const { fill, line } = getLayerIds(set.id);
|
|
56
|
+
|
|
57
|
+
// Add source if not exists
|
|
58
|
+
if (!map.getSource(sourceId)) {
|
|
59
|
+
map.addSource(sourceId, {
|
|
60
|
+
type: 'geojson',
|
|
61
|
+
data: { type: 'FeatureCollection', features: [] }
|
|
62
|
+
});
|
|
63
|
+
activeSources.add(sourceId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Add fill layer
|
|
67
|
+
if (!map.getLayer(fill)) {
|
|
68
|
+
map.addLayer({
|
|
69
|
+
id: fill,
|
|
70
|
+
type: 'fill',
|
|
71
|
+
source: sourceId,
|
|
72
|
+
paint: {
|
|
73
|
+
'fill-color': ['get', 'color'],
|
|
74
|
+
'fill-opacity': set.opacity
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
activeLayers.add(fill);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Add line layer
|
|
81
|
+
if (!map.getLayer(line)) {
|
|
82
|
+
map.addLayer({
|
|
83
|
+
id: line,
|
|
84
|
+
type: 'line',
|
|
85
|
+
source: sourceId,
|
|
86
|
+
paint: {
|
|
87
|
+
'line-color': ['get', 'lineColor'],
|
|
88
|
+
'line-width': ['get', 'lineWidth'],
|
|
89
|
+
'line-opacity': ['get', 'lineOpacity']
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
activeLayers.add(line);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove layers and source for a set
|
|
98
|
+
*/
|
|
99
|
+
function removeSetLayers(map: mapboxgl.Map, setIdToRemove: string) {
|
|
100
|
+
const sourceId = getSourceId(setIdToRemove);
|
|
101
|
+
const { fill, line } = getLayerIds(setIdToRemove);
|
|
102
|
+
|
|
103
|
+
if (map.getLayer(line)) {
|
|
104
|
+
map.removeLayer(line);
|
|
105
|
+
activeLayers.delete(line);
|
|
106
|
+
}
|
|
107
|
+
if (map.getLayer(fill)) {
|
|
108
|
+
map.removeLayer(fill);
|
|
109
|
+
activeLayers.delete(fill);
|
|
110
|
+
}
|
|
111
|
+
if (map.getSource(sourceId)) {
|
|
112
|
+
map.removeSource(sourceId);
|
|
113
|
+
activeSources.delete(sourceId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Render cells for a specific set
|
|
119
|
+
*/
|
|
120
|
+
function renderSet(map: mapboxgl.Map, set: CustomCellSet) {
|
|
121
|
+
const bounds = map.getBounds();
|
|
122
|
+
if (!bounds) return;
|
|
123
|
+
|
|
124
|
+
const zoom = map.getZoom();
|
|
125
|
+
const centerLat = map.getCenter().lat;
|
|
126
|
+
|
|
127
|
+
// Calculate base radius from pixel size
|
|
128
|
+
const baseRadiusMeters = calculateRadiusInMeters(centerLat, zoom, set.baseSize);
|
|
129
|
+
|
|
130
|
+
const features: GeoJSON.Feature[] = [];
|
|
131
|
+
|
|
132
|
+
for (const customCell of set.cells) {
|
|
133
|
+
const cell = customCell.resolvedCell;
|
|
134
|
+
if (!cell) continue;
|
|
135
|
+
|
|
136
|
+
// Check group visibility
|
|
137
|
+
if (!set.visibleGroups.has(customCell.customGroup)) continue;
|
|
138
|
+
|
|
139
|
+
// Viewport filter
|
|
140
|
+
if (!bounds.contains([cell.longitude, cell.latitude])) continue;
|
|
141
|
+
|
|
142
|
+
// Get color for this group
|
|
143
|
+
const color = set.groupColors[customCell.customGroup] || set.defaultColor;
|
|
144
|
+
|
|
145
|
+
// Apply size factor
|
|
146
|
+
const radiusMeters = baseRadiusMeters * customCell.sizeFactor;
|
|
147
|
+
|
|
148
|
+
// Generate arc feature
|
|
149
|
+
const feature = generateCellArc(cell, radiusMeters, 50, color);
|
|
150
|
+
|
|
151
|
+
// Add custom properties for tooltips
|
|
152
|
+
if (feature.properties) {
|
|
153
|
+
feature.properties.customGroup = customCell.customGroup;
|
|
154
|
+
feature.properties.sizeFactor = customCell.sizeFactor;
|
|
155
|
+
feature.properties.setName = set.name;
|
|
156
|
+
|
|
157
|
+
// Add extra fields
|
|
158
|
+
for (const [key, value] of Object.entries(customCell.extraFields)) {
|
|
159
|
+
feature.properties[`extra_${key}`] = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
features.push(feature);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Update source
|
|
167
|
+
const sourceId = getSourceId(set.id);
|
|
168
|
+
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
169
|
+
if (source) {
|
|
170
|
+
source.setData({
|
|
171
|
+
type: 'FeatureCollection',
|
|
172
|
+
features: features as any
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Update layer opacity
|
|
177
|
+
const { fill } = getLayerIds(set.id);
|
|
178
|
+
if (map.getLayer(fill)) {
|
|
179
|
+
map.setPaintProperty(fill, 'fill-opacity', set.opacity);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`[CustomCellsLayer] Rendered ${features.length} features for set "${set.name}"`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Main update function
|
|
187
|
+
*/
|
|
188
|
+
function updateLayers() {
|
|
189
|
+
const map = mapStore.map;
|
|
190
|
+
if (!map) return;
|
|
191
|
+
|
|
192
|
+
clearTimeout(updateTimeout);
|
|
193
|
+
updateTimeout = setTimeout(() => {
|
|
194
|
+
const setsToRender = setId
|
|
195
|
+
? setsStore.sets.filter(s => s.id === setId)
|
|
196
|
+
: setsStore.sets;
|
|
197
|
+
|
|
198
|
+
// Track which sets we're rendering
|
|
199
|
+
const activeSetIds = new Set(setsToRender.map(s => s.id));
|
|
200
|
+
|
|
201
|
+
// Remove layers for sets that no longer exist
|
|
202
|
+
for (const sourceId of activeSources) {
|
|
203
|
+
const setIdFromSource = sourceId.replace('custom-cells-', '');
|
|
204
|
+
if (!activeSetIds.has(setIdFromSource)) {
|
|
205
|
+
removeSetLayers(map, setIdFromSource);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Render each set
|
|
210
|
+
for (const set of setsToRender) {
|
|
211
|
+
if (set.visible) {
|
|
212
|
+
addSetLayers(map, set);
|
|
213
|
+
renderSet(map, set);
|
|
214
|
+
} else {
|
|
215
|
+
// Hide by clearing data
|
|
216
|
+
const sourceId = getSourceId(set.id);
|
|
217
|
+
const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
218
|
+
if (source) {
|
|
219
|
+
source.setData({ type: 'FeatureCollection', features: [] });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}, 100);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Setup and reactive updates
|
|
227
|
+
$effect(() => {
|
|
228
|
+
const map = mapStore.map;
|
|
229
|
+
if (!map) return;
|
|
230
|
+
|
|
231
|
+
const onMapEvent = () => updateLayers();
|
|
232
|
+
|
|
233
|
+
// Initial render
|
|
234
|
+
updateLayers();
|
|
235
|
+
|
|
236
|
+
// Listen to map events
|
|
237
|
+
map.on('moveend', onMapEvent);
|
|
238
|
+
map.on('zoomend', onMapEvent);
|
|
239
|
+
map.on('style.load', onMapEvent);
|
|
240
|
+
|
|
241
|
+
return () => {
|
|
242
|
+
map.off('moveend', onMapEvent);
|
|
243
|
+
map.off('zoomend', onMapEvent);
|
|
244
|
+
map.off('style.load', onMapEvent);
|
|
245
|
+
|
|
246
|
+
// Cleanup all layers
|
|
247
|
+
for (const sourceId of activeSources) {
|
|
248
|
+
const setIdFromSource = sourceId.replace('custom-cells-', '');
|
|
249
|
+
removeSetLayers(map, setIdFromSource);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// React to store changes
|
|
255
|
+
$effect(() => {
|
|
256
|
+
// Read version to trigger on changes
|
|
257
|
+
const _version = setsStore.version;
|
|
258
|
+
const _sets = setsStore.sets;
|
|
259
|
+
|
|
260
|
+
updateLayers();
|
|
261
|
+
});
|
|
262
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The custom cell sets store */
|
|
4
|
+
setsStore: CustomCellSetsStore;
|
|
5
|
+
/** Optional: specific set ID to render (if not provided, renders all) */
|
|
6
|
+
setId?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const CustomCellsLayer: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type CustomCellsLayer = ReturnType<typeof CustomCellsLayer>;
|
|
10
|
+
export default CustomCellsLayer;
|