poe-svelte-ui-lib 1.11.2 → 1.12.1
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/Map.svelte +181 -86
- package/dist/Map/Map.svelte.d.ts +1 -1
- package/dist/Map/MapProps.svelte +35 -2
- package/dist/Map/MapProps.svelte.d.ts +2 -2
- package/dist/Map/README.md +8 -6
- package/dist/Map/mapWrapper/Line.svelte +59 -0
- package/dist/Map/mapWrapper/Line.svelte.d.ts +10 -0
- package/dist/Map/mapWrapper/Popup.svelte +3 -1
- package/dist/Map/mapWrapper/Popup.svelte.d.ts +1 -0
- package/dist/Map/mapWrapper/index.d.ts +1 -0
- package/dist/Map/mapWrapper/index.js +1 -0
- package/dist/locales/translations.js +2 -0
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
package/dist/Map/Map.svelte
CHANGED
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
import { onDestroy, onMount } from "svelte"
|
|
5
5
|
import { fade } from "svelte/transition"
|
|
6
6
|
import { twMerge } from "tailwind-merge"
|
|
7
|
-
import { MapLibre, NavigationControl, ScaleControl, GeolocateControl, FullScreenControl, Marker, Popup, CustomControl } from "./mapWrapper"
|
|
7
|
+
import { MapLibre, NavigationControl, ScaleControl, GeolocateControl, FullScreenControl, Marker, Popup, Line, CustomControl } from "./mapWrapper"
|
|
8
8
|
|
|
9
|
-
let {
|
|
9
|
+
let {
|
|
10
|
+
id = crypto.randomUUID(),
|
|
11
|
+
label = { name: "", class: "" },
|
|
12
|
+
data = $bindable(),
|
|
13
|
+
markerIcon,
|
|
14
|
+
trackEnabled = $bindable(false),
|
|
15
|
+
trackLength = $bindable(1000),
|
|
16
|
+
}: IMapProps = $props()
|
|
10
17
|
|
|
11
18
|
interface MapDevice extends IDeviceGNSS {
|
|
12
19
|
isFresh: boolean
|
|
13
|
-
|
|
20
|
+
staleTimeoutId: number | null
|
|
21
|
+
removeTimeoutId: number | null
|
|
22
|
+
track: [number, number][]
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
let devices: MapDevice[] = $state([])
|
|
@@ -18,45 +27,66 @@
|
|
|
18
27
|
let isDarkMode = $state(false)
|
|
19
28
|
let markerTimeout = $state(30_000)
|
|
20
29
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const clearDeviceTimers = (device: MapDevice) => {
|
|
31
|
+
if (device.staleTimeoutId !== null) clearTimeout(device.staleTimeoutId)
|
|
32
|
+
if (device.removeTimeoutId !== null) clearTimeout(device.removeTimeoutId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Перезапускает цикл "устройство свежее" -> (без обновлений markerTimeout) "устарело" -> (без обновлений ещё markerTimeout) "удалено"
|
|
36
|
+
const restartFreshTimer = (devSN: string) => {
|
|
37
|
+
const idx = devices.findIndex((d) => d.DevSN === devSN)
|
|
38
|
+
if (idx === -1) return
|
|
39
|
+
|
|
40
|
+
clearDeviceTimers(devices[idx])
|
|
41
|
+
devices[idx].isFresh = true
|
|
42
|
+
|
|
43
|
+
devices[idx].staleTimeoutId = setTimeout(() => {
|
|
44
|
+
const staleIdx = devices.findIndex((d) => d.DevSN === devSN)
|
|
45
|
+
if (staleIdx === -1) return
|
|
46
|
+
|
|
47
|
+
devices[staleIdx].isFresh = false
|
|
48
|
+
devices[staleIdx].staleTimeoutId = null
|
|
49
|
+
devices[staleIdx].removeTimeoutId = setTimeout(() => {
|
|
50
|
+
devices = devices.filter((d) => d.DevSN !== devSN)
|
|
51
|
+
}, markerTimeout) as unknown as number
|
|
33
52
|
}, markerTimeout) as unknown as number
|
|
34
|
-
devices[index].timeoutId = id
|
|
35
|
-
devices[index].isFresh = true
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
// Обработка входящих данных
|
|
39
56
|
$effect(() => {
|
|
40
57
|
if (data) {
|
|
58
|
+
/* (0, 0) — признак отсутствия реального GPS-фикса, в траекторию такие точки не добавляем */
|
|
59
|
+
const hasFix = data.NavLat !== 0 || data.NavLon !== 0
|
|
41
60
|
const idx = devices.findIndex((d) => d.DevSN === data?.DevSN)
|
|
42
61
|
if (idx !== -1) {
|
|
43
62
|
/* Обновление существующего */
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
const track = hasFix ? [...devices[idx].track, [data.NavLon, data.NavLat] as [number, number]].slice(-trackLength) : devices[idx].track
|
|
64
|
+
devices[idx] = { ...devices[idx], ...data, track }
|
|
46
65
|
} else {
|
|
47
66
|
/* Новое устройство */
|
|
48
67
|
const newDevice: MapDevice = {
|
|
49
68
|
...data,
|
|
50
69
|
isFresh: true,
|
|
51
|
-
|
|
70
|
+
staleTimeoutId: null,
|
|
71
|
+
removeTimeoutId: null,
|
|
72
|
+
track: hasFix ? [[data.NavLon, data.NavLat]] : [],
|
|
52
73
|
}
|
|
53
74
|
devices.push(newDevice)
|
|
54
|
-
restartFreshTimer(devices.length - 1)
|
|
55
75
|
}
|
|
76
|
+
restartFreshTimer(data.DevSN)
|
|
56
77
|
data = null
|
|
57
78
|
}
|
|
58
79
|
})
|
|
59
80
|
|
|
81
|
+
// Подрезает уже накопленные траектории при уменьшении trackLength
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (trackLength > 0) {
|
|
84
|
+
devices.forEach((device) => {
|
|
85
|
+
if (device.track.length > trackLength) device.track = device.track.slice(-trackLength)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
60
90
|
const handleThemeChange = (event: CustomEvent) => {
|
|
61
91
|
isDarkMode = !event.detail.currentTheme
|
|
62
92
|
}
|
|
@@ -69,15 +99,22 @@
|
|
|
69
99
|
})
|
|
70
100
|
|
|
71
101
|
onDestroy(() => {
|
|
72
|
-
if (typeof window !== "undefined") window.
|
|
102
|
+
if (typeof window !== "undefined") window.removeEventListener("ThemeChange", handleThemeChange as EventListener)
|
|
73
103
|
|
|
74
104
|
for (const device of devices) {
|
|
75
|
-
|
|
76
|
-
clearTimeout(device.timeoutId)
|
|
77
|
-
}
|
|
105
|
+
clearDeviceTimers(device)
|
|
78
106
|
}
|
|
79
107
|
})
|
|
80
108
|
|
|
109
|
+
const trackLengthOptions: { label: string; value: number }[] = [
|
|
110
|
+
{ label: "100", value: 100 },
|
|
111
|
+
{ label: "500", value: 500 },
|
|
112
|
+
{ label: "1000", value: 1000 },
|
|
113
|
+
{ label: "5000", value: 5000 },
|
|
114
|
+
{ label: "10000", value: 10000 },
|
|
115
|
+
{ label: "50000", value: 50000 },
|
|
116
|
+
]
|
|
117
|
+
|
|
81
118
|
const timeoutOptions: { label: string; value: number }[] = [
|
|
82
119
|
{ label: "30 sec", value: 30_000 },
|
|
83
120
|
{ label: "1 min", value: 60_000 },
|
|
@@ -90,7 +127,7 @@
|
|
|
90
127
|
|
|
91
128
|
const changeTimeout = (val: number) => {
|
|
92
129
|
markerTimeout = val
|
|
93
|
-
devices.forEach((
|
|
130
|
+
devices.forEach((device) => restartFreshTimer(device.DevSN))
|
|
94
131
|
}
|
|
95
132
|
</script>
|
|
96
133
|
|
|
@@ -110,21 +147,47 @@
|
|
|
110
147
|
<FullScreenControl />
|
|
111
148
|
|
|
112
149
|
<CustomControl position="top-left">
|
|
113
|
-
<div class="flex items-center gap-
|
|
114
|
-
<label
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
<div class="flex flex-wrap items-center gap-3 px-2 py-1 text-black">
|
|
151
|
+
<label class="flex items-center gap-1.5 text-sm font-medium">
|
|
152
|
+
<input type="checkbox" bind:checked={trackEnabled} />
|
|
153
|
+
{$T("constructor.props.map.track")}
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
{#if trackEnabled}
|
|
157
|
+
<div class="flex items-center gap-2">
|
|
158
|
+
<label for="trackLength" class="text-sm font-medium">{$T("constructor.props.map.trackLength")}</label>
|
|
159
|
+
<select id="trackLength" class="rounded px-2 py-1 text-sm" bind:value={trackLength}>
|
|
160
|
+
{#each trackLengthOptions as opt}
|
|
161
|
+
<option value={opt.value}>{opt.label}</option>
|
|
162
|
+
{/each}
|
|
163
|
+
</select>
|
|
164
|
+
</div>
|
|
165
|
+
{/if}
|
|
166
|
+
|
|
167
|
+
<div class="flex items-center gap-2">
|
|
168
|
+
<label for="timeout" class="text-sm font-medium">{$T("constructor.props.map.timeout")}</label>
|
|
169
|
+
<select
|
|
170
|
+
id="timeout"
|
|
171
|
+
class="rounded px-2 py-1 text-sm"
|
|
172
|
+
bind:value={markerTimeout}
|
|
173
|
+
onchange={(e) => changeTimeout(parseInt((e.target as HTMLSelectElement).value))}
|
|
174
|
+
>
|
|
175
|
+
{#each timeoutOptions as opt}
|
|
176
|
+
<option value={opt.value}>{opt.label}</option>
|
|
177
|
+
{/each}
|
|
178
|
+
</select>
|
|
179
|
+
</div>
|
|
125
180
|
</div>
|
|
126
181
|
</CustomControl>
|
|
127
182
|
|
|
183
|
+
{#if trackEnabled}
|
|
184
|
+
{#each devices as device (device.DevSN)}
|
|
185
|
+
{#if device.track.length >= 2}
|
|
186
|
+
<Line id={`track-${device.DevSN}`} coordinates={device.track} color={device.isFresh ? "#22c55e" : "#ef4444"} />
|
|
187
|
+
{/if}
|
|
188
|
+
{/each}
|
|
189
|
+
{/if}
|
|
190
|
+
|
|
128
191
|
{#each devices as device}
|
|
129
192
|
<Marker lnglat={{ lng: device.NavLon, lat: device.NavLat }}>
|
|
130
193
|
{#snippet content()}
|
|
@@ -159,56 +222,88 @@
|
|
|
159
222
|
<p class="font-bold">{device.DevName}</p>
|
|
160
223
|
</div>
|
|
161
224
|
{/snippet}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3" />
|
|
187
|
-
</g>
|
|
188
|
-
</svg>
|
|
189
|
-
</button>
|
|
190
|
-
{#if isCopied}
|
|
191
|
-
<div
|
|
192
|
-
class="absolute top-1/2 right-0 -translate-y-1/2 transform rounded-md bg-(--green-color) px-1.5 py-1 text-sm shadow-lg"
|
|
193
|
-
transition:fade={{ duration: 200 }}
|
|
194
|
-
>
|
|
195
|
-
✓
|
|
225
|
+
|
|
226
|
+
<Popup closeButton={false} class="map-popup rounded-2xl text-left">
|
|
227
|
+
<div class="flex flex-col gap-1.5">
|
|
228
|
+
<div class="flex items-center gap-1.5">
|
|
229
|
+
<span class="size-2 shrink-0 rounded-full {device.isFresh ? 'bg-green-500' : 'bg-red-500'}"></span>
|
|
230
|
+
<h6 class="text-sm leading-none font-bold">{device.DevName}</h6>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
|
|
234
|
+
<div>
|
|
235
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
|
|
236
|
+
<p class="font-medium">{device.NavLat.toFixed(5)}, {device.NavLon.toFixed(5)}</p>
|
|
237
|
+
</div>
|
|
238
|
+
<div>
|
|
239
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Alt</p>
|
|
240
|
+
<p class="font-medium">{device.NavAlt.toFixed(1)} m</p>
|
|
241
|
+
</div>
|
|
242
|
+
<div>
|
|
243
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Heading</p>
|
|
244
|
+
<p class="font-medium">{device.NavHeading.toFixed(1)}°</p>
|
|
245
|
+
</div>
|
|
246
|
+
<div>
|
|
247
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Satellites</p>
|
|
248
|
+
<p class="font-medium">{device.NavSatUse}</p>
|
|
196
249
|
</div>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<p class="text-[0.6rem] font-mono text-center tracking-wide uppercase">{device.DevSN}</p>
|
|
253
|
+
|
|
254
|
+
<div class="flex items-center justify-between border-t border-(--border-color) pt-1">
|
|
255
|
+
<div class="relative flex items-center">
|
|
256
|
+
<button
|
|
257
|
+
class="flex cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-1 py-0.5 opacity-70 transition hover:opacity-100"
|
|
258
|
+
onclick={(e) => {
|
|
259
|
+
e.preventDefault()
|
|
260
|
+
navigator.clipboard.writeText(
|
|
261
|
+
`DevName: ${device.DevName}\nDevSN: ${device.DevSN}\nLat: ${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt}\nHeading: ${device.NavHeading} | Satellites: ${device.NavSatUse}`,
|
|
262
|
+
)
|
|
263
|
+
isCopied = true
|
|
264
|
+
setTimeout(() => (isCopied = false), 1000)
|
|
265
|
+
}}
|
|
266
|
+
aria-label="Copy"
|
|
267
|
+
title="Copy"
|
|
268
|
+
>
|
|
269
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24">
|
|
270
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5">
|
|
271
|
+
<path
|
|
272
|
+
d="M6 11c0-2.828 0-4.243.879-5.121C7.757 5 9.172 5 12 5h3c2.828 0 4.243 0 5.121.879C21 6.757 21 8.172 21 11v5c0 2.828 0 4.243-.879 5.121C19.243 22 17.828 22 15 22h-3c-2.828 0-4.243 0-5.121-.879C6 20.243 6 18.828 6 16z"
|
|
273
|
+
/>
|
|
274
|
+
<path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3" />
|
|
275
|
+
</g>
|
|
276
|
+
</svg>
|
|
277
|
+
</button>
|
|
278
|
+
{#if isCopied}
|
|
279
|
+
<div
|
|
280
|
+
class="absolute -top-1 right-0 -translate-y-full rounded-md bg-(--green-color) px-1.5 py-1 text-sm shadow-lg"
|
|
281
|
+
transition:fade={{ duration: 200 }}
|
|
282
|
+
>
|
|
283
|
+
✓
|
|
284
|
+
</div>
|
|
285
|
+
{/if}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<button
|
|
289
|
+
class="cursor-pointer rounded-lg border-none bg-transparent p-0.5 text-red-500 transition hover:text-red-600"
|
|
290
|
+
aria-label="Delete"
|
|
291
|
+
title="Delete"
|
|
292
|
+
onclick={() => (devices = devices.filter((dev) => dev.DevSN !== device.DevSN))}
|
|
210
293
|
>
|
|
211
|
-
|
|
294
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24"
|
|
295
|
+
><path
|
|
296
|
+
fill="none"
|
|
297
|
+
stroke="currentColor"
|
|
298
|
+
stroke-linecap="round"
|
|
299
|
+
stroke-linejoin="round"
|
|
300
|
+
stroke-width="1.5"
|
|
301
|
+
d="m19.5 5.5l-.62 10.025c-.158 2.561-.237 3.842-.88 4.763a4 4 0 0 1-1.2 1.128c-.957.584-2.24.584-4.806.584c-2.57 0-3.855 0-4.814-.585a4 4 0 0 1-1.2-1.13c-.642-.922-.72-2.205-.874-4.77L4.5 5.5M3 5.5h18m-4.944 0l-.683-1.408c-.453-.936-.68-1.403-1.071-1.695a2 2 0 0 0-.275-.172C13.594 2 13.074 2 12.035 2c-1.066 0-1.599 0-2.04.234a2 2 0 0 0-.278.18c-.395.303-.616.788-1.058 1.757L8.053 5.5m1.447 11v-6m5 6v-6"
|
|
302
|
+
color="currentColor"
|
|
303
|
+
/></svg
|
|
304
|
+
>
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
212
307
|
</div>
|
|
213
308
|
</Popup>
|
|
214
309
|
</Marker>
|
package/dist/Map/Map.svelte.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { IMapProps } from "../types";
|
|
2
|
-
declare const Map: import("svelte").Component<IMapProps, {}, "data">;
|
|
2
|
+
declare const Map: import("svelte").Component<IMapProps, {}, "data" | "trackEnabled" | "trackLength">;
|
|
3
3
|
type Map = ReturnType<typeof Map>;
|
|
4
4
|
export default Map;
|
package/dist/Map/MapProps.svelte
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { T } from "../locales/i18n"
|
|
3
|
-
import { type UIComponent, type
|
|
3
|
+
import { type UIComponent, type IMapProps, updateProperty, type IUIComponentHandler } from "../types"
|
|
4
4
|
import { ICONS } from "../icons"
|
|
5
5
|
import { optionsStore } from "../options"
|
|
6
6
|
import { getContext } from "svelte"
|
|
7
7
|
import CommonSnippets from "../CommonSnippets.svelte"
|
|
8
|
+
import { Switch, Select } from ".."
|
|
9
|
+
|
|
10
|
+
const TRACK_LENGTH_OPTIONS = [100, 500, 1000, 5000, 10000, 50000].map((value) => ({ id: crypto.randomUUID(), value, name: String(value) }))
|
|
8
11
|
|
|
9
12
|
const {
|
|
10
13
|
component,
|
|
11
14
|
onPropertyChange,
|
|
12
15
|
forConstructor = true,
|
|
13
16
|
} = $props<{
|
|
14
|
-
component: UIComponent & { properties: Partial<
|
|
17
|
+
component: UIComponent & { properties: Partial<IMapProps> }
|
|
15
18
|
onPropertyChange: (updates: Partial<{ properties?: string | object; name?: string; access?: string; eventHandler?: IUIComponentHandler }>) => void
|
|
16
19
|
forConstructor?: boolean
|
|
17
20
|
}>()
|
|
@@ -48,6 +51,21 @@
|
|
|
48
51
|
{component}
|
|
49
52
|
{onPropertyChange}
|
|
50
53
|
/>
|
|
54
|
+
<Switch
|
|
55
|
+
label={{ name: $T("constructor.props.map.track") }}
|
|
56
|
+
value={component.properties.trackEnabled ? 1 : 0}
|
|
57
|
+
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
58
|
+
onChange={(value) => updateProperty("trackEnabled", Boolean(value), component, onPropertyChange)}
|
|
59
|
+
/>
|
|
60
|
+
{#if component.properties.trackEnabled}
|
|
61
|
+
<Select
|
|
62
|
+
label={{ name: $T("constructor.props.map.trackLength") }}
|
|
63
|
+
type="input"
|
|
64
|
+
options={TRACK_LENGTH_OPTIONS}
|
|
65
|
+
value={TRACK_LENGTH_OPTIONS.find((opt) => opt.value === (component.properties.trackLength ?? 1000))}
|
|
66
|
+
onUpdate={(option) => updateProperty("trackLength", (option as { value: number }).value, component, onPropertyChange)}
|
|
67
|
+
/>
|
|
68
|
+
{/if}
|
|
51
69
|
</div>
|
|
52
70
|
</div>
|
|
53
71
|
{:else}
|
|
@@ -73,6 +91,21 @@
|
|
|
73
91
|
{component}
|
|
74
92
|
{onPropertyChange}
|
|
75
93
|
/>
|
|
94
|
+
<Switch
|
|
95
|
+
label={{ name: $T("constructor.props.map.track") }}
|
|
96
|
+
value={component.properties.trackEnabled ? 1 : 0}
|
|
97
|
+
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
98
|
+
onChange={(value) => updateProperty("trackEnabled", Boolean(value), component, onPropertyChange)}
|
|
99
|
+
/>
|
|
100
|
+
{#if component.properties.trackEnabled}
|
|
101
|
+
<Select
|
|
102
|
+
label={{ name: $T("constructor.props.map.trackLength") }}
|
|
103
|
+
type="input"
|
|
104
|
+
options={TRACK_LENGTH_OPTIONS}
|
|
105
|
+
value={TRACK_LENGTH_OPTIONS.find((opt) => opt.value === (component.properties.trackLength ?? 1000))}
|
|
106
|
+
onUpdate={(option) => updateProperty("trackLength", (option as { value: number }).value, component, onPropertyChange)}
|
|
107
|
+
/>
|
|
108
|
+
{/if}
|
|
76
109
|
</div>
|
|
77
110
|
</div>
|
|
78
111
|
{/if}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type UIComponent, type
|
|
1
|
+
import { type UIComponent, type IMapProps, type IUIComponentHandler } from "../types";
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
component: UIComponent & {
|
|
4
|
-
properties: Partial<
|
|
4
|
+
properties: Partial<IMapProps>;
|
|
5
5
|
};
|
|
6
6
|
onPropertyChange: (updates: Partial<{
|
|
7
7
|
properties?: string | object;
|
package/dist/Map/README.md
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
## Пропсы
|
|
8
8
|
|
|
9
|
-
| Название
|
|
10
|
-
|
|
|
11
|
-
| id
|
|
12
|
-
| label
|
|
13
|
-
| data
|
|
14
|
-
| markerIcon
|
|
9
|
+
| Название | Тип | По умолчанию | Описание |
|
|
10
|
+
| ------------ | ----------------------------------------------------- | ----------------------- | ------------------------------------------------------------------ |
|
|
11
|
+
| id | string | crypto.randomUUID() | Уникальный идентификатор компонента |
|
|
12
|
+
| label | { name?: string; class?: string } | { name: "", class: "" } | Настройки заголовка компонента |
|
|
13
|
+
| data | IDeviceGNSS | $bindable() | Данные GPS устройства для отображения на карте |
|
|
14
|
+
| markerIcon | ConstructorOfATypedSvelteComponent \| string \| null | undefined | Иконка для маркера устройства |
|
|
15
|
+
| trackEnabled | boolean | $bindable(false) | Включает отрисовку траектории движения устройств |
|
|
16
|
+
| trackLength | number | $bindable(1000) | Максимальное количество последних точек в траектории на устройство (100 / 500 / 1000 / 5000 / 10000 / 50000) |
|
|
15
17
|
|
|
16
18
|
### Вложенные типы
|
|
17
19
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from "svelte"
|
|
3
|
+
import { getMapContext } from "./contexts.svelte.js"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
id: string
|
|
7
|
+
coordinates: [number, number][]
|
|
8
|
+
color?: string
|
|
9
|
+
width?: number
|
|
10
|
+
opacity?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { id, coordinates, color = "#3b82f6", width = 3, opacity = 0.85 }: Props = $props()
|
|
14
|
+
|
|
15
|
+
const mapCtx = getMapContext()
|
|
16
|
+
if (!mapCtx.map) throw new Error("Map instance is not initialized.")
|
|
17
|
+
|
|
18
|
+
let added = false
|
|
19
|
+
|
|
20
|
+
const toGeoJSON = () => ({
|
|
21
|
+
type: "Feature" as const,
|
|
22
|
+
geometry: { type: "LineString" as const, coordinates: $state.snapshot(coordinates) },
|
|
23
|
+
properties: {},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (!mapCtx.map || added) return
|
|
28
|
+
|
|
29
|
+
mapCtx.addSource(id, { type: "geojson", data: toGeoJSON() })
|
|
30
|
+
mapCtx.addLayer({
|
|
31
|
+
id,
|
|
32
|
+
type: "line",
|
|
33
|
+
source: id,
|
|
34
|
+
layout: { "line-join": "round", "line-cap": "round" },
|
|
35
|
+
paint: { "line-color": color, "line-width": width, "line-opacity": opacity },
|
|
36
|
+
})
|
|
37
|
+
added = true
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (!added || !mapCtx.map) return
|
|
42
|
+
const source = mapCtx.map.getSource(id) as any
|
|
43
|
+
source?.setData(toGeoJSON())
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!added || !mapCtx.map || !mapCtx.map.getLayer(id)) return
|
|
48
|
+
mapCtx.map.setPaintProperty(id, "line-color", color)
|
|
49
|
+
mapCtx.map.setPaintProperty(id, "line-width", width)
|
|
50
|
+
mapCtx.map.setPaintProperty(id, "line-opacity", opacity)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
onDestroy(() => {
|
|
54
|
+
if (added) {
|
|
55
|
+
mapCtx.removeLayer(id)
|
|
56
|
+
mapCtx.removeSource(id)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
interface Props {
|
|
6
6
|
closeButton?: boolean
|
|
7
7
|
class?: string
|
|
8
|
+
maxWidth?: string
|
|
8
9
|
children?: Snippet
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
let { closeButton = true, class: className = undefined, children }: Props = $props()
|
|
12
|
+
let { closeButton = true, class: className = undefined, maxWidth = "none", children }: Props = $props()
|
|
12
13
|
|
|
13
14
|
let container = $state<HTMLElement | null>(null)
|
|
14
15
|
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
|
|
25
26
|
const options: any = {
|
|
26
27
|
closeButton,
|
|
28
|
+
maxWidth,
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
if (className) {
|
|
@@ -5,5 +5,6 @@ export { default as GeolocateControl } from "./controls/GeolocateControl.svelte"
|
|
|
5
5
|
export { default as FullScreenControl } from "./controls/FullScreenControl.svelte";
|
|
6
6
|
export { default as Marker } from "./Marker.svelte";
|
|
7
7
|
export { default as Popup } from "./Popup.svelte";
|
|
8
|
+
export { default as Line } from "./Line.svelte";
|
|
8
9
|
export { default as CustomControl } from "./controls/CustomControl.svelte";
|
|
9
10
|
export { getMapContext, getSourceContext, getMarkerContext } from "./contexts.svelte.js";
|
|
@@ -5,5 +5,6 @@ export { default as GeolocateControl } from "./controls/GeolocateControl.svelte"
|
|
|
5
5
|
export { default as FullScreenControl } from "./controls/FullScreenControl.svelte";
|
|
6
6
|
export { default as Marker } from "./Marker.svelte";
|
|
7
7
|
export { default as Popup } from "./Popup.svelte";
|
|
8
|
+
export { default as Line } from "./Line.svelte";
|
|
8
9
|
export { default as CustomControl } from "./controls/CustomControl.svelte";
|
|
9
10
|
export { getMapContext, getSourceContext, getMarkerContext } from "./contexts.svelte.js";
|
|
@@ -170,6 +170,8 @@ const translations = {
|
|
|
170
170
|
"constructor.props.multiselect": "Мультивыбор",
|
|
171
171
|
"constructor.props.access": "Доступ (не для владельца)",
|
|
172
172
|
"constructor.props.map.timeout": "Таймаут маркеров:",
|
|
173
|
+
"constructor.props.map.track": "Траектория",
|
|
174
|
+
"constructor.props.map.trackLength": "Точек в траектории:",
|
|
173
175
|
"constructor.props.joystick.pitch.axe": "Ocь ⮃",
|
|
174
176
|
"constructor.props.joystick.roll.axe": "Ось ↶ ↷",
|
|
175
177
|
"constructor.props.joystick.yaw.axe": "Ось ⮂",
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED