poe-svelte-ui-lib 1.85.0 → 1.86.0
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/WidgetMap/README.md +92 -2
- package/dist/WidgetMap/WidgetMap.svelte +225 -203
- package/dist/WidgetMap/WidgetMap.svelte.d.ts +1 -1
- package/dist/WidgetMap/WidgetMapProps.svelte +16 -1
- package/dist/WidgetMap/mission/MapAgro.svelte +713 -0
- package/dist/WidgetMap/mission/MapAgro.svelte.d.ts +19 -0
- package/dist/WidgetMap/mission/MapRoute.svelte +444 -0
- package/dist/WidgetMap/mission/MapRoute.svelte.d.ts +19 -0
- package/dist/WidgetMap/mission/MissionIcon.svelte +5 -0
- package/dist/WidgetMap/mission/MissionIcon.svelte.d.ts +18 -0
- package/dist/WidgetMap/mission/MissionLayers.svelte +220 -0
- package/dist/WidgetMap/mission/MissionLayers.svelte.d.ts +20 -0
- package/dist/WidgetMap/mission/coverage.d.ts +58 -0
- package/dist/WidgetMap/mission/coverage.js +754 -0
- package/dist/WidgetMap/mission/plan.d.ts +22 -0
- package/dist/WidgetMap/mission/plan.js +78 -0
- package/dist/WidgetMap/mission/route.d.ts +33 -0
- package/dist/WidgetMap/mission/route.js +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/locales/translations.js +245 -3
- package/dist/protocols/PROTOCOL.md +13 -0
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/WidgetMap/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# WidgetMap
|
|
2
2
|
|
|
3
|
-
Смарт-виджет «Карта»: где находятся объекты, куда они смотрят и как двигались. Каждый объект это одна группа полей устройства, на одной карте их может быть несколько. Карта на MapLibre.
|
|
3
|
+
Смарт-виджет «Карта»: где находятся объекты, куда они смотрят и как двигались. Каждый объект это одна группа полей устройства, на одной карте их может быть несколько. Ещё два режима строят миссию: «Маршрут» — обычный планировщик из точек с высотами, «Агро» — генератор миссии для агродрона. Аппараты из групп полей видны во всех режимах. Карта на MapLibre.
|
|
4
4
|
|
|
5
5
|
## Назначение
|
|
6
6
|
|
|
@@ -64,6 +64,84 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
64
64
|
|
|
65
65
|
`isTest` включает имитацию шести аппаратов `SIM-1`-`SIM-6` без устройства. Они летают над одним районом Минска, в пределах пары километров, и обновляются раз в полсекунды. На связи всегда три аппарата. Через 40-100 секунд один из них пропадает, и его сразу сменяет тот, кто молчит дольше всех. Поэтому видно все состояния маркера: свежий, красный устаревший, скрытый и снова появившийся. При включении карта переходит к этому району. Пока режим включён, `value` и `groups` не учитываются. В конструкторе все остальные свойства становятся бледными и недоступными, пока имитацию не выключат.
|
|
66
66
|
|
|
67
|
+
## Режим «Маршрут»
|
|
68
|
+
|
|
69
|
+
`mode="route"` — обычный планировщик миссии: точки по порядку облёта, у каждой своя высота и скорость. Аппараты из `groups` рисуются поверх маршрута, как в режиме «Объекты».
|
|
70
|
+
|
|
71
|
+
1. **«Изменить точки»** включает правку. Клик по карте добавляет точку в конец маршрута. Новая точка берёт высоту и скорость предыдущей, первая получает 30 м и 8 м/с. Esc или двойной клик заканчивают правку.
|
|
72
|
+
2. В режиме правки точки можно перетаскивать. Потяните за середину отрезка, чтобы вставить точку между соседними. Правый клик удаляет точку. Без правки на карте видны только номера точек, их не сдвинуть случайно. Точку взлёта `H` можно перетаскивать всегда.
|
|
73
|
+
3. Блок **«Итог»**: сверху расчёт (число точек, длина пути, время, наибольшая высота), ниже список точек с высотой, скоростью, расстоянием от предыдущей точки и удалением; порядок меняется перетаскиванием строки за ручку, как в списках панелей свойств, под ним флажок возврата с посадкой (RTL). Клик по номеру показывает точку на карте.
|
|
74
|
+
|
|
75
|
+
Миссия в формате QGC WPL 110: точка дома, `NAV_TAKEOFF` на высоту первой точки, точки маршрута со своими высотами, `DO_CHANGE_SPEED` перед точкой, где меняется скорость, в конце `RTL`, если включён возврат. Высоты отсчитываются от точки взлёта. Время: каждый отрезок со скоростью своей точки плюс набор высоты и посадка около 2.5 м/с.
|
|
76
|
+
|
|
77
|
+
План хранит владелец карты через `route` и `onRouteChange`. Без `route` показывается демо-облёт вокруг поля под Минском.
|
|
78
|
+
|
|
79
|
+
| Поле `IRoutePlan` | Описание |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `start` | Точка взлёта, `[lng, lat]` |
|
|
82
|
+
| `points` | Точки `{ point: [lng, lat], alt, speed }`: высота в метрах над точкой взлёта, скорость на пути к точке в м/с |
|
|
83
|
+
| `returnHome` | Вернуться и сесть в конце (RTL) |
|
|
84
|
+
|
|
85
|
+
## Режим «Агро»
|
|
86
|
+
|
|
87
|
+
`mode="agro"` превращает карту в генератор миссии для агродрона. Оператор обводит на карте одно или несколько полей и запретные зоны, задаёт угол проходов и ширину захвата. Карта строит траекторию, которая покрывает все поля, обходит зоны и выключает распылитель на разворотах и переходах. Готовую миссию можно скачать, скопировать или отправить на борт в формате QGC WPL 110 для автопилота ArduPilot.
|
|
88
|
+
|
|
89
|
+
Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются два демо-поля под Минском: в большом три запретные зоны, в малом вырез и дерево.
|
|
90
|
+
|
|
91
|
+
### Как пользоваться
|
|
92
|
+
|
|
93
|
+
1. **«Нарисовать поле»**: кликами по карте обведите участок. Контур замыкается кликом по первой точке, двойным кликом, Enter или кнопкой «Замкнуть». Backspace убирает последнюю точку, Esc сбрасывает начатый контур. Когда поле есть, кнопка называется **«Поля»**: в этом режиме клики по карте обводят ещё одно поле.
|
|
94
|
+
2. **«Запретные зоны»**: обведите места, где нельзя лететь и распылять: деревья, опоры ЛЭП, постройки, водоёмы. Зон может быть сколько угодно.
|
|
95
|
+
3. **Точка взлёта** (синяя) видна всегда, её можно перетащить в любой момент.
|
|
96
|
+
4. В блоке **«Маршрут и внесение»** задайте ширину захвата, отступ от границ, высоту, скорость, норму внесения и объём бака. У каждого поля свой угол проходов: новое поле сразу получает лучший угол, «Подобрать» пересчитывает углы всех полей, а ползунок «Угол для всех полей» ставит всем один угол, например чтобы проходы шли вдоль рядов посадки. Угол отдельного поля правится в его строке в блоке «Итог».
|
|
97
|
+
5. Блок **«Итог»** показывает контуры, площадь к обработке, покрытие, длину пути, время, расход раствора и число дозаправок.
|
|
98
|
+
6. **«Скачать»**, **«Копировать»** или **«Отправить»** миссию.
|
|
99
|
+
|
|
100
|
+
Точки контуров видны и перетаскиваются, только пока нажата кнопка «Поля» или «Запретные зоны». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
|
|
101
|
+
|
|
102
|
+
### Как строится траектория
|
|
103
|
+
|
|
104
|
+
1. Поле переводится в метры и поворачивается так, чтобы проходы шли вдоль оси.
|
|
105
|
+
2. Для каждого прохода считаются свободные отрезки: внутри поля и вне зон, с отступом по всей ширине полосы.
|
|
106
|
+
3. Отрезки собираются в участки: подряд идущие проходы, которые соединяются один к одному. Каждый участок облетается змейкой.
|
|
107
|
+
4. Порядок участков и сторона входа выбираются точным перебором, если участков не больше 10. Если больше, используется жадный выбор с улучшением 2-opt.
|
|
108
|
+
5. Переходы между участками идут по кратчайшему пути в обход зон с соблюдением отступа.
|
|
109
|
+
6. Несколько полей облетаются по очереди: каждый раз ближайшее к текущему положению. Путь к первому полю, переходы между полями и возврат домой строятся по открытой местности в обход всех зон.
|
|
110
|
+
|
|
111
|
+
Расчёт занимает 15-30 мс, траектория пересчитывается на каждое изменение.
|
|
112
|
+
|
|
113
|
+
### Миссия
|
|
114
|
+
|
|
115
|
+
- точка дома (точка взлёта или первая точка поля);
|
|
116
|
+
- `DO_CHANGE_SPEED` с заданной скоростью;
|
|
117
|
+
- точки маршрута на заданной высоте над точкой взлёта;
|
|
118
|
+
- команды распыления: включение в начале прохода, выключение в конце и перед вылетом.
|
|
119
|
+
|
|
120
|
+
| Режим распыления | Команда | Что нужно на борту |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| Распылитель ArduPilot | `DO_SPRAYER` (216) | `SPRAY_ENABLE=1`, выход насоса `SERVOx_FUNCTION=22` |
|
|
123
|
+
| Насос на выходе | `DO_SET_SERVO` (183) | канал и ШИМ вкл./выкл. задаются на карте |
|
|
124
|
+
| Без команд | нет | распылитель работает и на разворотах, участки обработаются дважды |
|
|
125
|
+
|
|
126
|
+
Кнопка «Отправить» неактивна, если не передан `onSend`. Во время отправки на ней «Отправка…», после успеха «Отправлено», при ошибке она краснеет. Время полёта считается по скорости плюс около 4 с на разворот. Покрытие — доля поля без зон, которая попадает под распыление: остальное съедают отступы.
|
|
127
|
+
|
|
128
|
+
### План миссии (IMissionPlan)
|
|
129
|
+
|
|
130
|
+
| Поле | Описание |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `fields` | Поля `{ contour: [lng, lat][], angle }`: контур и угол проходов, ° от севера по часовой |
|
|
133
|
+
| `holes` | Запретные зоны, массив контуров; зона относится к полю, внутри которого лежит её центр |
|
|
134
|
+
| `start` | Точка взлёта, `[lng, lat]` |
|
|
135
|
+
| `spacing` | Ширина захвата, м |
|
|
136
|
+
| `margin` | Отступ от границ и зон, м |
|
|
137
|
+
| `alt`, `speed` | Высота, м, и скорость, м/с |
|
|
138
|
+
| `returnHome` | Вернуться к точке взлёта в конце |
|
|
139
|
+
| `rate`, `tank` | Норма внесения, л/га, и объём бака, л |
|
|
140
|
+
| `sprayControl` | `sprayer`, `servo` или `none` |
|
|
141
|
+
| `servoChannel`, `servoOn`, `servoOff` | Канал и ШИМ насоса для режима `servo` |
|
|
142
|
+
|
|
143
|
+
Расчёт доступен и без карты: `planFields` (несколько полей), `planCoverage` (одно поле), `bestFieldAngle`, `toWaypointsFile` экспортируются из библиотеки.
|
|
144
|
+
|
|
67
145
|
## Пропсы
|
|
68
146
|
|
|
69
147
|
| Название | Тип | По умолчанию | Описание |
|
|
@@ -77,6 +155,12 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
77
155
|
| `trackLength` | `number` | `1000` | Точек траектории. Можно связать через `bind:` |
|
|
78
156
|
| `markerTimeout` | `number` | `30000` | Таймаут маркера, мс. Можно связать через `bind:` |
|
|
79
157
|
| `isTest` | `boolean` | `false` | Режим имитации |
|
|
158
|
+
| `mode` | `"objects" \| "route" \| "agro"` | `"objects"` | Карта объектов, планировщик маршрута или генератор миссии для агродрона |
|
|
159
|
+
| `plan` | `IMissionPlan` | `DEFAULT_MISSION_PLAN` | План миссии в режиме «Агро». Можно связать через `bind:` |
|
|
160
|
+
| `onPlanChange` | `(plan) => void` | | Вызывается при каждом изменении плана |
|
|
161
|
+
| `route` | `IRoutePlan` | `DEFAULT_ROUTE_PLAN` | Маршрут в режиме «Маршрут». Можно связать через `bind:` |
|
|
162
|
+
| `onRouteChange` | `(route) => void` | | Вызывается при каждом изменении маршрута |
|
|
163
|
+
| `onSend` | `(mission, plan) => void \| Promise` | | Отправка миссии, `mission` — текст QGC WPL 110 |
|
|
80
164
|
| `label.name` | `string` | | Заголовок. Пусто — «Карта» на языке пользователя |
|
|
81
165
|
| `wrapperClass` | `string` | `""` | Классы обёртки |
|
|
82
166
|
| `componentClass` | `string` | `""` | Мастер-цвет `bg-*` шапки |
|
|
@@ -87,14 +171,20 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
87
171
|
|
|
88
172
|
```svelte
|
|
89
173
|
<UI.WidgetMap groups={["MAV", "GNSS"]} value={{ MAV: mav, GNSS: gnss }} height={400} trackEnabled />
|
|
174
|
+
|
|
175
|
+
<UI.WidgetMap mode="route" bind:route groups={["MAV"]} value={{ MAV: mav }} onSend={(mission) => sendToDevice(mission)} />
|
|
176
|
+
|
|
177
|
+
<UI.WidgetMap mode="agro" bind:plan groups={["MAV"]} value={{ MAV: mav }} onSend={(mission) => sendToDevice(mission)} />
|
|
90
178
|
```
|
|
91
179
|
|
|
92
180
|
В DevCloud и WebApp `value` собирает `GUIPreview`: для каждой группы читает переменные `<ModSN>|<группа>.<ключ>`.
|
|
93
181
|
|
|
94
182
|
## Панель свойств (WidgetMapProps.svelte)
|
|
95
183
|
|
|
96
|
-
В конструкторе задают заголовок, группы через пробел, мастер-цвет, высоту карты (S, M, L, XL: 250, 400, 600, 800 пикселей), иконку маркера и начальные значения траектории.
|
|
184
|
+
В конструкторе задают режим («Объекты», «Маршрут» или «Агро»), заголовок, группы через пробел, мастер-цвет, высоту карты (S, M, L, XL: 250, 400, 600, 800 пикселей), иконку маркера и начальные значения траектории. Группы, иконка маркера, траектория и имитация действуют во всех режимах.
|
|
97
185
|
|
|
98
186
|
## Внутреннее устройство
|
|
99
187
|
|
|
100
188
|
В папке `mapWrapper/` лежат тонкие Svelte-обёртки над MapLibre GL: `MapLibre`, `Marker`, `Popup`, `Line`, элементы управления и `CustomControl` для своих кнопок поверх карты.
|
|
189
|
+
|
|
190
|
+
В папке `mission/` лежат режимы построения миссии: `MapRoute.svelte` и `route.ts` (режим «Маршрут»), `MapAgro.svelte` (интерфейс режима «Агро»), `coverage.ts` (расчёт траектории и выгрузка миссии), `plan.ts` (тип плана и план по умолчанию), `MissionLayers.svelte` (слои карты и перетаскивание точек).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- $lib/WidgetMap/WidgetMap.svelte — смарт-виджет
|
|
1
|
+
<!-- $lib/WidgetMap/WidgetMap.svelte — смарт-виджет «Карта», режим «Объекты» (по умолчанию), «Маршрут» (планировщик миссии, mission/MapRoute.svelte) или «Агро» (генератор миссии, mission/MapAgro.svelte); аппараты из групп полей рисуются во всех режимах. Режим «Объекты»: объекты из групп полей устройства (по умолчанию MAV) с полями Lat/Lon/Alt/Heading/Sat/DevName/DevSN (см. IWidgetMapData), каждая группа — свой маркер и своя траектория. Только отображение: ничего не отправляет и не знает про DeviceStore/WebSocket. Траекторию, длину трека и таймаут маркеров оператор переключает прямо поверх карты, «Показать все» — кнопка в шапке; вид карта сама не меняет — объектов может быть много. -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { onMount, untrack } from "svelte"
|
|
4
4
|
import { slide } from "svelte/transition"
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import MapIcon from "./MapIcon.svelte"
|
|
14
14
|
import { createFlightSim, HOME } from "../flightSim"
|
|
15
15
|
import FitIcon from "./FitIcon.svelte"
|
|
16
|
+
import MapAgro from "./mission/MapAgro.svelte"
|
|
17
|
+
import MapRoute from "./mission/MapRoute.svelte"
|
|
16
18
|
import * as UI from ".."
|
|
17
19
|
import { MapLibre, NavigationControl, ScaleControl, GeolocateControl, FullScreenControl, Marker, Popup, Line, CustomControl } from "./mapWrapper"
|
|
18
20
|
|
|
@@ -30,6 +32,12 @@
|
|
|
30
32
|
isTest = false,
|
|
31
33
|
collapsed = $bindable(false),
|
|
32
34
|
persistKey,
|
|
35
|
+
mode = "objects",
|
|
36
|
+
plan = $bindable(),
|
|
37
|
+
onPlanChange,
|
|
38
|
+
route = $bindable(),
|
|
39
|
+
onRouteChange,
|
|
40
|
+
onSend,
|
|
33
41
|
}: IWidgetMapProps = $props()
|
|
34
42
|
|
|
35
43
|
/* Подсказки — кнопка «?» в шапке, выбор общий для всех WidgetMap */
|
|
@@ -293,222 +301,236 @@
|
|
|
293
301
|
]
|
|
294
302
|
</script>
|
|
295
303
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
wrapperClass="w-auto"
|
|
306
|
-
componentClass="bg-transparent px-3"
|
|
307
|
-
content={{ icon: FitIcon, name: $T("constructor.props.map.fitAll"), info: { text: $T("constructor.props.map.fitAllHint"), side: "bottom" } }}
|
|
308
|
-
disabled={collapsed || visibleBoards.length === 0}
|
|
309
|
-
onClick={fitAllBoards}
|
|
310
|
-
/>
|
|
311
|
-
<span
|
|
312
|
-
class="text-[12px] font-semibold tabular-nums {freshCount === 0
|
|
313
|
-
? 'text-(--red-color)'
|
|
314
|
-
: freshCount < groupList.length
|
|
315
|
-
? 'text-(--muted-color)'
|
|
316
|
-
: 'text-(--green-color)'}"
|
|
317
|
-
use:hint={{ text: $T("widget.map.hint.boards"), enabled: $hints }}
|
|
318
|
-
>
|
|
319
|
-
{freshCount} / {groupList.length}
|
|
320
|
-
</span>
|
|
321
|
-
</div>
|
|
322
|
-
{/snippet}
|
|
323
|
-
</WidgetHeader>
|
|
324
|
-
|
|
325
|
-
{#if !collapsed}
|
|
326
|
-
<div transition:slide={{ duration: 150 }} style="height: {height}px">
|
|
327
|
-
<MapLibre
|
|
328
|
-
bind:map={mapInstance}
|
|
329
|
-
class="h-full w-full overflow-hidden rounded-[14px] border border-(--hairline-color)"
|
|
330
|
-
style={isDarkMode ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" : "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"}
|
|
331
|
-
zoom={view.zoom}
|
|
332
|
-
center={view.center}
|
|
333
|
-
bearing={view.bearing}
|
|
334
|
-
maxPitch={0}
|
|
335
|
-
>
|
|
336
|
-
<NavigationControl />
|
|
337
|
-
<ScaleControl />
|
|
338
|
-
<GeolocateControl />
|
|
339
|
-
<FullScreenControl />
|
|
304
|
+
<!-- Аппараты из групп полей: маркеры, карточки и траектории; рисуются во всех режимах -->
|
|
305
|
+
{#snippet vehicles()}
|
|
306
|
+
{#if trackEnabled}
|
|
307
|
+
{#each visibleBoards as board (board.group)}
|
|
308
|
+
{#if board.track.length >= 2}
|
|
309
|
+
<Line id={`track-${board.group}`} coordinates={board.track} color={isFresh(board) ? "#22c55e" : "#ef4444"} />
|
|
310
|
+
{/if}
|
|
311
|
+
{/each}
|
|
312
|
+
{/if}
|
|
340
313
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
314
|
+
{#each visibleBoards as board (board.group)}
|
|
315
|
+
{@const isFollowed = followed.includes(board.group)}
|
|
316
|
+
<Marker lnglat={{ lng: board.lon, lat: board.lat }}>
|
|
317
|
+
{#snippet content()}
|
|
318
|
+
<div class="flex flex-col items-center justify-center leading-none">
|
|
319
|
+
<div
|
|
320
|
+
class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
|
|
321
|
+
{isFresh(board) ? 'text-(--green-color)' : 'text-(--red-color)'}"
|
|
322
|
+
style="rotate: {board.heading}deg;"
|
|
323
|
+
>
|
|
324
|
+
{#if markerIcon}
|
|
325
|
+
{#if typeof markerIcon === "string"}
|
|
326
|
+
{@html markerIcon}
|
|
327
|
+
{:else}
|
|
328
|
+
{@const IconComponent = markerIcon}
|
|
329
|
+
<IconComponent />
|
|
330
|
+
{/if}
|
|
331
|
+
{:else}
|
|
332
|
+
<div class="rotate-270">
|
|
333
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
|
334
|
+
<path
|
|
335
|
+
fill="none"
|
|
336
|
+
stroke="currentColor"
|
|
337
|
+
stroke-linecap="round"
|
|
338
|
+
stroke-linejoin="round"
|
|
339
|
+
stroke-width="1.5"
|
|
340
|
+
d="M14.76 12H6.832m0 0c0-.275-.057-.55-.17-.808L4.285 5.814c-.76-1.72 1.058-3.442 2.734-2.591L20.8 10.217c1.46.74 1.46 2.826 0 3.566L7.02 20.777c-1.677.851-3.495-.872-2.735-2.591l2.375-5.378A2 2 0 0 0 6.83 12"
|
|
341
|
+
/></svg
|
|
342
|
+
>
|
|
356
343
|
</div>
|
|
357
344
|
{/if}
|
|
358
|
-
<div class="flex items-center gap-2" use:hint={{ text: $T("widget.map.hint.timeout"), enabled: $hints }}>
|
|
359
|
-
<span class="text-sm font-medium">{$T("constructor.props.map.timeout")}</span>
|
|
360
|
-
<select class="rounded px-2 py-1 text-sm" bind:value={markerTimeout}>
|
|
361
|
-
{#each timeoutOptions as opt}
|
|
362
|
-
<option value={opt.value}>{opt.label}</option>
|
|
363
|
-
{/each}
|
|
364
|
-
</select>
|
|
365
|
-
</div>
|
|
366
345
|
</div>
|
|
367
|
-
|
|
346
|
+
<p class="font-bold">{board.name}</p>
|
|
347
|
+
<!-- Метка слежения видна и при закрытом окне деталей -->
|
|
348
|
+
{#if isFollowed}
|
|
349
|
+
<span class="mt-0.5 size-2 rounded-full bg-(--accent-color) ring-2 ring-white" title={$T("widget.map.follow")}></span>
|
|
350
|
+
{/if}
|
|
351
|
+
</div>
|
|
352
|
+
{/snippet}
|
|
368
353
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
{
|
|
374
|
-
|
|
375
|
-
|
|
354
|
+
<Popup closeButton={false} class="map-popup text-left">
|
|
355
|
+
<div class="flex flex-col gap-1.5">
|
|
356
|
+
<div class="flex items-center gap-1.5">
|
|
357
|
+
<span class="size-2 shrink-0 rounded-full {isFresh(board) ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
|
|
358
|
+
<h6 class="text-sm leading-none font-bold">{board.name}</h6>
|
|
359
|
+
{#if board.name !== board.group}<span class="text-[0.6rem] font-semibold uppercase opacity-60">{board.group}</span>{/if}
|
|
360
|
+
</div>
|
|
376
361
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
<div class="rotate-270">
|
|
396
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
|
397
|
-
<path
|
|
398
|
-
fill="none"
|
|
399
|
-
stroke="currentColor"
|
|
400
|
-
stroke-linecap="round"
|
|
401
|
-
stroke-linejoin="round"
|
|
402
|
-
stroke-width="1.5"
|
|
403
|
-
d="M14.76 12H6.832m0 0c0-.275-.057-.55-.17-.808L4.285 5.814c-.76-1.72 1.058-3.442 2.734-2.591L20.8 10.217c1.46.74 1.46 2.826 0 3.566L7.02 20.777c-1.677.851-3.495-.872-2.735-2.591l2.375-5.378A2 2 0 0 0 6.83 12"
|
|
404
|
-
/></svg
|
|
405
|
-
>
|
|
406
|
-
</div>
|
|
407
|
-
{/if}
|
|
408
|
-
</div>
|
|
409
|
-
<p class="font-bold">{board.name}</p>
|
|
410
|
-
<!-- Метка слежения видна и при закрытом окне деталей -->
|
|
411
|
-
{#if isFollowed}
|
|
412
|
-
<span class="mt-0.5 size-2 rounded-full bg-(--accent-color) ring-2 ring-white" title={$T("widget.map.follow")}></span>
|
|
413
|
-
{/if}
|
|
414
|
-
</div>
|
|
415
|
-
{/snippet}
|
|
362
|
+
<div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
|
|
363
|
+
<div>
|
|
364
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Lat / Lon</p>
|
|
365
|
+
<p class="font-medium">{board.lat.toFixed(6)}, {board.lon.toFixed(6)}</p>
|
|
366
|
+
</div>
|
|
367
|
+
<div>
|
|
368
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Alt</p>
|
|
369
|
+
<p class="font-medium">{board.alt.toFixed(1)} m</p>
|
|
370
|
+
</div>
|
|
371
|
+
<div>
|
|
372
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Heading</p>
|
|
373
|
+
<p class="font-medium">{board.heading.toFixed(1)}°</p>
|
|
374
|
+
</div>
|
|
375
|
+
<div>
|
|
376
|
+
<p class="text-[0.6rem] font-semibold tracking-wide uppercase opacity-60">Satellites</p>
|
|
377
|
+
<p class="font-medium">{board.sat}</p>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
416
380
|
|
|
417
|
-
|
|
418
|
-
<div class="flex flex-col gap-1.5">
|
|
419
|
-
<div class="flex items-center gap-1.5">
|
|
420
|
-
<span class="size-2 shrink-0 rounded-full {isFresh(board) ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
|
|
421
|
-
<h6 class="text-sm leading-none font-bold">{board.name}</h6>
|
|
422
|
-
{#if board.name !== board.group}<span class="text-[0.6rem] font-semibold uppercase opacity-60">{board.group}</span>{/if}
|
|
423
|
-
</div>
|
|
381
|
+
{#if board.sn}<p class="text-center font-mono text-[0.6rem] tracking-wide uppercase">{board.sn}</p>{/if}
|
|
424
382
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
383
|
+
<div class="grid grid-cols-[1fr_auto_1fr] items-center border-t border-(--hairline-color) pt-1">
|
|
384
|
+
<div class="relative flex items-center justify-self-start">
|
|
385
|
+
<button
|
|
386
|
+
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"
|
|
387
|
+
onclick={(e) => {
|
|
388
|
+
e.preventDefault()
|
|
389
|
+
copyBoard(board)
|
|
390
|
+
}}
|
|
391
|
+
aria-label="Copy"
|
|
392
|
+
title="Copy"
|
|
393
|
+
>
|
|
394
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24">
|
|
395
|
+
<g fill="none" stroke="currentColor" stroke-width="1.5">
|
|
396
|
+
<path
|
|
397
|
+
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"
|
|
398
|
+
/>
|
|
399
|
+
<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" />
|
|
400
|
+
</g>
|
|
401
|
+
</svg>
|
|
402
|
+
</button>
|
|
403
|
+
<CopiedChip show={isCopied} class="-top-1 right-0 -translate-y-full" />
|
|
404
|
+
</div>
|
|
443
405
|
|
|
444
|
-
|
|
406
|
+
<button
|
|
407
|
+
class="flex cursor-pointer items-center gap-1 rounded-lg border px-2 py-0.5 text-xs font-semibold transition {isFollowed
|
|
408
|
+
? 'border-(--accent-color) bg-(--accent-color) text-white'
|
|
409
|
+
: 'border-(--hairline-color) bg-transparent opacity-80 hover:opacity-100'}"
|
|
410
|
+
aria-pressed={isFollowed}
|
|
411
|
+
onclick={() => toggleFollow(board.group)}
|
|
412
|
+
>
|
|
413
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 24 24">
|
|
414
|
+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5">
|
|
415
|
+
<circle cx="12" cy="12" r="7" />
|
|
416
|
+
<circle cx="12" cy="12" r="2.5" fill={isFollowed ? "currentColor" : "none"} />
|
|
417
|
+
<path d="M12 2v3m0 14v3M2 12h3m14 0h3" />
|
|
418
|
+
</g>
|
|
419
|
+
</svg>
|
|
420
|
+
{$T("widget.map.follow")}
|
|
421
|
+
</button>
|
|
445
422
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
423
|
+
<button
|
|
424
|
+
class="cursor-pointer justify-self-end rounded-lg border-none bg-transparent p-0.5 text-(--red-color) transition hover:opacity-80"
|
|
425
|
+
aria-label="Hide"
|
|
426
|
+
title="Hide"
|
|
427
|
+
onclick={() => hideBoard(board.group)}
|
|
428
|
+
>
|
|
429
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1.25rem" height="1.25rem" viewBox="0 0 24 24"
|
|
430
|
+
><path
|
|
431
|
+
fill="none"
|
|
432
|
+
stroke="currentColor"
|
|
433
|
+
stroke-linecap="round"
|
|
434
|
+
stroke-linejoin="round"
|
|
435
|
+
stroke-width="1.5"
|
|
436
|
+
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"
|
|
437
|
+
/></svg
|
|
438
|
+
>
|
|
439
|
+
</button>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</Popup>
|
|
443
|
+
</Marker>
|
|
444
|
+
{/each}
|
|
445
|
+
{/snippet}
|
|
468
446
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
447
|
+
<!-- Режим «Агро» — генератор миссии для агродрона, «Маршрут» — обычный планировщик миссии, остальное — карта объектов -->
|
|
448
|
+
{#if mode === "agro"}
|
|
449
|
+
<MapAgro {wrapperClass} {componentClass} {label} height={height ?? 520} bind:plan bind:collapsed {persistKey} {onPlanChange} {onSend} overlay={vehicles} />
|
|
450
|
+
{:else if mode === "route"}
|
|
451
|
+
<MapRoute {wrapperClass} {componentClass} {label} height={height ?? 520} bind:route bind:collapsed {persistKey} {onRouteChange} {onSend} overlay={vehicles} />
|
|
452
|
+
{:else}
|
|
453
|
+
<div
|
|
454
|
+
bind:this={rootEl}
|
|
455
|
+
class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
|
|
456
|
+
style={accentStyle}
|
|
457
|
+
>
|
|
458
|
+
<WidgetHeader icon={MapIcon} label={label?.name || $T("widget.map.title")} bind:collapsed {hints}>
|
|
459
|
+
{#snippet right()}
|
|
460
|
+
<div class="flex items-center gap-3">
|
|
461
|
+
<UI.Button
|
|
462
|
+
wrapperClass="w-auto"
|
|
463
|
+
componentClass="bg-transparent px-3"
|
|
464
|
+
content={{ icon: FitIcon, name: $T("constructor.props.map.fitAll"), info: { text: $T("constructor.props.map.fitAllHint"), side: "bottom" } }}
|
|
465
|
+
disabled={collapsed || visibleBoards.length === 0}
|
|
466
|
+
onClick={fitAllBoards}
|
|
467
|
+
/>
|
|
468
|
+
<span
|
|
469
|
+
class="text-[12px] font-semibold tabular-nums {freshCount === 0
|
|
470
|
+
? 'text-(--red-color)'
|
|
471
|
+
: freshCount < groupList.length
|
|
472
|
+
? 'text-(--muted-color)'
|
|
473
|
+
: 'text-(--green-color)'}"
|
|
474
|
+
use:hint={{ text: $T("widget.map.hint.boards"), enabled: $hints }}
|
|
475
|
+
>
|
|
476
|
+
{freshCount} / {groupList.length}
|
|
477
|
+
</span>
|
|
478
|
+
</div>
|
|
479
|
+
{/snippet}
|
|
480
|
+
</WidgetHeader>
|
|
485
481
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
482
|
+
{#if !collapsed}
|
|
483
|
+
<div transition:slide={{ duration: 150 }} style="height: {height}px">
|
|
484
|
+
<MapLibre
|
|
485
|
+
bind:map={mapInstance}
|
|
486
|
+
class="h-full w-full overflow-hidden rounded-[14px] border border-(--hairline-color)"
|
|
487
|
+
style={isDarkMode
|
|
488
|
+
? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
|
|
489
|
+
: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"}
|
|
490
|
+
zoom={view.zoom}
|
|
491
|
+
center={view.center}
|
|
492
|
+
bearing={view.bearing}
|
|
493
|
+
maxPitch={0}
|
|
494
|
+
>
|
|
495
|
+
<NavigationControl />
|
|
496
|
+
<ScaleControl />
|
|
497
|
+
<GeolocateControl />
|
|
498
|
+
<FullScreenControl />
|
|
499
|
+
|
|
500
|
+
<!-- Панель оператора поверх карты -->
|
|
501
|
+
<CustomControl position="top-left">
|
|
502
|
+
<div class="flex flex-wrap items-center gap-3 rounded-lg px-2 py-1 text-black">
|
|
503
|
+
<label class="flex items-center gap-1.5 text-sm font-medium" use:hint={{ text: $T("widget.map.hint.track"), enabled: $hints }}>
|
|
504
|
+
<input type="checkbox" bind:checked={trackEnabled} />
|
|
505
|
+
{$T("constructor.props.map.track")}
|
|
506
|
+
</label>
|
|
507
|
+
{#if trackEnabled}
|
|
508
|
+
<div class="flex items-center gap-2">
|
|
509
|
+
<span class="text-sm font-medium">{$T("constructor.props.map.trackLength")}</span>
|
|
510
|
+
<select class="rounded px-2 py-1 text-sm" bind:value={trackLength}>
|
|
511
|
+
{#each trackLengthOptions as opt}
|
|
512
|
+
<option value={opt}>{opt}</option>
|
|
513
|
+
{/each}
|
|
514
|
+
</select>
|
|
503
515
|
</div>
|
|
516
|
+
{/if}
|
|
517
|
+
<div class="flex items-center gap-2" use:hint={{ text: $T("widget.map.hint.timeout"), enabled: $hints }}>
|
|
518
|
+
<span class="text-sm font-medium">{$T("constructor.props.map.timeout")}</span>
|
|
519
|
+
<select class="rounded px-2 py-1 text-sm" bind:value={markerTimeout}>
|
|
520
|
+
{#each timeoutOptions as opt}
|
|
521
|
+
<option value={opt.value}>{opt.label}</option>
|
|
522
|
+
{/each}
|
|
523
|
+
</select>
|
|
504
524
|
</div>
|
|
505
|
-
</
|
|
506
|
-
</
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
525
|
+
</div>
|
|
526
|
+
</CustomControl>
|
|
527
|
+
|
|
528
|
+
{@render vehicles()}
|
|
529
|
+
</MapLibre>
|
|
530
|
+
</div>
|
|
531
|
+
{/if}
|
|
532
|
+
</div>
|
|
533
|
+
{/if}
|
|
512
534
|
|
|
513
535
|
<style>
|
|
514
536
|
/* Попап по правилам карточки (design-handoff, раздел 9): радиус 14, линия, тень уровня 2 */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { IWidgetMapProps } from "../types";
|
|
2
|
-
declare const WidgetMap: import("svelte").Component<IWidgetMapProps, {}, "collapsed" | "trackEnabled" | "trackLength" | "markerTimeout">;
|
|
2
|
+
declare const WidgetMap: import("svelte").Component<IWidgetMapProps, {}, "collapsed" | "trackEnabled" | "trackLength" | "markerTimeout" | "plan" | "route">;
|
|
3
3
|
type WidgetMap = ReturnType<typeof WidgetMap>;
|
|
4
4
|
export default WidgetMap;
|