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.
@@ -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 { id = crypto.randomUUID(), label = { name: "", class: "" }, data = $bindable(), markerIcon }: IMapProps = $props()
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
- timeoutId: number | null
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 restartFreshTimer = (index: number) => {
22
- const device = devices[index]
23
- // Очистить старый таймер, если есть
24
- if (device.timeoutId !== null) {
25
- clearTimeout(device.timeoutId)
26
- }
27
- // Запустить новый
28
- const id = setTimeout(() => {
29
- if (index < devices.length && devices[index].DevSN === device.DevSN) {
30
- devices[index].isFresh = false
31
- devices[index].timeoutId = null
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
- devices[idx] = { ...devices[idx], ...data }
45
- restartFreshTimer(idx)
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
- timeoutId: null,
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.addEventListener("ThemeChange", handleThemeChange as EventListener)
102
+ if (typeof window !== "undefined") window.removeEventListener("ThemeChange", handleThemeChange as EventListener)
73
103
 
74
104
  for (const device of devices) {
75
- if (device.timeoutId !== null) {
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((_, idx) => restartFreshTimer(idx))
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-2 px-2 py-1 text-black">
114
- <label for="timeout" class="text-sm font-medium">{$T("constructor.props.map.timeout")}</label>
115
- <select
116
- id="timeout"
117
- class="rounded px-2 py-1 text-sm"
118
- bind:value={markerTimeout}
119
- onchange={(e) => changeTimeout(parseInt((e.target as HTMLSelectElement).value))}
120
- >
121
- {#each timeoutOptions as opt}
122
- <option value={opt.value}>{opt.label}</option>
123
- {/each}
124
- </select>
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
- <Popup closeButton={false} class="map-popup rounded-2xl text-left w-[26rem]">
163
- <p>DevSN: {device.DevSN}</p>
164
- <p>DevName: {device.DevName}</p>
165
- <p>Lat: {`${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt.toFixed(1)}`}</p>
166
- <p>Heading: {device.NavHeading.toFixed(1)} | Sat Use: {device.NavSatUse}</p>
167
-
168
- <div class="relative flex justify-between">
169
- <button
170
- class="absolute right-0 flex cursor-pointer border-none bg-transparent"
171
- onclick={(e) => {
172
- e.preventDefault()
173
- navigator.clipboard.writeText(
174
- `DevName: ${device.DevName}\nDevSN: ${device.DevSN}\nLat: ${device.NavLat.toFixed(3)} | Lon: ${device.NavLon.toFixed(3)} | Alt: ${device.NavAlt}\nHeading: ${device.NavHeading} | Sat Use: ${device.NavSatUse}`,
175
- )
176
- isCopied = true
177
- setTimeout(() => (isCopied = false), 1000)
178
- }}
179
- aria-label="Copy"
180
- >
181
- <svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24">
182
- <g fill="none" stroke="currentColor" stroke-width="1.5">
183
- <path
184
- 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"
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
- {/if}
198
-
199
- <button class="size-6 cursor-pointer" aria-label="Delete" onclick={() => (devices = devices.filter((dev) => dev.DevSN !== device.DevSN))}>
200
- <svg xmlns="http://www.w3.org/2000/svg" width="1.5rem" height="1.5rem" viewBox="0 0 24 24"
201
- ><path
202
- fill="none"
203
- stroke="currentColor"
204
- stroke-linecap="round"
205
- stroke-linejoin="round"
206
- stroke-width="1.5"
207
- 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"
208
- color="currentColor"
209
- /></svg
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
- </button>
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>
@@ -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;
@@ -1,17 +1,20 @@
1
1
  <script lang="ts">
2
2
  import { T } from "../locales/i18n"
3
- import { type UIComponent, type IGraphProps, updateProperty, type IUIComponentHandler } from "../types"
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<IGraphProps> }
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 IGraphProps, type IUIComponentHandler } from "../types";
1
+ import { type UIComponent, type IMapProps, type IUIComponentHandler } from "../types";
2
2
  type $$ComponentProps = {
3
3
  component: UIComponent & {
4
- properties: Partial<IGraphProps>;
4
+ properties: Partial<IMapProps>;
5
5
  };
6
6
  onPropertyChange: (updates: Partial<{
7
7
  properties?: string | object;
@@ -6,12 +6,14 @@
6
6
 
7
7
  ## Пропсы
8
8
 
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 | Иконка для маркера устройства |
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>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ id: string;
3
+ coordinates: [number, number][];
4
+ color?: string;
5
+ width?: number;
6
+ opacity?: number;
7
+ }
8
+ declare const Line: import("svelte").Component<Props, {}, "">;
9
+ type Line = ReturnType<typeof Line>;
10
+ export default Line;
@@ -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) {
@@ -2,6 +2,7 @@ import { type Snippet } from "svelte";
2
2
  interface Props {
3
3
  closeButton?: boolean;
4
4
  class?: string;
5
+ maxWidth?: string;
5
6
  children?: Snippet;
6
7
  }
7
8
  declare const Popup: import("svelte").Component<Props, {}, "">;
@@ -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
@@ -224,6 +224,8 @@ export interface IMapProps {
224
224
  };
225
225
  data: IDeviceGNSS | null;
226
226
  markerIcon?: ConstructorOfATypedSvelteComponent | string;
227
+ trackEnabled?: boolean;
228
+ trackLength?: number;
227
229
  }
228
230
  export interface IProgressBarProps {
229
231
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.11.2",
3
+ "version": "1.12.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -56,4 +56,4 @@
56
56
  "vite": "^8.1.0",
57
57
  "vite-plugin-compression": "^0.5.1"
58
58
  }
59
- }
59
+ }