poe-svelte-ui-lib 1.11.3 → 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,14 +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
20
  staleTimeoutId: number | null
14
21
  removeTimeoutId: number | null
22
+ track: [number, number][]
15
23
  }
16
24
 
17
25
  let devices: MapDevice[] = $state([])
@@ -47,10 +55,13 @@
47
55
  // Обработка входящих данных
48
56
  $effect(() => {
49
57
  if (data) {
58
+ /* (0, 0) — признак отсутствия реального GPS-фикса, в траекторию такие точки не добавляем */
59
+ const hasFix = data.NavLat !== 0 || data.NavLon !== 0
50
60
  const idx = devices.findIndex((d) => d.DevSN === data?.DevSN)
51
61
  if (idx !== -1) {
52
62
  /* Обновление существующего */
53
- devices[idx] = { ...devices[idx], ...data }
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 }
54
65
  } else {
55
66
  /* Новое устройство */
56
67
  const newDevice: MapDevice = {
@@ -58,6 +69,7 @@
58
69
  isFresh: true,
59
70
  staleTimeoutId: null,
60
71
  removeTimeoutId: null,
72
+ track: hasFix ? [[data.NavLon, data.NavLat]] : [],
61
73
  }
62
74
  devices.push(newDevice)
63
75
  }
@@ -66,6 +78,15 @@
66
78
  }
67
79
  })
68
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
+
69
90
  const handleThemeChange = (event: CustomEvent) => {
70
91
  isDarkMode = !event.detail.currentTheme
71
92
  }
@@ -85,6 +106,15 @@
85
106
  }
86
107
  })
87
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
+
88
118
  const timeoutOptions: { label: string; value: number }[] = [
89
119
  { label: "30 sec", value: 30_000 },
90
120
  { label: "1 min", value: 60_000 },
@@ -117,21 +147,47 @@
117
147
  <FullScreenControl />
118
148
 
119
149
  <CustomControl position="top-left">
120
- <div class="flex items-center gap-2 px-2 py-1 text-black">
121
- <label for="timeout" class="text-sm font-medium">{$T("constructor.props.map.timeout")}</label>
122
- <select
123
- id="timeout"
124
- class="rounded px-2 py-1 text-sm"
125
- bind:value={markerTimeout}
126
- onchange={(e) => changeTimeout(parseInt((e.target as HTMLSelectElement).value))}
127
- >
128
- {#each timeoutOptions as opt}
129
- <option value={opt.value}>{opt.label}</option>
130
- {/each}
131
- </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>
132
180
  </div>
133
181
  </CustomControl>
134
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
+
135
191
  {#each devices as device}
136
192
  <Marker lnglat={{ lng: device.NavLon, lat: device.NavLat }}>
137
193
  {#snippet content()}
@@ -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,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.3",
3
+ "version": "1.12.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {