poe-svelte-ui-lib 1.93.0 → 1.94.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/WidgetDeviceInfo/WidgetDeviceInfo.svelte +1 -1
- package/dist/WidgetFileSystem/WidgetFileSystem.svelte +1 -1
- package/dist/WidgetMap/README.md +2 -2
- package/dist/WidgetMap/WidgetMap.svelte +1 -1
- package/dist/WidgetMap/mission/MapAgro.svelte +47 -6
- package/dist/WidgetMap/mission/MapRoute.svelte +1 -1
- package/dist/WidgetMap/mission/MissionLayers.svelte +21 -3
- package/dist/WidgetMap/mission/MissionLayers.svelte.d.ts +1 -0
- package/dist/WidgetMap/mission/SpinnerIcon.svelte +3 -4
- package/dist/WidgetMap/mission/coverage.js +173 -74
- package/dist/WidgetMap/mission/plan.js +46 -34
- package/dist/WidgetPFD/WidgetPFD.svelte +1 -1
- package/dist/WidgetRGBStrip/WidgetRGBStrip.svelte +1 -1
- package/dist/WidgetStackInfo/WidgetStackInfo.svelte +1 -1
- package/dist/WidgetWiFi/README.md +24 -24
- package/dist/WidgetWiFi/WidgetWiFi.svelte +1 -1
- package/dist/hints.d.ts +1 -1
- package/dist/hints.js +8 -7
- package/dist/locales/translations.js +7 -6
- package/package.json +1 -1
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}: IWidgetDeviceInfoProps = $props()
|
|
27
27
|
|
|
28
28
|
/* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetDeviceInfo */
|
|
29
|
-
const hints = hintsSetting("WidgetDeviceInfo")
|
|
29
|
+
const hints = $derived(hintsSetting("WidgetDeviceInfo", persistKey))
|
|
30
30
|
|
|
31
31
|
let confirmReset = $state(false)
|
|
32
32
|
const factoryReset = () => {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}: IWidgetFileSystemProps = $props()
|
|
31
31
|
|
|
32
32
|
/* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetFileSystem */
|
|
33
|
-
const hints = hintsSetting("WidgetFileSystem")
|
|
33
|
+
const hints = $derived(hintsSetting("WidgetFileSystem", persistKey))
|
|
34
34
|
|
|
35
35
|
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
|
36
36
|
|
package/dist/WidgetMap/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
94
94
|
|
|
95
95
|
`mode="agro"` превращает карту в генератор миссии для агродрона. Оператор обводит на карте одно или несколько полей и запретные зоны, задаёт угол проходов и ширину захвата. Карта строит траекторию, которая покрывает все поля, обходит зоны и выключает распылитель на разворотах и переходах. Готовую миссию можно скачать, скопировать или отправить на борт в формате QGC WPL 110 для автопилота ArduPilot.
|
|
96
96
|
|
|
97
|
-
Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются три демо-поля под Минском: большое с рощей, опорой ЛЭП и постройкой, среднее
|
|
97
|
+
Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются три демо-поля под Минском: большое сложной формы с прорезью и выступами, рощей, опорой ЛЭП и постройкой, среднее с выступами, крюком и прудом, малое с вырезом и деревом. Между полями стоит большая треугольная зона, внизу зона, которая задевает все три поля; перелёты их обходят. Ширина захвата по умолчанию 5 м, отступ от границ 2 м. Ширина захвата по умолчанию 5 м, отступ от границ 2 м.
|
|
98
98
|
|
|
99
99
|
### Как пользоваться
|
|
100
100
|
|
|
@@ -108,7 +108,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
108
108
|
5. Блок **«Итог»** показывает контуры, площадь к обработке, покрытие, длину пути, время, расход раствора и число дозаправок.
|
|
109
109
|
6. **«Скачать»**, **«Копировать»** или **«Отправить»** миссию.
|
|
110
110
|
|
|
111
|
-
Точки контуров видны и перетаскиваются, только пока включён «Редактор полей» или «Редактор запретных зон». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
|
|
111
|
+
Точки контуров видны и перетаскиваются, только пока включён «Редактор полей» или «Редактор запретных зон». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Там же видно, где какое поле или зона: при наведении на строку контур подсвечивается на карте, нажатие на название приближает к нему карту. Подписей на самой карте нет, маленькой зоне их некуда поставить. Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
|
|
112
112
|
|
|
113
113
|
### Как строится траектория
|
|
114
114
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}: IWidgetMapProps = $props()
|
|
45
45
|
|
|
46
46
|
/* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetMap */
|
|
47
|
-
const hints = hintsSetting("WidgetMap")
|
|
47
|
+
const hints = $derived(hintsSetting("WidgetMap", persistKey))
|
|
48
48
|
|
|
49
49
|
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
onSend,
|
|
53
53
|
}: Props = $props()
|
|
54
54
|
|
|
55
|
-
const hints = hintsSetting("WidgetMap")
|
|
55
|
+
const hints = $derived(hintsSetting("WidgetMap", persistKey))
|
|
56
56
|
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
|
57
57
|
|
|
58
58
|
let hasMergedPersisted = false
|
|
@@ -285,6 +285,30 @@
|
|
|
285
285
|
geometry: { type: "LineString", coordinates: s.points },
|
|
286
286
|
})),
|
|
287
287
|
})
|
|
288
|
+
/* Поле или зона, на которые навели в списке «Итога», подсвечиваются на карте; нажатие на название приближает карту к контуру */
|
|
289
|
+
let highlighted = $state<{ kind: "field" | "hole"; index: number } | null>(null)
|
|
290
|
+
let highlightGeo = $derived.by(() => {
|
|
291
|
+
if (!highlighted) return null
|
|
292
|
+
const ring = highlighted.kind === "field" ? fields[highlighted.index] : plan.holes[highlighted.index]
|
|
293
|
+
if (!ring || ring.length < 3) return null
|
|
294
|
+
return {
|
|
295
|
+
type: "FeatureCollection",
|
|
296
|
+
features: [{ type: "Feature", properties: { kind: highlighted.kind }, geometry: { type: "Polygon", coordinates: [closed(ring)] } }],
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
const flyTo = (ring: LngLat[]) => {
|
|
300
|
+
if (!mapInstance || ring.length < 3) return
|
|
301
|
+
const lngs = ring.map((p) => p[0])
|
|
302
|
+
const lats = ring.map((p) => p[1])
|
|
303
|
+
mapInstance.fitBounds(
|
|
304
|
+
[
|
|
305
|
+
[Math.min(...lngs), Math.min(...lats)],
|
|
306
|
+
[Math.max(...lngs), Math.max(...lats)],
|
|
307
|
+
],
|
|
308
|
+
{ padding: 60, maxZoom: 19, duration: 500 },
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
288
312
|
let draftGeo = $derived({
|
|
289
313
|
type: "FeatureCollection",
|
|
290
314
|
features: drawing
|
|
@@ -542,6 +566,7 @@
|
|
|
542
566
|
{onDragEnd}
|
|
543
567
|
{onHandleRightClick}
|
|
544
568
|
colors={mapColors}
|
|
569
|
+
highlight={highlightGeo}
|
|
545
570
|
onCursor={(ll) => (cursor = ll)}
|
|
546
571
|
/>
|
|
547
572
|
{@render overlay?.()}
|
|
@@ -682,9 +707,16 @@
|
|
|
682
707
|
<span class="text-[11px] font-semibold tracking-wide text-(--faint-color) uppercase">{$T("widget.mission.zones")}</span>
|
|
683
708
|
<div class="flex flex-col gap-1.5 rounded-lg bg-(--container-color) p-3 text-[13px] tabular-nums">
|
|
684
709
|
{#each fields as field, i}
|
|
685
|
-
<div
|
|
686
|
-
|
|
687
|
-
|
|
710
|
+
<div
|
|
711
|
+
class="-mx-1.5 flex items-center justify-between gap-2 rounded-md px-1.5 transition-colors duration-150 hover:bg-(--green-soft)"
|
|
712
|
+
role="listitem"
|
|
713
|
+
onmouseenter={() => (highlighted = { kind: "field", index: i })}
|
|
714
|
+
onmouseleave={() => (highlighted = null)}
|
|
715
|
+
>
|
|
716
|
+
<button
|
|
717
|
+
class="flex cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left text-(--font-color)"
|
|
718
|
+
title={$T("widget.mission.showOnMap")}
|
|
719
|
+
onclick={() => flyTo(field)}><span class="size-2.5 rounded-sm bg-(--green-mark)"></span>{$T("widget.mission.field")} {i + 1}</button
|
|
688
720
|
>
|
|
689
721
|
<span class="flex items-center gap-3">
|
|
690
722
|
<span class="flex items-center gap-1" use:hint={{ text: $T("widget.mission.hint.fieldAngle"), enabled: $hints }}>
|
|
@@ -706,8 +738,17 @@
|
|
|
706
738
|
<span class="text-(--muted-color)">{$T("widget.mission.noField")}</span>
|
|
707
739
|
{/each}
|
|
708
740
|
{#each plan.holes as hole, i}
|
|
709
|
-
<div
|
|
710
|
-
|
|
741
|
+
<div
|
|
742
|
+
class="-mx-1.5 flex items-center justify-between gap-2 rounded-md px-1.5 transition-colors duration-150 hover:bg-(--red-soft)"
|
|
743
|
+
role="listitem"
|
|
744
|
+
onmouseenter={() => (highlighted = { kind: "hole", index: i })}
|
|
745
|
+
onmouseleave={() => (highlighted = null)}
|
|
746
|
+
>
|
|
747
|
+
<button
|
|
748
|
+
class="flex cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left text-(--font-color)"
|
|
749
|
+
title={$T("widget.mission.showOnMap")}
|
|
750
|
+
onclick={() => flyTo(hole)}><span class="size-2.5 rounded-sm bg-(--red-mark)"></span>{$T("widget.mission.hole")} {i + 1}</button
|
|
751
|
+
>
|
|
711
752
|
<span class="flex items-center gap-2">
|
|
712
753
|
<span class="font-semibold">{areaText(fieldArea(hole, []))}</span>
|
|
713
754
|
<button class="cursor-pointer border-none bg-transparent p-0 text-(--red-text)" onclick={() => removeHole(i)} aria-label="Delete"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- $lib/WidgetMap/mission/MissionLayers.svelte - слои карты генератора миссии: поле и зоны, траектория, черновик контура и ручки вершин; клики и перетаскивание ручек отдаёт наверх -->
|
|
1
|
+
<!-- $lib/WidgetMap/mission/MissionLayers.svelte - слои карты генератора миссии: поле и зоны, траектория, подсветка выбранного в списке контура, черновик контура и ручки вершин; клики и перетаскивание ручек отдаёт наверх -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { onDestroy, untrack } from "svelte"
|
|
4
4
|
import { getMapContext } from "../mapWrapper"
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
onHandleRightClick: (id: string) => void
|
|
19
19
|
onCursor: (ll: LngLat | null) => void
|
|
20
20
|
colors: IMapColors /* Маршрут и точки - мастер-цвет, поле, зоны и развороты - цвета по смыслу */
|
|
21
|
+
highlight?: any /* Контур, на который навели в списке «Итога»: поле или зона с kind */
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
let { zones, path, draft, handles, drawing, onMapClick, onMapDblClick, onDrag, onDragEnd, onHandleRightClick, onCursor, colors }: Props = $props()
|
|
24
|
+
let { zones, path, draft, handles, drawing, onMapClick, onMapDblClick, onDrag, onDragEnd, onHandleRightClick, onCursor, colors, highlight }: Props = $props()
|
|
24
25
|
|
|
25
26
|
/* Краски слоёв от цветов виджета: одна таблица и для создания слоёв, и для перекраски */
|
|
26
27
|
const paints = (c: IMapColors): Record<string, Record<string, any>> => {
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
"msn-path-spray": { "line-color": c.green },
|
|
34
35
|
"msn-path-route": { "line-color": c.accent },
|
|
35
36
|
"msn-draft-line": { "line-color": byKind },
|
|
37
|
+
"msn-highlight-fill": { "fill-color": byKind },
|
|
38
|
+
"msn-highlight-line": { "line-color": byKind },
|
|
36
39
|
"msn-handles": {
|
|
37
40
|
"circle-color": ["match", ["get", "role"], "mid", "#ffffff", "start", c.accent, "wp", c.accent, byKind],
|
|
38
41
|
"circle-stroke-color": ["match", ["get", "role"], "mid", ["match", ["get", "kind"], "hole", c.red, "route", c.accent, c.green], "#ffffff"],
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
const mapCtx = getMapContext()
|
|
46
49
|
const map = mapCtx.map!
|
|
47
50
|
|
|
48
|
-
const SOURCES = ["msn-zones", "msn-path", "msn-draft", "msn-handles"]
|
|
51
|
+
const SOURCES = ["msn-zones", "msn-path", "msn-highlight", "msn-draft", "msn-handles"]
|
|
49
52
|
const empty = { type: "FeatureCollection", features: [] }
|
|
50
53
|
|
|
51
54
|
for (const id of SOURCES) mapCtx.addSource(id, { type: "geojson", data: empty as any })
|
|
@@ -97,6 +100,20 @@
|
|
|
97
100
|
layout: { "line-join": "round", "line-cap": "round" },
|
|
98
101
|
paint: { ...initial["msn-path-route"], "line-width": 3 },
|
|
99
102
|
})
|
|
103
|
+
/* Подсветка контура поверх проходов: плотная заливка и толстая обводка, видна и у маленькой зоны */
|
|
104
|
+
mapCtx.addLayer({
|
|
105
|
+
id: "msn-highlight-fill",
|
|
106
|
+
type: "fill",
|
|
107
|
+
source: "msn-highlight",
|
|
108
|
+
paint: { ...initial["msn-highlight-fill"], "fill-opacity": 0.45 },
|
|
109
|
+
})
|
|
110
|
+
mapCtx.addLayer({
|
|
111
|
+
id: "msn-highlight-line",
|
|
112
|
+
type: "line",
|
|
113
|
+
source: "msn-highlight",
|
|
114
|
+
layout: { "line-join": "round" },
|
|
115
|
+
paint: { ...initial["msn-highlight-line"], "line-width": 5 },
|
|
116
|
+
})
|
|
100
117
|
mapCtx.addLayer({
|
|
101
118
|
id: "msn-draft-line",
|
|
102
119
|
type: "line",
|
|
@@ -141,6 +158,7 @@
|
|
|
141
158
|
$effect(() => setData("msn-path", $state.snapshot(path)))
|
|
142
159
|
$effect(() => setData("msn-draft", $state.snapshot(draft)))
|
|
143
160
|
$effect(() => setData("msn-handles", $state.snapshot(handles)))
|
|
161
|
+
$effect(() => setData("msn-highlight", $state.snapshot(highlight) ?? empty))
|
|
144
162
|
|
|
145
163
|
/* Рисование: двойной клик закрывает контур, а не приближает карту */
|
|
146
164
|
$effect(() => {
|
|
@@ -16,6 +16,7 @@ interface Props {
|
|
|
16
16
|
onHandleRightClick: (id: string) => void;
|
|
17
17
|
onCursor: (ll: LngLat | null) => void;
|
|
18
18
|
colors: IMapColors;
|
|
19
|
+
highlight?: any;
|
|
19
20
|
}
|
|
20
21
|
declare const MissionLayers: import("svelte").Component<Props, {}, "">;
|
|
21
22
|
type MissionLayers = ReturnType<typeof MissionLayers>;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
<!-- $lib/WidgetMap/mission/SpinnerIcon.svelte - колесо ожидания размером с родителя: подбор угла
|
|
1
|
+
<!-- $lib/WidgetMap/mission/SpinnerIcon.svelte - колесо ожидания размером с родителя: подбор угла проходов; крутится CSS-анимацией, браузер ведёт её отдельно от расчёта, поэтому она не замирает -->
|
|
2
2
|
<script lang="ts"></script>
|
|
3
3
|
|
|
4
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"
|
|
4
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" class="animate-spin [animation-duration:0.8s]"
|
|
5
5
|
><path fill="currentColor" d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z" opacity=".35" /><path
|
|
6
6
|
fill="currentColor"
|
|
7
7
|
d="M20 12h2A10 10 0 0 0 12 2V4A8 8 0 0 1 20 12Z"
|
|
8
|
-
|
|
9
|
-
></svg
|
|
8
|
+
/></svg
|
|
10
9
|
>
|
|
@@ -35,6 +35,60 @@ const segmentsCross = (a, b, c, d) => {
|
|
|
35
35
|
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
|
|
36
36
|
};
|
|
37
37
|
const segSegDist = (a, b, c, d) => segmentsCross(a, b, c, d) ? 0 : Math.min(segPointDist(a, c, d), segPointDist(b, c, d), segPointDist(c, a, b), segPointDist(d, a, b));
|
|
38
|
+
/* Те же проверки на числах, без временных массивов: их вызывают сотни тысяч раз за расчёт */
|
|
39
|
+
const segPointDistXY = (px, py, ax, ay, bx, by) => {
|
|
40
|
+
const dx = bx - ax;
|
|
41
|
+
const dy = by - ay;
|
|
42
|
+
const len2 = dx * dx + dy * dy;
|
|
43
|
+
const t = len2 < EPS ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2));
|
|
44
|
+
return Math.hypot(px - ax - dx * t, py - ay - dy * t);
|
|
45
|
+
};
|
|
46
|
+
const segSegDistXY = (ax, ay, bx, by, cx, cy, dx, dy) => {
|
|
47
|
+
const d1 = (dx - cx) * (ay - cy) - (dy - cy) * (ax - cx);
|
|
48
|
+
const d2 = (dx - cx) * (by - cy) - (dy - cy) * (bx - cx);
|
|
49
|
+
const d3 = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
50
|
+
const d4 = (bx - ax) * (dy - ay) - (by - ay) * (dx - ax);
|
|
51
|
+
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
|
|
52
|
+
return 0;
|
|
53
|
+
return Math.min(segPointDistXY(ax, ay, cx, cy, dx, dy), segPointDistXY(bx, by, cx, cy, dx, dy), segPointDistXY(cx, cy, ax, ay, bx, by), segPointDistXY(dx, dy, ax, ay, bx, by));
|
|
54
|
+
};
|
|
55
|
+
const pointInRingXY = (x, y, r) => {
|
|
56
|
+
let inside = false;
|
|
57
|
+
for (let i = 0, j = r.length - 1; i < r.length; j = i++) {
|
|
58
|
+
const xi = r[i][0];
|
|
59
|
+
const yi = r[i][1];
|
|
60
|
+
const xj = r[j][0];
|
|
61
|
+
const yj = r[j][1];
|
|
62
|
+
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)
|
|
63
|
+
inside = !inside;
|
|
64
|
+
}
|
|
65
|
+
return inside;
|
|
66
|
+
};
|
|
67
|
+
/* Стороны контуров с рамками: по 8 чисел на сторону (концы и границы рамки) */
|
|
68
|
+
const packEdges = (rings) => {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const ring of rings)
|
|
71
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
72
|
+
const [ax, ay] = ring[j];
|
|
73
|
+
const [bx, by] = ring[i];
|
|
74
|
+
out.push(ax, ay, bx, by, Math.min(ax, bx), Math.min(ay, by), Math.max(ax, bx), Math.max(ay, by));
|
|
75
|
+
}
|
|
76
|
+
return Float64Array.from(out);
|
|
77
|
+
};
|
|
78
|
+
/* Отрезок не ближе clearance ни к одной стороне; стороны, чья рамка дальше clearance от рамки отрезка, не проверяются */
|
|
79
|
+
const edgesClear = (edges, ax, ay, bx, by, clearance) => {
|
|
80
|
+
const minx = Math.min(ax, bx) - clearance;
|
|
81
|
+
const maxx = Math.max(ax, bx) + clearance;
|
|
82
|
+
const miny = Math.min(ay, by) - clearance;
|
|
83
|
+
const maxy = Math.max(ay, by) + clearance;
|
|
84
|
+
for (let k = 0; k < edges.length; k += 8) {
|
|
85
|
+
if (edges[k + 4] > maxx || edges[k + 6] < minx || edges[k + 5] > maxy || edges[k + 7] < miny)
|
|
86
|
+
continue;
|
|
87
|
+
if (segSegDistXY(ax, ay, bx, by, edges[k], edges[k + 1], edges[k + 2], edges[k + 3]) < clearance)
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
38
92
|
/* ********************************************************** */
|
|
39
93
|
/* Интервалы на прямой */
|
|
40
94
|
const intersectSets = (a, b) => {
|
|
@@ -105,10 +159,14 @@ class FreeSpace {
|
|
|
105
159
|
field;
|
|
106
160
|
holes;
|
|
107
161
|
margin;
|
|
162
|
+
allEdges;
|
|
163
|
+
holeEdges;
|
|
108
164
|
constructor(field, holes, margin) {
|
|
109
165
|
this.field = field;
|
|
110
166
|
this.holes = holes;
|
|
111
167
|
this.margin = margin;
|
|
168
|
+
this.allEdges = packEdges([field, ...holes]);
|
|
169
|
+
this.holeEdges = packEdges(holes);
|
|
112
170
|
}
|
|
113
171
|
vertexYs = () => [this.field, ...this.holes].flat().map((p) => p[1]);
|
|
114
172
|
rawAt(y) {
|
|
@@ -135,28 +193,23 @@ class FreeSpace {
|
|
|
135
193
|
}
|
|
136
194
|
/* Отрезок пути допустим: внутри поля, вне зон, не ближе margin к их границам */
|
|
137
195
|
segmentClear(a, b, clearance) {
|
|
138
|
-
const
|
|
139
|
-
|
|
196
|
+
const mx = (a[0] + b[0]) / 2;
|
|
197
|
+
const my = (a[1] + b[1]) / 2;
|
|
198
|
+
if (!pointInRingXY(mx, my, this.field))
|
|
140
199
|
return false;
|
|
141
|
-
for (const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return true;
|
|
200
|
+
for (const h of this.holes)
|
|
201
|
+
if (pointInRingXY(mx, my, h))
|
|
202
|
+
return false;
|
|
203
|
+
return edgesClear(this.allEdges, a[0], a[1], b[0], b[1], clearance);
|
|
148
204
|
}
|
|
149
205
|
/* Для точки взлёта вне поля: граница поля не мешает, только запретные зоны с отступом */
|
|
150
206
|
segmentClearOfHoles(a, b, clearance) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
207
|
+
const mx = (a[0] + b[0]) / 2;
|
|
208
|
+
const my = (a[1] + b[1]) / 2;
|
|
209
|
+
for (const h of this.holes)
|
|
210
|
+
if (pointInRingXY(mx, my, h))
|
|
211
|
+
return false;
|
|
212
|
+
return edgesClear(this.holeEdges, a[0], a[1], b[0], b[1], clearance);
|
|
160
213
|
}
|
|
161
214
|
inField(p) {
|
|
162
215
|
return pointInRing(p, this.field);
|
|
@@ -205,78 +258,115 @@ class FreeSpace {
|
|
|
205
258
|
}
|
|
206
259
|
}
|
|
207
260
|
/* ********************************************************** */
|
|
208
|
-
/* Кратчайшие пути в обход
|
|
261
|
+
/* Кратчайшие пути в обход препятствий: граф видимости, кратчайшие пути между его узлами считаются один раз (Флойд-Уоршелл) */
|
|
209
262
|
class Router {
|
|
210
263
|
space;
|
|
211
264
|
clearance;
|
|
212
265
|
nodes;
|
|
213
|
-
|
|
266
|
+
/* Длины кратчайших путей между узлами и следующий узел на пути, матрицы n x n */
|
|
267
|
+
far;
|
|
268
|
+
next;
|
|
214
269
|
cache = new Map();
|
|
215
270
|
constructor(space, clearance) {
|
|
216
271
|
this.space = space;
|
|
217
272
|
this.clearance = clearance;
|
|
218
273
|
this.nodes = space.graphNodes();
|
|
219
274
|
const n = this.nodes.length;
|
|
220
|
-
|
|
221
|
-
|
|
275
|
+
const far = new Float64Array(n * n).fill(Infinity);
|
|
276
|
+
const next = new Int32Array(n * n).fill(-1);
|
|
222
277
|
for (let i = 0; i < n; i++) {
|
|
278
|
+
far[i * n + i] = 0;
|
|
279
|
+
next[i * n + i] = i;
|
|
223
280
|
for (let j = i + 1; j < n; j++) {
|
|
224
|
-
if (space.segmentClear(this.nodes[i], this.nodes[j], clearance))
|
|
225
|
-
|
|
281
|
+
if (!space.segmentClear(this.nodes[i], this.nodes[j], clearance))
|
|
282
|
+
continue;
|
|
283
|
+
const d = dist(this.nodes[i], this.nodes[j]);
|
|
284
|
+
far[i * n + j] = far[j * n + i] = d;
|
|
285
|
+
next[i * n + j] = j;
|
|
286
|
+
next[j * n + i] = i;
|
|
226
287
|
}
|
|
227
288
|
}
|
|
289
|
+
for (let k = 0; k < n; k++)
|
|
290
|
+
for (let i = 0; i < n; i++) {
|
|
291
|
+
const ik = far[i * n + k];
|
|
292
|
+
if (ik === Infinity)
|
|
293
|
+
continue;
|
|
294
|
+
for (let j = 0; j < n; j++) {
|
|
295
|
+
const c = ik + far[k * n + j];
|
|
296
|
+
if (c < far[i * n + j]) {
|
|
297
|
+
far[i * n + j] = c;
|
|
298
|
+
next[i * n + j] = next[i * n + k];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.far = far;
|
|
303
|
+
this.next = next;
|
|
304
|
+
}
|
|
305
|
+
/* Видимость точки из узлов графа: одна и та же точка входа или выхода ячейки участвует во многих маршрутах */
|
|
306
|
+
visCache = new Map();
|
|
307
|
+
visibility(p, loose) {
|
|
308
|
+
const key = `${p[0]},${p[1]},${loose}`;
|
|
309
|
+
let vis = this.visCache.get(key);
|
|
310
|
+
if (!vis) {
|
|
311
|
+
vis = new Float64Array(this.nodes.length);
|
|
312
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
313
|
+
const q = this.nodes[i];
|
|
314
|
+
const ok = loose ? this.space.segmentClearOfHoles(p, q, this.clearance) : this.space.segmentClear(p, q, this.clearance);
|
|
315
|
+
vis[i] = ok ? dist(p, q) : Infinity;
|
|
316
|
+
}
|
|
317
|
+
this.visCache.set(key, vis);
|
|
318
|
+
}
|
|
319
|
+
return vis;
|
|
228
320
|
}
|
|
229
321
|
route(a, b) {
|
|
230
|
-
|
|
231
|
-
const outA = !this.space.inField(a);
|
|
232
|
-
const outB = !this.space.inField(b);
|
|
233
|
-
const clear = (p, q, loose) => (loose ? this.space.segmentClearOfHoles(p, q, this.clearance) : this.space.segmentClear(p, q, this.clearance));
|
|
234
|
-
if (clear(a, b, outA || outB))
|
|
235
|
-
return { len: dist(a, b), path: [a, b] };
|
|
236
|
-
const key = `${a[0].toFixed(2)},${a[1].toFixed(2)}>${b[0].toFixed(2)},${b[1].toFixed(2)}`;
|
|
322
|
+
const key = `${a[0]},${a[1]}>${b[0]},${b[1]}`;
|
|
237
323
|
const hit = this.cache.get(key);
|
|
238
324
|
if (hit)
|
|
239
325
|
return hit;
|
|
326
|
+
/* Конец вне поля (точка взлёта): его отрезки проверяются только на зоны */
|
|
327
|
+
const outA = !this.space.inField(a);
|
|
328
|
+
const outB = !this.space.inField(b);
|
|
329
|
+
const loose = outA || outB;
|
|
330
|
+
if (loose ? this.space.segmentClearOfHoles(a, b, this.clearance) : this.space.segmentClear(a, b, this.clearance)) {
|
|
331
|
+
const direct = { len: dist(a, b), path: [a, b] };
|
|
332
|
+
this.cache.set(key, direct);
|
|
333
|
+
return direct;
|
|
334
|
+
}
|
|
240
335
|
const n = this.nodes.length;
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (;;) {
|
|
255
|
-
let u = -1;
|
|
256
|
-
for (let i = 0; i < total; i++)
|
|
257
|
-
if (!done[i] && best[i] < Infinity && (u === -1 || best[i] < best[u]))
|
|
258
|
-
u = i;
|
|
259
|
-
if (u === -1 || u === n + 1)
|
|
260
|
-
break;
|
|
261
|
-
done[u] = true;
|
|
262
|
-
for (let v = 0; v < total; v++) {
|
|
263
|
-
if (done[v] || v === u)
|
|
336
|
+
const visA = this.visibility(a, outA);
|
|
337
|
+
const visB = this.visibility(b, outB);
|
|
338
|
+
/* Лучший путь: от a к видимому узлу i, по графу к узлу j, от j к b */
|
|
339
|
+
let len = Infinity;
|
|
340
|
+
let bi = -1;
|
|
341
|
+
let bj = -1;
|
|
342
|
+
for (let i = 0; i < n; i++) {
|
|
343
|
+
const ai = visA[i];
|
|
344
|
+
if (ai === Infinity)
|
|
345
|
+
continue;
|
|
346
|
+
for (let j = 0; j < n; j++) {
|
|
347
|
+
const jb = visB[j];
|
|
348
|
+
if (jb === Infinity)
|
|
264
349
|
continue;
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
350
|
+
const c = ai + this.far[i * n + j] + jb;
|
|
351
|
+
if (c < len) {
|
|
352
|
+
len = c;
|
|
353
|
+
bi = i;
|
|
354
|
+
bj = j;
|
|
269
355
|
}
|
|
270
356
|
}
|
|
271
357
|
}
|
|
272
358
|
let res;
|
|
273
|
-
if (
|
|
359
|
+
if (len === Infinity)
|
|
274
360
|
res = { len: Infinity, path: [a, b] };
|
|
275
361
|
else {
|
|
276
|
-
const path = [];
|
|
277
|
-
for (let v =
|
|
278
|
-
path.
|
|
279
|
-
|
|
362
|
+
const path = [a];
|
|
363
|
+
for (let v = bi;; v = this.next[v * n + bj]) {
|
|
364
|
+
path.push(this.nodes[v]);
|
|
365
|
+
if (v === bj)
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
path.push(b);
|
|
369
|
+
res = { len, path };
|
|
280
370
|
}
|
|
281
371
|
this.cache.set(key, res);
|
|
282
372
|
return res;
|
|
@@ -340,16 +430,18 @@ const orderCells = (cells, cost, start) => {
|
|
|
340
430
|
for (let i = 0; i < n; i++)
|
|
341
431
|
for (let k = 0; k < 4; k++)
|
|
342
432
|
dp[at(1 << i, i, k)] = startCost(i, k);
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
433
|
+
/* Стоимости всех переходов «выход варианта k ячейки i - вход варианта l ячейки j» заранее, числами: перебор обращается к ним сотни тысяч раз */
|
|
434
|
+
const m = n * 4;
|
|
435
|
+
const trans = new Float64Array(m * m);
|
|
436
|
+
for (let i = 0; i < n; i++)
|
|
437
|
+
for (let k = 0; k < 4; k++)
|
|
438
|
+
for (let j = 0; j < n; j++) {
|
|
439
|
+
if (j === i)
|
|
440
|
+
continue;
|
|
441
|
+
for (let l = 0; l < 4; l++)
|
|
442
|
+
trans[(i * 4 + k) * m + j * 4 + l] = cost(exits[i][k], entries[j][l]);
|
|
443
|
+
}
|
|
444
|
+
const tc = (i, k, j, l) => trans[(i * 4 + k) * m + j * 4 + l];
|
|
353
445
|
for (let mask = 1; mask < full; mask++) {
|
|
354
446
|
for (let i = 0; i < n; i++) {
|
|
355
447
|
if (!(mask & (1 << i)))
|
|
@@ -769,7 +861,14 @@ export const searchFieldAngle = async (contour, holes, spacing, margin, speed, o
|
|
|
769
861
|
cache.set(key, c);
|
|
770
862
|
return c;
|
|
771
863
|
};
|
|
772
|
-
|
|
864
|
+
/* Страница получает управление, когда расчёты заняли больше 12 мс: так она успевает отрисоваться, а лишних пауз между быстрыми расчётами нет */
|
|
865
|
+
let lastYield = performance.now();
|
|
866
|
+
const yieldUi = async () => {
|
|
867
|
+
if (performance.now() - lastYield < 12)
|
|
868
|
+
return;
|
|
869
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
870
|
+
lastYield = performance.now();
|
|
871
|
+
};
|
|
773
872
|
/* Направления сторон поля: вдоль длинной стороны проходы часто ложатся лучше всего */
|
|
774
873
|
const kLon = Math.cos(((contour[0]?.[1] ?? 0) * Math.PI) / 180);
|
|
775
874
|
const edges = contour.map((p, i) => {
|
|
@@ -1,25 +1,37 @@
|
|
|
1
|
-
/* По умолчанию: три поля под Минском: большое с рощей, опорой ЛЭП и постройкой; среднее
|
|
1
|
+
/* По умолчанию: три поля под Минском: большое сложной формы с прорезью, выступами, рощей, опорой ЛЭП и постройкой; среднее с выступами, крюком и прудом; малое с вырезом и деревом. Между полями большая треугольная зона, внизу зона, задевающая все три поля; перелёты их обходят; взлёт ниже полей */
|
|
2
2
|
export const DEFAULT_MISSION_PLAN = {
|
|
3
3
|
fields: [
|
|
4
4
|
{
|
|
5
|
-
angle:
|
|
5
|
+
angle: 75,
|
|
6
6
|
contour: [
|
|
7
|
-
[27.
|
|
8
|
-
[27.
|
|
9
|
-
[27.
|
|
10
|
-
[27.
|
|
11
|
-
[27.
|
|
12
|
-
[27.
|
|
13
|
-
[27.
|
|
14
|
-
[27.
|
|
15
|
-
[27.
|
|
16
|
-
[27.
|
|
17
|
-
[27.
|
|
18
|
-
[27.
|
|
19
|
-
[27.
|
|
20
|
-
[27.
|
|
21
|
-
[27.
|
|
22
|
-
[27.
|
|
7
|
+
[27.458785, 53.889681],
|
|
8
|
+
[27.45957, 53.889867],
|
|
9
|
+
[27.460165, 53.889824],
|
|
10
|
+
[27.460868, 53.889354],
|
|
11
|
+
[27.460523, 53.888778],
|
|
12
|
+
[27.460882, 53.888508],
|
|
13
|
+
[27.461045, 53.888674],
|
|
14
|
+
[27.460807, 53.888838],
|
|
15
|
+
[27.461687, 53.8889],
|
|
16
|
+
[27.462012, 53.888625],
|
|
17
|
+
[27.461701, 53.88826],
|
|
18
|
+
[27.459938, 53.888278],
|
|
19
|
+
[27.459929, 53.88841],
|
|
20
|
+
[27.460308, 53.888531],
|
|
21
|
+
[27.46017, 53.888676],
|
|
22
|
+
[27.459823, 53.888826],
|
|
23
|
+
[27.459879, 53.888559],
|
|
24
|
+
[27.459242, 53.888732],
|
|
25
|
+
[27.459251, 53.888501],
|
|
26
|
+
[27.458508, 53.888435],
|
|
27
|
+
[27.458564, 53.888951],
|
|
28
|
+
[27.458855, 53.888927],
|
|
29
|
+
[27.45885, 53.889313],
|
|
30
|
+
[27.459691, 53.889499],
|
|
31
|
+
[27.460092, 53.889372],
|
|
32
|
+
[27.460176, 53.889514],
|
|
33
|
+
[27.459537, 53.889653],
|
|
34
|
+
[27.458855, 53.889425],
|
|
23
35
|
],
|
|
24
36
|
},
|
|
25
37
|
{
|
|
@@ -45,7 +57,7 @@ export const DEFAULT_MISSION_PLAN = {
|
|
|
45
57
|
],
|
|
46
58
|
},
|
|
47
59
|
{
|
|
48
|
-
angle:
|
|
60
|
+
angle: 8.9,
|
|
49
61
|
contour: [
|
|
50
62
|
[27.463255, 53.8891],
|
|
51
63
|
[27.463825, 53.8891],
|
|
@@ -85,22 +97,22 @@ export const DEFAULT_MISSION_PLAN = {
|
|
|
85
97
|
[27.462455, 53.889153],
|
|
86
98
|
],
|
|
87
99
|
[
|
|
88
|
-
[27.
|
|
89
|
-
[27.
|
|
90
|
-
[27.
|
|
91
|
-
[27.
|
|
92
|
-
[27.
|
|
93
|
-
[27.462074, 53.888704],
|
|
100
|
+
[27.460383, 53.889936],
|
|
101
|
+
[27.461291, 53.889961],
|
|
102
|
+
[27.46183, 53.889012],
|
|
103
|
+
[27.462074, 53.888963],
|
|
104
|
+
[27.462074, 53.888711],
|
|
94
105
|
],
|
|
95
106
|
[
|
|
96
|
-
[27.
|
|
97
|
-
[27.
|
|
98
|
-
[27.
|
|
99
|
-
[27.
|
|
100
|
-
[27.
|
|
101
|
-
[27.
|
|
102
|
-
[27.
|
|
103
|
-
[27.
|
|
107
|
+
[27.461499, 53.888386],
|
|
108
|
+
[27.46197, 53.888493],
|
|
109
|
+
[27.462054, 53.8886],
|
|
110
|
+
[27.462222, 53.888666],
|
|
111
|
+
[27.463063, 53.888683],
|
|
112
|
+
[27.462948, 53.88841],
|
|
113
|
+
[27.463299, 53.888323],
|
|
114
|
+
[27.463287, 53.888204],
|
|
115
|
+
[27.461499, 53.888212],
|
|
104
116
|
],
|
|
105
117
|
[
|
|
106
118
|
[27.463657, 53.888499],
|
|
@@ -109,7 +121,7 @@ export const DEFAULT_MISSION_PLAN = {
|
|
|
109
121
|
[27.463657, 53.888376],
|
|
110
122
|
],
|
|
111
123
|
],
|
|
112
|
-
start: [27.
|
|
124
|
+
start: [27.459408, 53.88832],
|
|
113
125
|
spacing: 5,
|
|
114
126
|
margin: 2,
|
|
115
127
|
alt: 4,
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
/* Ключ подсказки элемента под курсором; текст берётся из tips - обновляется вместе с данными */
|
|
316
316
|
let tip: string | null = $state(null)
|
|
317
317
|
/* Подсказки включаются кнопкой «?» в шапке - выбор общий для всех PFD в браузере (hints.ts) */
|
|
318
|
-
const hints = hintsSetting("WidgetPFD")
|
|
318
|
+
const hints = $derived(hintsSetting("WidgetPFD", persistKey))
|
|
319
319
|
$effect(() => {
|
|
320
320
|
if (!$hints) tip = null
|
|
321
321
|
})
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
let { wrapperClass = "", componentClass = "", label = {}, value, collapsed = $bindable(false), persistKey, onChange, onSave }: IWidgetRGBStripProps = $props()
|
|
18
18
|
|
|
19
19
|
/* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetRGBStrip */
|
|
20
|
-
const hints = hintsSetting("WidgetRGBStrip")
|
|
20
|
+
const hints = $derived(hintsSetting("WidgetRGBStrip", persistKey))
|
|
21
21
|
|
|
22
22
|
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
|
23
23
|
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
/* Текущая занятость - мастер-цвет, пик занятости с момента старта (Total − MinFree) - его светлая подложка */
|
|
63
63
|
/* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetStackInfo */
|
|
64
|
-
const hints = hintsSetting("WidgetStackInfo")
|
|
64
|
+
const hints = $derived(hintsSetting("WidgetStackInfo", persistKey))
|
|
65
65
|
|
|
66
66
|
let heaps = $derived(
|
|
67
67
|
(value?.Heap ?? []).map((h) => {
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
## Ключи
|
|
18
18
|
|
|
19
|
-
| Ключ
|
|
20
|
-
|
|
21
|
-
| `WiFiMode`
|
|
22
|
-
| `StaSSID`, `StaPSK`
|
|
23
|
-
| `StaticIP`
|
|
24
|
-
| `StaIP`, `StaMS`, `StaGW` | Адрес, маска и шлюз в сети роутера
|
|
25
|
-
| `ApSSID`, `ApPSK`
|
|
26
|
-
| `ApIP`, `ApMS`, `ApGW`
|
|
19
|
+
| Ключ | Описание |
|
|
20
|
+
| ------------------------- | ----------------------------------------------------------- |
|
|
21
|
+
| `WiFiMode` | Режим: `0` выключен, `1` клиент, `2` точка доступа, `3` оба |
|
|
22
|
+
| `StaSSID`, `StaPSK` | Сеть роутера и её пароль |
|
|
23
|
+
| `StaticIP` | `0` DHCP, `1` статический адрес |
|
|
24
|
+
| `StaIP`, `StaMS`, `StaGW` | Адрес, маска и шлюз в сети роутера |
|
|
25
|
+
| `ApSSID`, `ApPSK` | Имя и пароль своей точки доступа |
|
|
26
|
+
| `ApIP`, `ApMS`, `ApGW` | Адрес, маска и шлюз своей точки доступа |
|
|
27
27
|
|
|
28
28
|
## Особенности
|
|
29
29
|
|
|
@@ -34,26 +34,26 @@
|
|
|
34
34
|
- Переход в режим «только точка доступа» отключает подключение к роутеру. Поэтому при сохранении такого режима появляется предупреждение (`confirmOnAP`). На само переключение режима предупреждения нет.
|
|
35
35
|
- Мастер-цвет (`componentClass`) красит значок, переключатели, рамку фокуса и шапку таблиц в виджете.
|
|
36
36
|
- Шапка сворачивает виджет. Свёрнутое состояние по умолчанию задаётся в конструкторе (`collapsed`), а с `persistKey` личный выбор пользователя запоминается в браузере.
|
|
37
|
-
- Кнопка «?» в шапке включает подсказки к неочевидным полям. Выбор
|
|
37
|
+
- Кнопка «?» в шапке включает подсказки к неочевидным полям. Выбор запоминается в браузере для каждого виджета отдельно, по его `persistKey`. Без `persistKey` выбор общий для всех виджетов этого типа.
|
|
38
38
|
- Сам виджет с устройством не говорит. Читает и пишет поля приложение (`GUIPreview.svelte` в облаке и WebApp).
|
|
39
39
|
|
|
40
40
|
## Пропсы
|
|
41
41
|
|
|
42
|
-
| Название
|
|
43
|
-
|
|
44
|
-
| `id`
|
|
45
|
-
| `keys`
|
|
46
|
-
| `value`
|
|
47
|
-
| `label.name`
|
|
48
|
-
| `wrapperClass`
|
|
49
|
-
| `componentClass` | `string`
|
|
50
|
-
| `allowedModes`
|
|
51
|
-
| `confirmOnAP`
|
|
52
|
-
| `scanCommand`
|
|
53
|
-
| `collapsed`
|
|
54
|
-
| `persistKey`
|
|
55
|
-
| `onScan`
|
|
56
|
-
| `onSave`
|
|
42
|
+
| Название | Тип | По умолчанию | Описание |
|
|
43
|
+
| ---------------- | -------------------------------------- | ---------------- | ------------------------------------------------- |
|
|
44
|
+
| `id` | `string` | `"CFG"` | Префикс группы полей |
|
|
45
|
+
| `keys` | `Partial<Record<ключ, string>>` | имена ключей | Другие имена полей, если у прошивки иначе |
|
|
46
|
+
| `value` | `IWidgetWiFiConfig` | | Текущие значения полей |
|
|
47
|
+
| `label.name` | `string` | `"WiFi"` | Заголовок. Пусто - название на языке пользователя |
|
|
48
|
+
| `wrapperClass` | `string` | `""` | Классы обёртки |
|
|
49
|
+
| `componentClass` | `string` | `""` | Мастер-цвет `bg-*` |
|
|
50
|
+
| `allowedModes` | `number[]` | `[1, 2, 3]` | Какие режимы можно выбрать |
|
|
51
|
+
| `confirmOnAP` | `boolean` | `true` | Предупреждать при сохранении режима точки доступа |
|
|
52
|
+
| `scanCommand` | `{ header, argument }` | `GET`, `APsList` | Команда поиска сетей |
|
|
53
|
+
| `collapsed` | `boolean` | `false` | Свёрнут ли. Можно связать через `bind:collapsed` |
|
|
54
|
+
| `persistKey` | `string` | | Ключ для запоминания свёрнутого состояния |
|
|
55
|
+
| `onScan` | `() => Promise<{ id, name, value }[]>` | | Найти сети. `name` подпись в списке, `value` SSID |
|
|
56
|
+
| `onSave` | `(config) => void` | | Нажата «Сохранить», в `config` изменённые поля |
|
|
57
57
|
|
|
58
58
|
## Пример
|
|
59
59
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}: IWidgetWiFiProps = $props()
|
|
30
30
|
|
|
31
31
|
/* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetWiFi */
|
|
32
|
-
const hints = hintsSetting("WidgetWiFi")
|
|
32
|
+
const hints = $derived(hintsSetting("WidgetWiFi", persistKey))
|
|
33
33
|
|
|
34
34
|
/* "Мастер-цвет" виджета - переопределяет --accent-color/--accent-soft на корне, дальше это штатный каскад переменных, на котором и так построены компоненты библиотеки: значок, режимы STA/AP, фокус-кольцо у полей и кнопка "Сохранить" (маркер bg-accent) перекрашиваются вместе, одним источником истины; красной остаётся только кнопка подтверждения режима AP. */
|
|
35
35
|
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
package/dist/hints.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Writable } from "svelte/store";
|
|
2
|
-
export declare const hintsSetting: (widget: string) => Writable<boolean>;
|
|
2
|
+
export declare const hintsSetting: (widget: string, instanceKey?: string) => Writable<boolean>;
|
|
3
3
|
export declare function hideHint(): void;
|
|
4
4
|
export type HintParams = {
|
|
5
5
|
text?: string;
|
package/dist/hints.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/* $lib/hints.ts */
|
|
2
|
-
/* Подсказки смарт-виджетов: у каждого виджета своя кнопка «?» в шапке, выбор запоминается в браузере для
|
|
2
|
+
/* Подсказки смарт-виджетов: у каждого виджета своя кнопка «?» в шапке, выбор запоминается в браузере для этого экземпляра (по persistKey) или, без него, для всех виджетов типа; элемент получает подсказку действием use:hint={{ text, enabled }} - всплывающее окно над ним в стиле темы */
|
|
3
3
|
import { writable } from "svelte/store";
|
|
4
4
|
/* Другие ключи хранения, из которых читается выбор оператора */
|
|
5
5
|
const LEGACY_KEYS = { WidgetPFD: "WidgetPFD.tips" };
|
|
6
6
|
const settings = new Map();
|
|
7
|
-
/* Настройка подсказок виджета: выключены по
|
|
8
|
-
export const hintsSetting = (widget) => {
|
|
9
|
-
const
|
|
7
|
+
/* Настройка подсказок виджета: выключены по умолчанию; с instanceKey (persistKey виджета) у каждого экземпляра своя, ключ WidgetHints.<тип>:<экземпляр>, без него - одна на тип, ключ WidgetHints.<тип>; в браузер пишется только выбор оператора (нажатие «?»); ключи Hints.<тип> выбором не являются и удаляются */
|
|
8
|
+
export const hintsSetting = (widget, instanceKey) => {
|
|
9
|
+
const id = instanceKey ? `${widget}:${instanceKey}` : widget;
|
|
10
|
+
const cached = settings.get(id);
|
|
10
11
|
if (cached)
|
|
11
12
|
return cached;
|
|
12
|
-
const storageKey = `WidgetHints.${
|
|
13
|
+
const storageKey = `WidgetHints.${id}`;
|
|
13
14
|
let initial = false;
|
|
14
15
|
try {
|
|
15
16
|
if (typeof localStorage !== "undefined") {
|
|
16
17
|
localStorage.removeItem(`Hints.${widget}`);
|
|
17
|
-
const saved = localStorage.getItem(storageKey) ?? localStorage.getItem(LEGACY_KEYS[widget] ?? "");
|
|
18
|
+
const saved = localStorage.getItem(storageKey) ?? (instanceKey ? null : localStorage.getItem(LEGACY_KEYS[widget] ?? ""));
|
|
18
19
|
if (saved !== null)
|
|
19
20
|
initial = saved === "1";
|
|
20
21
|
}
|
|
@@ -37,7 +38,7 @@ export const hintsSetting = (widget) => {
|
|
|
37
38
|
/* хранилище недоступно - выбор действует до перезагрузки */
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
|
-
settings.set(
|
|
41
|
+
settings.set(id, store);
|
|
41
42
|
return store;
|
|
42
43
|
};
|
|
43
44
|
/* Одно всплывающее окно на страницу: создаётся при первом показе, позиционируется над элементом (или под ним, если сверху нет места) */
|
|
@@ -121,9 +121,9 @@ const translations = {
|
|
|
121
121
|
zh: "🎯 **用途**\n主飞行显示器(PFD) - 飞行监控的主要仪表:姿态、速度、高度、航向和系统状态。\n\n📦 **数据包**\n- `MAV` 分组的字段 - MAVLink 自驾仪遥测(可通过设置中的前缀更改),无需绑定变量\n- 遥测流:`Roll`、`Pitch`、`Heading`、`Load`、`Slip` 约每秒 10-20 次,其余每秒 1-2 次\n- 仅显示,不发送任何数据\n\n🔑 **键**\n- `Roll`、`Pitch` - 数字,°:坡度(向右 +)和俯仰(向上 +)\n- `Heading` - 数字,0-360°:航向\n- `GSpeed`、`ASpeed` - 数字,m/s:地速和空速\n- `Alt` - 数字,m:相对起飞点高度;`AltMSL` - 海拔高度\n- `Climb` - 数字,m/s:升降速度(向上 +)\n- `Load` - 数字,g:法向过载(1 - 平飞);`Slip` - 侧向过载,g(向右 +);非数字 - 不显示 G 和侧滑球\n- `Thr` - 数字,%:油门\n- `WindDir`、`WindSpd` - 风:来向,°,和风速,m/s\n- `Lat`、`Lon` - 数字,°:坐标\n- `Vx`、`Vy` - 数字,m/s:向北和向东的速度 - 用于航迹\n- `Fix` - 数字:GPS 定位类型,`0`-`1` 无、`2` 2D、`3` 3D、`4`-`6` DGPS/RTK;`Sat` - 卫星数\n- `BV` - V;`BC` - A(`-1` 未测量);`BRem` - %(`-1` 未知)\n- `Mode` - 数字:自驾仪模式;`VType` - 机型(决定模式名称);`Armed` - `1` 已解锁\n- `HomeLat`、`HomeLon` - 数字,°:起飞点,`0` - 未知\n- `Press` - hPa,`Temp` - °C\n- `WPNum` - 航点编号(`0` - 无任务);`WPDist`、`WPBrg` - 到航点的距离,m,和方位,°\n- `AltErr`、`SpdErr` - 目标减当前:高度,m,空速,m/s;`XTE` - 偏航距离,m\n- `RSSI`、`RemRSSI` - 本地和远端信号,%(`-1` 未知);`Drop` - 丢包率,%\n- `HDOP` - GPS 精度(1 以下优秀,大于 2 不准确);`EKF` - 导航滤波器方差(0.5 起注意,0.8 起告警);`SensErr` - 故障传感器数\n- `Msg`、`MsgSev`、`MsgSeq` - 自驾仪消息、严重程度(`0`-`3` 错误、`4` 警告、`5`-`7` 信息)、编号\n\n✨ **特点**\n- 数据包之间的姿态经过平滑\n- 将鼠标悬停在仪表或行中的任意元素上 - 在地平线底部的信息行中显示含义和当前值;可通过标题栏中的「?」按钮关闭\n- 速度带和高度带下方的角落:空速(相对空气)和升降速度(+ 爬升、− 下降)\n- 地平线上方两角:`G` - 过载(超出限值为红色,最小和最大值见提示)和 `THR` - 油门\n- 风 - 位于 `G` 下方:箭头为相对机头的风吹去方向(向下 - 逆风,向上 - 顺风),旁边为风速\n- 「航线」组显示按当前地速到航点的时间\n- 坡度指针下方的侧滑球:居中 - 无侧滑,偏离 - 侧滑或外滑(方向舵朝球的方向)\n- 速度带上的黄色箭头 - 趋势:以当前加速度 10 秒后地速将到达的位置\n- 航向带上的粉色 ▲ 为航迹:载具相对地面的实际运动方向(由 `Vx`、`Vy` 计算);绿色 ● - 起飞点方向;仪表下方有带角度的图例\n- 超过设定时间无数据 - 仪表被划叉并标记「无数据」\n- 在窄单元格中,速度带和高度带变为地平线上的数字\n- 行已分组:上方「系统 / GPS / 链路 / 电池」 - 状态,下方「航线 / 起飞点 / 航迹 / 位置 / 空气」 - 去向与位置\n- 青色标记 - 当前航点的目标:速度带和高度带上的速度与高度,航向带上的方位 ◆\n- 自驾仪消息 - 在地平线底部同一信息行显示 10 秒,颜色按严重程度;悬停该行可查看历史\n- 键 `Lat`、`Lon`、`Alt`、`Heading`、`Sat` 与「地图」通用 - 一个分组同时供两个组件使用\n\n⚙️ **设置**\n- 分组前缀(ID)、主色\n- 限值:最小/最大速度和最大高度 - 速度带和高度带上的红色区域;最大坡度 - 红色弧线;最小/最大过载 - G 值变红;低电量;「无数据」前的时间",
|
|
122
122
|
},
|
|
123
123
|
"component.info.WidgetMap": {
|
|
124
|
-
ru: "🎯 **Назначение**\nКарта объектов в реальном времени: положение, курс и траектория каждого объекта. Два других режима строят миссию: «Маршрут» - планировщик из точек, «Агро» - генератор миссии опрыскивания для агродрона.\n\n📦 **Пакет данных**\n- Поля групп устройства, по группе на объект: по умолчанию `MAV`, несколько - через пробел (`MAV GNSS`); привязывать переменные не нужно\n- Поток телеметрии: координаты - раз в 0.5-1 с, имя и серийный номер - реже\n- «Объекты» только показывают; «Маршрут» и «Агро» отдают миссию наружу через `onSend`, план хранит владелец карты (`route` / `plan` и их колбэки)\n\n🔑 **Ключи**\n- `Lat` - число, °: широта, обязательный; `Lat` и `Lon` = 0 - нет фикса, объект не рисуется\n- `Lon` - число, °: долгота, обязательный\n- `Alt` - число, м: высота\n- `Heading` - число, 0-360°: курс, поворачивает маркер\n- `Sat` - число спутников\n- `DevName` - строка: имя объекта, подпись маркера; без него - имя группы\n- `DevSN` - строка: серийный номер\n- `WPNum` - номер текущей команды миссии; нужен только «Маршруту» и «Агро» для прогресса в шапке\n\n✨ **Особенности**\n- Ключи общие с «Пилотажным дисплеем» и «Об устройстве» - один пакет питает несколько виджетов\n- Клик по объекту - карточка со всеми значениями, кнопки «Копировать», «Следить» и «Скрыть»\n- Траекторию, число её точек и таймаут маркеров оператор переключает на карте; объект без новых данных краснеет, ещё через таймаут скрывается\n- «Показать все» в шапке вписывает объекты в экран; сама карта вид не меняет\n- В шапке - сколько объектов на связи из заданных\n- Только 2D: правой кнопкой мыши карта поворачивается, компас под зумом возвращает её на север\n- Мастер-цвет красит весь виджет: кнопки, линию маршрута, точки, точку взлёта, переходы и аппарат в имитации. Цвета со смыслом не меняются: поля зелёные, запретные зоны красные, развороты оранжевые; «Очистить», удаление и ошибка отправки красные\n\n⚙️ **Настройки**\n- Группы полей, мастер-цвет\n- Высота карты `S`/`M`/`L`/`XL`, иконка маркера\n- Начальные значения: траектория и число её точек\n- Режим: «Объекты», «Маршрут» или «Агро»\n- Режим имитации: шесть аппаратов в «Объектах», полёт по траектории в «Маршруте» и «Агро»; панель имитации внизу карты по центру\n\n📍 **Режим «Маршрут»**\n- Обычный планировщик: точки по порядку облёта, у каждой своя высота и скорость\n- «Редактор точек» в левом верхнем углу карты включает правку: клик по карте добавляет точку, точки перетаскиваются, середина отрезка вставляет точку, правый клик удаляет; «Готово» или Esc заканчивают правку; без правки видны только номера\n- Высота, скорость и удаление - в списке точек блока «Итог», порядок меняется перетаскиванием строки; возврат с посадкой (RTL)\n- Миссия QGC WPL 110: взлёт, точки, RTL; «Скачать», «Копировать», «Отправить»; аппараты из групп видны поверх маршрута\n- Прогресс в шапке: в имитации - по ней, иначе по `WPNum` и положению аппарата первой группы\n\n🌾 **Режим «Агро»**\n- Одно или несколько полей и запретные зоны на карте превращаются в траекторию опрыскивания\n- «Редактор полей» и «Редактор запретных зон» в левом верхнем углу карты: контур обводится кликами, замыкается кликом по первой точке, двойным кликом, Enter или «Замкнуть»; вершины перетаскиваются, середина стороны добавляет точку, правый клик удаляет; точка взлёта перетаскивается всегда\n- Запретная зона может лежать в поле, выходить за его край или стоять вне полей. Из проходов вырезается только её часть внутри поля, площадь поля уменьшается только на эту часть\n- ⚠️ Перелёты от точки взлёта к полю, между полями и домой обходят все зоны с отступом. Опоры ЛЭП, вышки, деревья и постройки между полями обязательно обводите зонами, иначе аппарат полетит напрямую и может в них врезаться\n- Проходы с шагом не больше ширины захвата; крайние проходы на отступе от края, от зон тоже держится отступ; по умолчанию захват 5 м, отступ 2 м\n- Зоны и вырезы делят поле на ячейки; порядок ячеек до 10 штук перебирается полностью, больше - от ближайшей с улучшением 2-opt; переходы и развороты идут по кратчайшему пути в обход зон\n- Поля облетаются по очереди от ближайшего, у каждого поля свой угол проходов\n- «Авторасчёт» подбирает угол каждого поля по полной траектории: цена - время облёта плюс 4 с на разворот плюс штраф за недолив (метр недолитого прохода как три метра полёта). Поиск: шаг 5° и направления сторон поля, затем 1° около трёх лучших, затем 0.1° около победителя; около 75 расчётов на поле, прогресс на карте. Кнопка «i» рядом открывает оператору подробную справку\n- Покрытие меньше 100 % в основном из-за отступа от границ и зон\n- Распылитель включается на проходах и выключается на разворотах и переходах: `DO_SPRAYER` или `DO_SET_SERVO`\n- Миссия QGC WPL 110: «Скачать», «Копировать», «Отправить» на борт; аппараты из групп видны поверх поля\n- В имитации квадрокоптер облетает проходы, зелёный во время распыления\n- Без `plan` открывается демо: три поля под Минском с зонами внутри полей, на их краях и между полями",
|
|
125
|
-
en: '🎯 **Purpose**\nA real-time map of objects: position, heading and track of each object. Two other modes build a mission: "Route" is a waypoint planner, "Agro" is a spraying mission generator for an agricultural drone.\n\n📦 **Data packet**\n- Device field groups, one group per object: `MAV` by default, several separated by spaces (`MAV GNSS`); no variable binding needed\n- Telemetry stream: coordinates every 0.5-1 s, name and serial number less often\n- "Objects" only displays; "Route" and "Agro" pass the mission out through `onSend`, the plan is kept by the map owner (`route` / `plan` and their callbacks)\n\n🔑 **Keys**\n- `Lat` - number, °: latitude, required; `Lat` and `Lon` = 0 - no fix, the object is not drawn\n- `Lon` - number, °: longitude, required\n- `Alt` - number, m: altitude\n- `Heading` - number, 0-360°: heading, rotates the marker\n- `Sat` - satellite count\n- `DevName` - string: object name, the marker label; without it - the group name\n- `DevSN` - string: serial number\n- `WPNum` - number of the current mission command; only "Route" and "Agro" use it for progress in the header\n\n✨ **Features**\n- Keys are shared with the "Flight Display" and "About Device" - one packet feeds several widgets\n- Click an object - a card with all values, "Copy", "Follow" and "Hide" buttons\n- The operator switches the track, its point count and marker timeout on the map; an object without new data turns red, after one more timeout it is hidden\n- "Show all" in the header fits all objects into view; the map never moves by itself\n- The header shows how many of the configured objects are online\n- 2D only: the right mouse button rotates the map, the compass under the zoom returns it to north\n- The master color paints the whole widget: buttons, route line, points, takeoff point, transits and the vehicle in simulation. Meaningful colors stay: fields green, no-fly zones red, turns orange; "Clear", delete and send error red\n\n⚙️ **Settings**\n- Field groups, master color\n- Map height `S`/`M`/`L`/`XL`, marker icon\n- Defaults: track and its point count\n- Mode: "Objects", "Route" or "Agro"\n- Simulation mode: six vehicles in "Objects", a flight along the path in "Route" and "Agro"; the simulation panel is at the bottom center of the map\n\n📍 **Route mode**\n- A regular planner: points in flight order, each with its own altitude and speed\n- "Waypoint editor" in the top left corner of the map turns on editing: a click adds a point, points can be dragged, a segment midpoint inserts one, right-click removes; "Done" or Esc ends editing; otherwise only numbers are shown\n- Altitude, speed and removal in the point list of the Summary block, order is changed by dragging a row; return and land (RTL)\n- QGC WPL 110 mission: takeoff, points, RTL; "Download", "Copy", "Send"; vehicles from the groups are shown over the route\n- Progress in the header: from the simulation, otherwise from `WPNum` and the position of the vehicle from the first group\n\n🌾 **Agro mode**\n- One or more fields and no-fly zones on the map become a spraying path\n- "Field editor" and "No-fly zone editor" in the top left corner of the map: an outline is traced by clicks and closed by clicking the first point, a double click, Enter or "Close"; vertices can be dragged, a side midpoint adds a point, right-click removes; the takeoff point can always be dragged\n- A no-fly zone can lie inside a field, stick out over its edge or stand outside fields. Only its part inside the field is cut from the passes, and the field area is reduced only by that part\n- ⚠️ Transits from takeoff to a field, between fields and back home avoid all zones with the margin. Be sure to outline power line poles, towers, trees and buildings between fields as zones, otherwise the vehicle flies straight and may crash into them\n- Passes are spaced no wider than the swath; the outer passes keep the margin from the edge, zones are kept at the margin too; defaults: swath 5 m, margin 2 m\n- Zones and notches split a field into cells; up to 10 cells their order is checked exhaustively, beyond that nearest first with 2-opt improvement; transits and turns follow the shortest path around zones\n- Fields are flown one after another, nearest first, each field has its own pass angle\n- "Auto-calculate" picks each field\'s angle from the full path: cost is flight time plus 4 s per turn plus a penalty for missed spraying (a missed meter of pass counts as three meters of flight). Search: 5° steps and field side directions, then 1° around the three best, then 0.1° around the winner; about 75 calculations per field, progress on the map. The "i" button next to it opens a detailed explanation for the operator\n- Coverage below 100 % is mostly due to the margin from boundaries and zones\n- The sprayer is on during passes and off on turns and transits: `DO_SPRAYER` or `DO_SET_SERVO`\n- QGC WPL 110 mission: "Download", "Copy", "Send" to the vehicle; vehicles from the groups are shown over the field\n- In simulation a quadcopter flies the passes, green while spraying\n- Without `plan` a demo opens: three fields near Minsk with zones inside fields, on their edges and between fields',
|
|
126
|
-
zh: "🎯 **用途**\n实时对象地图:每个对象的位置、航向和轨迹。另外两个模式用于生成任务:「航线」是航点规划器,「农业」是农业无人机喷洒任务生成器。\n\n📦 **数据包**\n- 设备字段分组,每个分组一个对象:默认 `MAV`,多个用空格分隔(`MAV GNSS`);无需绑定变量\n- 遥测流:坐标每 0.5-1 秒一次,名称和序列号频率更低\n- 「对象」仅显示;「航线」和「农业」通过 `onSend` 输出任务,计划由地图的使用方保存(`route` / `plan` 及其回调)\n\n🔑 **键**\n- `Lat` - 数字,°:纬度,必需;`Lat` 和 `Lon` 为 0 - 无定位,不绘制对象\n- `Lon` - 数字,°:经度,必需\n- `Alt` - 数字,m:高度\n- `Heading` - 数字,0-360°:航向,旋转标记\n- `Sat` - 卫星数\n- `DevName` - 字符串:对象名称,标记标签;缺省时为分组名\n- `DevSN` - 字符串:序列号\n- `WPNum` - 当前任务指令编号;仅「航线」和「农业」用于标题栏中的进度\n\n✨ **特点**\n- 键与「飞行显示器」和「关于设备」通用 - 一个数据包可供多个组件使用\n- 点击对象 - 显示所有值的卡片,带「复制」、「跟随」和「隐藏」按钮\n- 操作员在地图上切换轨迹、轨迹点数和标记超时;无新数据的对象变红,再过一个超时后隐藏\n- 标题栏中的「显示全部」将对象纳入视野;地图本身不会自动移动\n- 标题栏显示已配置对象中在线的数量\n- 仅 2D:按住鼠标右键旋转地图,缩放按钮下方的指南针将其恢复为正北\n- 主色作用于整个组件:按钮、航线、航点、起飞点、转场以及模拟中的飞行器。有含义的颜色保持不变:田块为绿色,禁飞区为红色,转弯为橙色;「清除」、删除和发送错误为红色\n\n⚙️ **设置**\n- 字段分组、主色\n- 地图高度 `S`/`M`/`L`/`XL`、标记图标\n- 默认值:轨迹及其点数\n- 模式:「对象」、「航线」或「农业」\n- 模拟模式:「对象」中六架飞行器,「航线」和「农业」中沿航线飞行;模拟面板位于地图底部中央\n\n📍 **航线模式**\n- 常规规划:按飞行顺序放置航点,每个航点有自己的高度和速度\n- 地图左上角的「航点编辑」开启编辑:点击地图添加航点,航点可拖动,拖动线段中点插入,右键删除;「完成」或 Esc 结束编辑;未开启时仅显示编号\n- 在“汇总”的航点列表中设置高度、速度和删除,拖动行可调整顺序;返航降落 (RTL)\n- QGC WPL 110 任务:起飞、航点、RTL;「下载」、「复制」、「发送」;分组中的飞行器显示在航线上方\n- 标题中的进度:模拟时取自模拟,否则根据第一个分组飞行器的 `WPNum` 和位置\n\n🌾 **农业模式**\n- 地图上的一个或多个田块和禁飞区生成喷洒航线\n- 地图左上角的「田块编辑」和「禁飞区编辑」:点击绘制轮廓,点击第一个点、双击、Enter 或「闭合」完成;顶点可拖动,拖动边的中点添加顶点,右键删除;起飞点可随时拖动\n- 禁飞区可以位于田块内、跨出田块边缘或位于田块外。作业行只去掉其在田块内的部分,田块面积也只减去这部分\n- ⚠️ 从起飞点到田块、田块之间以及返航的转场都会按边距绕开所有禁飞区。务必把田块之间的电力线杆、塔、树木和建筑圈为禁飞区,否则飞行器会直线飞行并可能撞上它们\n- 作业行间距不大于喷幅;最外侧作业行与边缘保持边距,与禁飞区也保持边距;默认喷幅 5 m,边距 2 m\n- 禁飞区和缺口把田块分成若干单元;不超过 10 个单元时完整遍历顺序,更多时从最近的开始并用 2-opt 改进;转场和转弯沿绕开禁飞区的最短路径\n- 田块按由近到远依次作业,每个田块有自己的作业行角度\n- 「自动计算」按完整航线为每个田块选择角度:代价为飞行时间加每次转弯 4 s,再加漏喷惩罚(漏喷一米作业行按三米飞行计算)。搜索:5° 步长并加上田块各边方向,然后在三个最佳附近以 1° 步长,最后在胜出角度附近以 0.1° 步长;每个田块约 75 次计算,进度显示在地图上。旁边的「i」按钮为操作员打开详细说明\n- 覆盖率低于 100 % 主要是由于与边界和禁飞区的边距\n- 喷洒器在作业行开启,在转弯和转场时关闭:`DO_SPRAYER` 或 `DO_SET_SERVO`\n- QGC WPL 110 任务:「下载」、「复制」、「发送」到飞行器;分组中的飞行器显示在田块上方\n- 模拟中四旋翼沿作业行飞行,喷洒时为绿色\n- 未传入 `plan` 时打开演示:明斯克附近三个田块,禁飞区位于田块内、田块边缘以及田块之间",
|
|
124
|
+
ru: "🎯 **Назначение**\nКарта объектов в реальном времени: положение, курс и траектория каждого объекта. Два других режима строят миссию: «Маршрут» - планировщик из точек, «Агро» - генератор миссии опрыскивания для агродрона.\n\n📦 **Пакет данных**\n- Поля групп устройства, по группе на объект: по умолчанию `MAV`, несколько - через пробел (`MAV GNSS`); привязывать переменные не нужно\n- Поток телеметрии: координаты - раз в 0.5-1 с, имя и серийный номер - реже\n- «Объекты» только показывают; «Маршрут» и «Агро» отдают миссию наружу через `onSend`, план хранит владелец карты (`route` / `plan` и их колбэки)\n\n🔑 **Ключи**\n- `Lat` - число, °: широта, обязательный; `Lat` и `Lon` = 0 - нет фикса, объект не рисуется\n- `Lon` - число, °: долгота, обязательный\n- `Alt` - число, м: высота\n- `Heading` - число, 0-360°: курс, поворачивает маркер\n- `Sat` - число спутников\n- `DevName` - строка: имя объекта, подпись маркера; без него - имя группы\n- `DevSN` - строка: серийный номер\n- `WPNum` - номер текущей команды миссии; нужен только «Маршруту» и «Агро» для прогресса в шапке\n\n✨ **Особенности**\n- Ключи общие с «Пилотажным дисплеем» и «Об устройстве» - один пакет питает несколько виджетов\n- Клик по объекту - карточка со всеми значениями, кнопки «Копировать», «Следить» и «Скрыть»\n- Траекторию, число её точек и таймаут маркеров оператор переключает на карте; объект без новых данных краснеет, ещё через таймаут скрывается\n- «Показать все» в шапке вписывает объекты в экран; сама карта вид не меняет\n- В шапке - сколько объектов на связи из заданных\n- Только 2D: правой кнопкой мыши карта поворачивается, компас под зумом возвращает её на север\n- Мастер-цвет красит весь виджет: кнопки, линию маршрута, точки, точку взлёта, переходы и аппарат в имитации. Цвета со смыслом не меняются: поля зелёные, запретные зоны красные, развороты оранжевые; «Очистить», удаление и ошибка отправки красные\n\n⚙️ **Настройки**\n- Группы полей, мастер-цвет\n- Высота карты `S`/`M`/`L`/`XL`, иконка маркера\n- Начальные значения: траектория и число её точек\n- Режим: «Объекты», «Маршрут» или «Агро»\n- Режим имитации: шесть аппаратов в «Объектах», полёт по траектории в «Маршруте» и «Агро»; панель имитации внизу карты по центру\n\n📍 **Режим «Маршрут»**\n- Обычный планировщик: точки по порядку облёта, у каждой своя высота и скорость\n- «Редактор точек» в левом верхнем углу карты включает правку: клик по карте добавляет точку, точки перетаскиваются, середина отрезка вставляет точку, правый клик удаляет; «Готово» или Esc заканчивают правку; без правки видны только номера\n- Высота, скорость и удаление - в списке точек блока «Итог», порядок меняется перетаскиванием строки; возврат с посадкой (RTL)\n- Миссия QGC WPL 110: взлёт, точки, RTL; «Скачать», «Копировать», «Отправить»; аппараты из групп видны поверх маршрута\n- Прогресс в шапке: в имитации - по ней, иначе по `WPNum` и положению аппарата первой группы\n\n🌾 **Режим «Агро»**\n- Одно или несколько полей и запретные зоны на карте превращаются в траекторию опрыскивания\n- «Редактор полей» и «Редактор запретных зон» в левом верхнем углу карты: контур обводится кликами, замыкается кликом по первой точке, двойным кликом, Enter или «Замкнуть»; вершины перетаскиваются, середина стороны добавляет точку, правый клик удаляет; точка взлёта перетаскивается всегда\n- Запретная зона может лежать в поле, выходить за его край или стоять вне полей. Из проходов вырезается только её часть внутри поля, площадь поля уменьшается только на эту часть\n- ⚠️ Перелёты от точки взлёта к полю, между полями и домой обходят все зоны с отступом. Опоры ЛЭП, вышки, деревья и постройки между полями обязательно обводите зонами, иначе аппарат полетит напрямую и может в них врезаться\n- Проходы с шагом не больше ширины захвата; крайние проходы на отступе от края, от зон тоже держится отступ; по умолчанию захват 5 м, отступ 2 м\n- Зоны и вырезы делят поле на ячейки; порядок ячеек до 10 штук перебирается полностью, больше - от ближайшей с улучшением 2-opt; переходы и развороты идут по кратчайшему пути в обход зон\n- Поля облетаются по очереди от ближайшего, у каждого поля свой угол проходов\n- «Авторасчёт» подбирает угол каждого поля по полной траектории: цена - время облёта плюс 4 с на разворот плюс штраф за недолив (метр недолитого прохода как три метра полёта). Поиск: шаг 5° и направления сторон поля, затем 1° около трёх лучших, затем 0.1° около победителя; около 75 расчётов на поле, прогресс на карте. Кнопка «i» рядом открывает оператору подробную справку\n- В блоке «Итог» наведение на поле или зону подсвечивает контур на карте, нажатие на название приближает к нему карту\n- Покрытие меньше 100 % в основном из-за отступа от границ и зон\n- Распылитель включается на проходах и выключается на разворотах и переходах: `DO_SPRAYER` или `DO_SET_SERVO`\n- Миссия QGC WPL 110: «Скачать», «Копировать», «Отправить» на борт; аппараты из групп видны поверх поля\n- В имитации квадрокоптер облетает проходы, зелёный во время распыления\n- Без `plan` открывается демо: три поля под Минском с зонами внутри полей, на их краях и между полями",
|
|
125
|
+
en: '🎯 **Purpose**\nA real-time map of objects: position, heading and track of each object. Two other modes build a mission: "Route" is a waypoint planner, "Agro" is a spraying mission generator for an agricultural drone.\n\n📦 **Data packet**\n- Device field groups, one group per object: `MAV` by default, several separated by spaces (`MAV GNSS`); no variable binding needed\n- Telemetry stream: coordinates every 0.5-1 s, name and serial number less often\n- "Objects" only displays; "Route" and "Agro" pass the mission out through `onSend`, the plan is kept by the map owner (`route` / `plan` and their callbacks)\n\n🔑 **Keys**\n- `Lat` - number, °: latitude, required; `Lat` and `Lon` = 0 - no fix, the object is not drawn\n- `Lon` - number, °: longitude, required\n- `Alt` - number, m: altitude\n- `Heading` - number, 0-360°: heading, rotates the marker\n- `Sat` - satellite count\n- `DevName` - string: object name, the marker label; without it - the group name\n- `DevSN` - string: serial number\n- `WPNum` - number of the current mission command; only "Route" and "Agro" use it for progress in the header\n\n✨ **Features**\n- Keys are shared with the "Flight Display" and "About Device" - one packet feeds several widgets\n- Click an object - a card with all values, "Copy", "Follow" and "Hide" buttons\n- The operator switches the track, its point count and marker timeout on the map; an object without new data turns red, after one more timeout it is hidden\n- "Show all" in the header fits all objects into view; the map never moves by itself\n- The header shows how many of the configured objects are online\n- 2D only: the right mouse button rotates the map, the compass under the zoom returns it to north\n- The master color paints the whole widget: buttons, route line, points, takeoff point, transits and the vehicle in simulation. Meaningful colors stay: fields green, no-fly zones red, turns orange; "Clear", delete and send error red\n\n⚙️ **Settings**\n- Field groups, master color\n- Map height `S`/`M`/`L`/`XL`, marker icon\n- Defaults: track and its point count\n- Mode: "Objects", "Route" or "Agro"\n- Simulation mode: six vehicles in "Objects", a flight along the path in "Route" and "Agro"; the simulation panel is at the bottom center of the map\n\n📍 **Route mode**\n- A regular planner: points in flight order, each with its own altitude and speed\n- "Waypoint editor" in the top left corner of the map turns on editing: a click adds a point, points can be dragged, a segment midpoint inserts one, right-click removes; "Done" or Esc ends editing; otherwise only numbers are shown\n- Altitude, speed and removal in the point list of the Summary block, order is changed by dragging a row; return and land (RTL)\n- QGC WPL 110 mission: takeoff, points, RTL; "Download", "Copy", "Send"; vehicles from the groups are shown over the route\n- Progress in the header: from the simulation, otherwise from `WPNum` and the position of the vehicle from the first group\n\n🌾 **Agro mode**\n- One or more fields and no-fly zones on the map become a spraying path\n- "Field editor" and "No-fly zone editor" in the top left corner of the map: an outline is traced by clicks and closed by clicking the first point, a double click, Enter or "Close"; vertices can be dragged, a side midpoint adds a point, right-click removes; the takeoff point can always be dragged\n- A no-fly zone can lie inside a field, stick out over its edge or stand outside fields. Only its part inside the field is cut from the passes, and the field area is reduced only by that part\n- ⚠️ Transits from takeoff to a field, between fields and back home avoid all zones with the margin. Be sure to outline power line poles, towers, trees and buildings between fields as zones, otherwise the vehicle flies straight and may crash into them\n- Passes are spaced no wider than the swath; the outer passes keep the margin from the edge, zones are kept at the margin too; defaults: swath 5 m, margin 2 m\n- Zones and notches split a field into cells; up to 10 cells their order is checked exhaustively, beyond that nearest first with 2-opt improvement; transits and turns follow the shortest path around zones\n- Fields are flown one after another, nearest first, each field has its own pass angle\n- "Auto-calculate" picks each field\'s angle from the full path: cost is flight time plus 4 s per turn plus a penalty for missed spraying (a missed meter of pass counts as three meters of flight). Search: 5° steps and field side directions, then 1° around the three best, then 0.1° around the winner; about 75 calculations per field, progress on the map. The "i" button next to it opens a detailed explanation for the operator\n- In the Summary block hovering a field or zone highlights its outline on the map, clicking its name zooms the map to it\n- Coverage below 100 % is mostly due to the margin from boundaries and zones\n- The sprayer is on during passes and off on turns and transits: `DO_SPRAYER` or `DO_SET_SERVO`\n- QGC WPL 110 mission: "Download", "Copy", "Send" to the vehicle; vehicles from the groups are shown over the field\n- In simulation a quadcopter flies the passes, green while spraying\n- Without `plan` a demo opens: three fields near Minsk with zones inside fields, on their edges and between fields',
|
|
126
|
+
zh: "🎯 **用途**\n实时对象地图:每个对象的位置、航向和轨迹。另外两个模式用于生成任务:「航线」是航点规划器,「农业」是农业无人机喷洒任务生成器。\n\n📦 **数据包**\n- 设备字段分组,每个分组一个对象:默认 `MAV`,多个用空格分隔(`MAV GNSS`);无需绑定变量\n- 遥测流:坐标每 0.5-1 秒一次,名称和序列号频率更低\n- 「对象」仅显示;「航线」和「农业」通过 `onSend` 输出任务,计划由地图的使用方保存(`route` / `plan` 及其回调)\n\n🔑 **键**\n- `Lat` - 数字,°:纬度,必需;`Lat` 和 `Lon` 为 0 - 无定位,不绘制对象\n- `Lon` - 数字,°:经度,必需\n- `Alt` - 数字,m:高度\n- `Heading` - 数字,0-360°:航向,旋转标记\n- `Sat` - 卫星数\n- `DevName` - 字符串:对象名称,标记标签;缺省时为分组名\n- `DevSN` - 字符串:序列号\n- `WPNum` - 当前任务指令编号;仅「航线」和「农业」用于标题栏中的进度\n\n✨ **特点**\n- 键与「飞行显示器」和「关于设备」通用 - 一个数据包可供多个组件使用\n- 点击对象 - 显示所有值的卡片,带「复制」、「跟随」和「隐藏」按钮\n- 操作员在地图上切换轨迹、轨迹点数和标记超时;无新数据的对象变红,再过一个超时后隐藏\n- 标题栏中的「显示全部」将对象纳入视野;地图本身不会自动移动\n- 标题栏显示已配置对象中在线的数量\n- 仅 2D:按住鼠标右键旋转地图,缩放按钮下方的指南针将其恢复为正北\n- 主色作用于整个组件:按钮、航线、航点、起飞点、转场以及模拟中的飞行器。有含义的颜色保持不变:田块为绿色,禁飞区为红色,转弯为橙色;「清除」、删除和发送错误为红色\n\n⚙️ **设置**\n- 字段分组、主色\n- 地图高度 `S`/`M`/`L`/`XL`、标记图标\n- 默认值:轨迹及其点数\n- 模式:「对象」、「航线」或「农业」\n- 模拟模式:「对象」中六架飞行器,「航线」和「农业」中沿航线飞行;模拟面板位于地图底部中央\n\n📍 **航线模式**\n- 常规规划:按飞行顺序放置航点,每个航点有自己的高度和速度\n- 地图左上角的「航点编辑」开启编辑:点击地图添加航点,航点可拖动,拖动线段中点插入,右键删除;「完成」或 Esc 结束编辑;未开启时仅显示编号\n- 在“汇总”的航点列表中设置高度、速度和删除,拖动行可调整顺序;返航降落 (RTL)\n- QGC WPL 110 任务:起飞、航点、RTL;「下载」、「复制」、「发送」;分组中的飞行器显示在航线上方\n- 标题中的进度:模拟时取自模拟,否则根据第一个分组飞行器的 `WPNum` 和位置\n\n🌾 **农业模式**\n- 地图上的一个或多个田块和禁飞区生成喷洒航线\n- 地图左上角的「田块编辑」和「禁飞区编辑」:点击绘制轮廓,点击第一个点、双击、Enter 或「闭合」完成;顶点可拖动,拖动边的中点添加顶点,右键删除;起飞点可随时拖动\n- 禁飞区可以位于田块内、跨出田块边缘或位于田块外。作业行只去掉其在田块内的部分,田块面积也只减去这部分\n- ⚠️ 从起飞点到田块、田块之间以及返航的转场都会按边距绕开所有禁飞区。务必把田块之间的电力线杆、塔、树木和建筑圈为禁飞区,否则飞行器会直线飞行并可能撞上它们\n- 作业行间距不大于喷幅;最外侧作业行与边缘保持边距,与禁飞区也保持边距;默认喷幅 5 m,边距 2 m\n- 禁飞区和缺口把田块分成若干单元;不超过 10 个单元时完整遍历顺序,更多时从最近的开始并用 2-opt 改进;转场和转弯沿绕开禁飞区的最短路径\n- 田块按由近到远依次作业,每个田块有自己的作业行角度\n- 「自动计算」按完整航线为每个田块选择角度:代价为飞行时间加每次转弯 4 s,再加漏喷惩罚(漏喷一米作业行按三米飞行计算)。搜索:5° 步长并加上田块各边方向,然后在三个最佳附近以 1° 步长,最后在胜出角度附近以 0.1° 步长;每个田块约 75 次计算,进度显示在地图上。旁边的「i」按钮为操作员打开详细说明\n- 在“汇总”中将鼠标悬停在田块或禁飞区上会在地图上高亮其轮廓,点击名称会将地图缩放到该处\n- 覆盖率低于 100 % 主要是由于与边界和禁飞区的边距\n- 喷洒器在作业行开启,在转弯和转场时关闭:`DO_SPRAYER` 或 `DO_SET_SERVO`\n- QGC WPL 110 任务:「下载」、「复制」、「发送」到飞行器;分组中的飞行器显示在田块上方\n- 模拟中四旋翼沿作业行飞行,喷洒时为绿色\n- 未传入 `plan` 时打开演示:明斯克附近三个田块,禁飞区位于田块内、田块边缘以及田块之间",
|
|
127
127
|
},
|
|
128
128
|
"component.info.WidgetStackInfo": {
|
|
129
129
|
ru: '🎯 **Назначение**\nОтладка прошивки: свободная память и запас стека задач.\n\n📦 **Пакет данных**\n- Один объект `StackInfo` из ответа на `GET StackInfo`, привязывать переменные не нужно\n- Запрос - кнопкой «Обновить» или автоопросом\n\n🔑 **Ключи**\n- `Heap` - массив `[{ "Name", "Total", "Free", "MinFree" }]`, байт: кучи памяти - `Internal` встроенная и `PSRAM` внешняя ОЗУ у ESP, `Heap` у STM, в виджете понятными подписями; `MinFree` - минимум свободного с момента старта\n- `Tasks` - массив `[{ "Name", "State", "Prio", "Core", "CPU", "FreeMin" }]`: задачи; `State` - `0` выполняется, `1` готова, `2` ждёт, `3` приостановлена, `4` удалена; `Core` - ядро (только ESP32, `-1` без привязки); `CPU` - загрузка задачи, % одного ядра с прошлого ответа; `FreeMin` - минимальный запас стека, байт\n\n✨ **Особенности**\n- Светлая часть полосы кучи - пик занятости\n- Задачи отсортированы по запасу стека - самые рискованные сверху\n- Загрузка от 50 % - оранжевым, от 80 % - красным (кроме `IDLE` - это свободное время ядра); колонка есть, если прошивка её присылает\n- Свёрнутый виджет не опрашивает устройство\n\n⚙️ **Настройки**\n- Порог запаса стека: меньше порога - красным, меньше удвоенного - оранжевым\n- Автоопрос: выкл / 1 / 5 / 10 с; мастер-цвет',
|
|
@@ -758,11 +758,12 @@ const translations = {
|
|
|
758
758
|
"widget.mission.passes": { ru: "Маршрут", en: "Route", zh: "航线" },
|
|
759
759
|
"widget.mission.angle": { ru: "Угол проходов", en: "Pass angle", zh: "作业角度" },
|
|
760
760
|
"widget.mission.auto": { ru: "Авторасчёт", en: "Auto-calculate", zh: "自动计算" },
|
|
761
|
+
"widget.mission.showOnMap": { ru: "Показать на карте", en: "Show on the map", zh: "在地图上显示" },
|
|
761
762
|
"widget.mission.autoInfoTitle": { ru: "Как работает авторасчёт", en: "How auto-calculate works", zh: "自动计算的原理" },
|
|
762
763
|
"widget.mission.autoInfo": {
|
|
763
|
-
ru: "Авторасчёт подбирает для каждого поля направление проходов, при котором поле обрабатывается быстрее всего и как можно полнее. Ниже по шагам, как это устроено.\n\n## 🗺️ 1. Подготовка\n- Карта вокруг поля переводится в плоскость в метрах. Для полей размером в несколько километров ошибка такого перевода незаметна.\n- Поле поворачивается так, чтобы проходы шли слева направо. Дальше всё считается в этой повёрнутой системе.\n- Запретная зона может лежать внутри поля, выходить за его край или стоять вне полей. Полю она мешает, если задевает его, а перелётам мешает всегда.\n\n## 🚜 2. Проходы\n- Проходы идут параллельно с шагом не больше ширины захвата.\n- Первый и последний проходы стоят на расстоянии отступа от края поля. Остальные распределяются между ними поровну, поэтому соседние проходы могут слегка перекрываться, но не оставляют просветов.\n- Проход должен быть свободен по всей своей ширине: от его оси в обе стороны на величину отступа не должно быть ни края поля, ни запретной зоны.\n- Концы прохода укорачиваются на отступ от границы. Куски короче четверти захвата отбрасываются, но порог не меньше 1 м.\n- 💧 Распылитель работает только на проходах. На разворотах и переходах он выключен.\n\n## 🧩 3. Ячейки\n- Если запретная зона или вырез разрезает проходы на части, поле делится на ячейки. Внутри ячейки проходы идут подряд змейкой и соединяются один к одному.\n- Ячейку можно начать четырьмя способами: с первого или последнего прохода и с левого или правого конца.\n\n## 🔀 4. Порядок облёта\n- Ищется порядок ячеек и способ входа в каждую, при котором путь между ними самый короткий.\n- До 10 ячеек перебираются все варианты (точный метод Хелда-Карпа), так что порядок гарантированно лучший.\n- Больше 10 ячеек: сначала каждый раз берётся ближайшая ячейка, потом порядок улучшается перестановками участков (2-opt), до 50 кругов.\n- ✈️ Переходы и развороты прокладываются в обход препятствий. Вокруг углов поля и зон ставятся опорные точки чуть дальше отступа, и по ним ищется кратчайший путь (алгоритм Дейкстры). Путь не подходит к границам ближе отступа.\n- Если точка взлёта стоит за полем, путь к полю обходит только запретные зоны.\n- ⚠️ Перелёты от точки взлёта к полю, между полями и домой обходят все запретные зоны, в том числе стоящие вне полей. Поэтому опоры ЛЭП, вышки, деревья и постройки между полями обязательно обведите запретными зонами, иначе аппарат полетит напрямую и может в них врезаться.\n- Несколько полей облетаются по очереди: каждый раз следующее ближайшее к текущему положению. У каждого поля свой угол.\n\n## ⚖️ 5. Как сравниваются углы\nДля каждого угла строится полная траектория и считается её «цена» в секундах:\n- ⏱️ весь путь (проходы, развороты, переходы), делённый на скорость;\n- 🔄 плюс 4 с на каждый разворот: аппарат тормозит и разгоняется;\n- 🚫 плюс штраф за недолив. Площадь поля, делённая на ширину захвата, даёт длину проходов при идеальном покрытии. Каждый метр, которого не хватает до неё, считается как три метра полёта: за пропущенным куском пришлось бы возвращаться;\n- ⛔ если к какой-то ячейке нет пути, угол фактически отбрасывается.\nПобеждает угол с наименьшей ценой.\n\n## 🔍 6. Поиск угла приближениями\nЦена меняется скачками: на полградуса больше, и появляется или пропадает целый проход. Поэтому угол ищется в три захода:\n- 1️⃣ все направления с шагом 5° (36 углов) плюс направления каждой стороны поля;\n- 2️⃣ около трёх лучших вариантов, разнесённых минимум на 5°, шаг 1° на ±4°. Три кандидата нужны, чтобы не застрять в ложном минимуме;\n- 3️⃣ около победителя шаг 0,1° на ±0,5°.\nВсего около 75 полных расчётов на поле. Расчёт идёт частями, чтобы интерфейс не замирал. Прогресс виден на карте.\n\nКогда поле только замкнули, ему сразу ставится быстрая оценка без прокладки путей (перебор через 3°, затем через 0,5°), а потом её уточняет полный поиск.\n\n## 🤔 Почему проходы не вдоль стороны поля\nНаправления сторон всегда есть среди кандидатов, но выигрывают они не всегда.\n- Если вести проходы вдоль одной стороны, они идут наискось к другим сторонам и к краям выреза. Каждый проход обрывается за отступ до наклонной границы, и вдоль неё остаются необработанные клинья.\n- Пример с малого восточного демо-поля: вдоль левой стороны (5,6°) покрытие 78,
|
|
764
|
-
en: 'Auto-calculate picks a pass direction for each field so that the field is sprayed as fast and as completely as possible. Here is how it works, step by step.\n\n## 🗺️ 1. Preparation\n- The map around the field is converted to a flat plane in meters. For fields a few kilometers across the error of this conversion is negligible.\n- The field is rotated so that passes run left to right. Everything else is computed in this rotated frame.\n- A no-fly zone can lie inside a field, stick out over its edge or stand outside fields. It affects a field if it touches it, and it always affects transits.\n\n## 🚜 2. Passes\n- Passes run in parallel, spaced no wider than the swath.\n- The first and last passes sit one margin away from the field edge. The rest are spread evenly between them, so neighboring passes may overlap slightly but never leave gaps.\n- A pass must be clear across its whole width: within one margin on both sides of its axis there must be neither the field edge nor a no-fly zone.\n- Pass ends are shortened by the margin from the boundary. Pieces shorter than a quarter of the swath are dropped, with a threshold of at least 1 m.\n- 💧 The sprayer works only on passes. It is off on turns and transits.\n\n## 🧩 3. Cells\n- If a no-fly zone or a notch cuts passes into pieces, the field is split into cells. Inside a cell passes follow each other in a zigzag and connect one to one.\n- A cell can be started in four ways: from the first or the last pass, from the left or the right end.\n\n## 🔀 4. Flight order\n- The search looks for the order of cells and the way into each one that makes the path between them the shortest.\n- Up to 10 cells all options are checked (exact Held-Karp method), so the order is guaranteed to be the best.\n- Over 10 cells: first the nearest cell is taken each time, then the order is improved by reversing parts of it (2-opt), up to 50 rounds.\n- ✈️ Transits and turns go around obstacles. Anchor points are placed near the corners of the field and zones slightly beyond the margin, and the shortest path is found through them (Dijkstra\'s algorithm). The path never comes closer to a boundary than the margin.\n- If the takeoff point is outside the field, the path to the field only avoids no-fly zones.\n- ⚠️ Transits from takeoff to a field, between fields and back home avoid all no-fly zones, including those outside fields. So be sure to outline power line poles, towers, trees and buildings between fields as no-fly zones, otherwise the vehicle flies straight and may crash into them.\n- Several fields are flown one by one: each time the one nearest to the current position. Each field has its own angle.\n\n## ⚖️ 5. How angles are compared\nFor each angle the full path is built and its "cost" in seconds is calculated:\n- ⏱️ the whole path (passes, turns, transits) divided by the speed;\n- 🔄 plus 4 s for each turn: the vehicle brakes and speeds up again;\n- 🚫 plus a penalty for missed spraying. The field area divided by the swath gives the pass length for perfect coverage. Each meter short of it counts as three meters of flight: the vehicle would have to come back for the missed piece;\n- ⛔ if some cell cannot be reached, the angle is effectively rejected.\nThe angle with the lowest cost wins.\n\n## 🔍 6. Searching the angle step by step\nThe cost changes in jumps: half a degree more, and a whole pass appears or disappears. So the angle is searched in three rounds:\n- 1️⃣ all directions in 5° steps (36 angles) plus the direction of every field side;\n- 2️⃣ around the three best options, at least 5° apart, 1° steps over ±4°. Three candidates are needed so the search does not get stuck in a false minimum;\n- 3️⃣ around the winner, 0.1° steps over ±0.5°.\nThat is about 75 full calculations per field. The work is split into chunks so the interface does not freeze. Progress is shown on the map.\n\nWhen a field has just been closed, it immediately gets a quick estimate without path routing (3° steps, then 0.5°), and the full search refines it afterwards.\n\n## 🤔 Why passes do not follow a field side\nSide directions are always among the candidates, but they do not always win.\n- If passes follow one side, they cross the other sides and the notch edges at an angle. Each pass stops one margin before the slanted boundary, and untreated wedges remain along it.\n- Example from the small eastern demo field: along the left side (5.6°) the coverage is 78.
|
|
765
|
-
zh: "自动计算为每个田块选择作业行方向,使田块喷洒最快、覆盖尽可能完整。下面逐步说明其原理。\n\n## 🗺️ 1. 准备\n- 田块周围的地图被转换为以米为单位的平面。对于几公里大小的田块,这种转换的误差可以忽略。\n- 田块被旋转,使作业行从左到右。之后的计算都在这个旋转后的坐标系中进行。\n- 禁飞区可以位于田块内、跨出田块边缘或位于田块外。只要与田块接触就影响该田块,并且始终影响转场。\n\n## 🚜 2. 作业行\n- 作业行相互平行,间距不大于喷幅。\n- 第一行和最后一行距田块边缘一个边距。其余各行在两者之间均匀分布,因此相邻作业行可能略有重叠,但不会留下空隙。\n- 作业行在整个宽度上必须畅通:轴线两侧一个边距内不能有田块边缘或禁飞区。\n- 作业行两端从边界缩短一个边距。短于四分之一喷幅的片段会被舍弃,阈值不小于 1 m。\n- 💧 喷洒器只在作业行上工作,转弯和转场时关闭。\n\n## 🧩 3. 单元\n- 如果禁飞区或缺口把作业行切成几段,田块会被分成若干单元。单元内作业行呈之字形依次相连,一一对应。\n- 每个单元有四种开始方式:从第一行或最后一行开始,从左端或右端开始。\n\n## 🔀 4. 飞行顺序\n- 寻找单元顺序和每个单元的进入方式,使单元之间的路径最短。\n- 不超过 10 个单元时检查所有方案(精确的 Held-Karp 方法),保证顺序最优。\n- 超过 10 个单元时:先每次选择最近的单元,再通过翻转部分顺序 (2-opt) 改进,最多 50 轮。\n- ✈️ 转场和转弯绕开障碍物。在田块和禁飞区的角附近、略大于边距的位置设置支点,并通过它们寻找最短路径(Dijkstra 算法)。路径与边界的距离不小于边距。\n- 如果起飞点在田块外,前往田块的路径只绕开禁飞区。\n- ⚠️ 从起飞点到田块、田块之间以及返航的转场会绕开所有禁飞区,包括田块外的禁飞区。因此务必把田块之间的电力线杆、塔、树木和建筑圈为禁飞区,否则飞行器会直线飞行并可能撞上它们。\n- 多个田块依次作业:每次选择离当前位置最近的田块。每个田块有自己的角度。\n\n## ⚖️ 5. 如何比较角度\n对每个角度构建完整航线,并以秒为单位计算其“代价”:\n- ⏱️ 全部路径(作业行、转弯、转场)除以速度;\n- 🔄 每次转弯加 4 s:飞行器需要减速再加速;\n- 🚫 加上漏喷惩罚。田块面积除以喷幅得到完全覆盖时的作业行长度。每缺少一米按三米飞行计算:漏掉的部分需要返回补喷;\n- ⛔ 如果某个单元无法到达,该角度实际上被排除。\n代价最低的角度胜出。\n\n## 🔍 6. 逐步逼近搜索角度\n代价是跳跃变化的:角度多半度,就可能多出或少掉一整行。因此角度分三轮搜索:\n- 1️⃣ 以 5° 步长遍历所有方向(36 个角度),并加上田块每条边的方向;\n- 2️⃣ 在三个最佳方案附近(彼此至少相距 5°),以 1° 步长搜索 ±4°。需要三个候选,避免陷入假的最小值;\n- 3️⃣ 在胜出角度附近,以 0.1° 步长搜索 ±0.5°。\n每个田块大约需要 75 次完整计算。计算分批进行,界面不会卡住。进度显示在地图上。\n\n田块刚闭合时,会立即得到一个不规划路径的快速估算(先 3° 步长,再 0.5°),随后由完整搜索进一步精确。\n\n## 🤔 为什么作业行不沿田块的边\n各边的方向总在候选之中,但并不总是最优。\n- 如果作业行沿一条边,就会与其他边和缺口边缘斜交。每行在斜边界前一个边距处中断,沿边界留下未作业的楔形区域。\n- 东侧小演示田块的例子:沿左边 (5.6°) 覆盖率为 78.
|
|
764
|
+
ru: "Авторасчёт подбирает для каждого поля направление проходов, при котором поле обрабатывается быстрее всего и как можно полнее. Ниже по шагам, как это устроено.\n\n## 🗺️ 1. Подготовка\n- Карта вокруг поля переводится в плоскость в метрах. Для полей размером в несколько километров ошибка такого перевода незаметна.\n- Поле поворачивается так, чтобы проходы шли слева направо. Дальше всё считается в этой повёрнутой системе.\n- Запретная зона может лежать внутри поля, выходить за его край или стоять вне полей. Полю она мешает, если задевает его, а перелётам мешает всегда.\n\n## 🚜 2. Проходы\n- Проходы идут параллельно с шагом не больше ширины захвата.\n- Первый и последний проходы стоят на расстоянии отступа от края поля. Остальные распределяются между ними поровну, поэтому соседние проходы могут слегка перекрываться, но не оставляют просветов.\n- Проход должен быть свободен по всей своей ширине: от его оси в обе стороны на величину отступа не должно быть ни края поля, ни запретной зоны.\n- Концы прохода укорачиваются на отступ от границы. Куски короче четверти захвата отбрасываются, но порог не меньше 1 м.\n- 💧 Распылитель работает только на проходах. На разворотах и переходах он выключен.\n\n## 🧩 3. Ячейки\n- Если запретная зона или вырез разрезает проходы на части, поле делится на ячейки. Внутри ячейки проходы идут подряд змейкой и соединяются один к одному.\n- Ячейку можно начать четырьмя способами: с первого или последнего прохода и с левого или правого конца.\n\n## 🔀 4. Порядок облёта\n- Ищется порядок ячеек и способ входа в каждую, при котором путь между ними самый короткий.\n- До 10 ячеек перебираются все варианты (точный метод Хелда-Карпа), так что порядок гарантированно лучший.\n- Больше 10 ячеек: сначала каждый раз берётся ближайшая ячейка, потом порядок улучшается перестановками участков (2-opt), до 50 кругов.\n- ✈️ Переходы и развороты прокладываются в обход препятствий. Вокруг углов поля и зон ставятся опорные точки чуть дальше отступа, и по ним ищется кратчайший путь (алгоритм Дейкстры). Путь не подходит к границам ближе отступа.\n- Если точка взлёта стоит за полем, путь к полю обходит только запретные зоны.\n- ⚠️ Перелёты от точки взлёта к полю, между полями и домой обходят все запретные зоны, в том числе стоящие вне полей. Поэтому опоры ЛЭП, вышки, деревья и постройки между полями обязательно обведите запретными зонами, иначе аппарат полетит напрямую и может в них врезаться.\n- Несколько полей облетаются по очереди: каждый раз следующее ближайшее к текущему положению. У каждого поля свой угол.\n\n## ⚖️ 5. Как сравниваются углы\nДля каждого угла строится полная траектория и считается её «цена» в секундах:\n- ⏱️ весь путь (проходы, развороты, переходы), делённый на скорость;\n- 🔄 плюс 4 с на каждый разворот: аппарат тормозит и разгоняется;\n- 🚫 плюс штраф за недолив. Площадь поля, делённая на ширину захвата, даёт длину проходов при идеальном покрытии. Каждый метр, которого не хватает до неё, считается как три метра полёта: за пропущенным куском пришлось бы возвращаться;\n- ⛔ если к какой-то ячейке нет пути, угол фактически отбрасывается.\nПобеждает угол с наименьшей ценой.\n\n## 🔍 6. Поиск угла приближениями\nЦена меняется скачками: на полградуса больше, и появляется или пропадает целый проход. Поэтому угол ищется в три захода:\n- 1️⃣ все направления с шагом 5° (36 углов) плюс направления каждой стороны поля;\n- 2️⃣ около трёх лучших вариантов, разнесённых минимум на 5°, шаг 1° на ±4°. Три кандидата нужны, чтобы не застрять в ложном минимуме;\n- 3️⃣ около победителя шаг 0,1° на ±0,5°.\nВсего около 75 полных расчётов на поле. Расчёт идёт частями, чтобы интерфейс не замирал. Прогресс виден на карте.\n\nКогда поле только замкнули, ему сразу ставится быстрая оценка без прокладки путей (перебор через 3°, затем через 0,5°), а потом её уточняет полный поиск.\n\n## 🤔 Почему проходы не вдоль стороны поля\nНаправления сторон всегда есть среди кандидатов, но выигрывают они не всегда.\n- Если вести проходы вдоль одной стороны, они идут наискось к другим сторонам и к краям выреза. Каждый проход обрывается за отступ до наклонной границы, и вдоль неё остаются необработанные клинья.\n- Пример с малого восточного демо-поля: вдоль левой стороны (5,6°) покрытие 78,6 %, а угол 8,9°, выбранный подбором, даёт 84,6 % и на один разворот меньше почти за то же время. Это компромисс между левой стороной и правыми сторонами (около 14-15°).\n- Если все длинные стороны поля параллельны, подбор выберет угол вдоль них.\n\n## 💡 Советы\n- Покрытие меньше 100 % в основном из-за отступа от границ и зон. Чтобы обработать больше, уменьшите отступ.\n- Угол можно задать вручную: общим ползунком или у каждого поля в блоке «Итог».\n- После правки контура нажмите «Авторасчёт» ещё раз.\n- Время облёта в «Итоге» считается так же: путь со скоростью плюс 4 с на разворот.",
|
|
765
|
+
en: 'Auto-calculate picks a pass direction for each field so that the field is sprayed as fast and as completely as possible. Here is how it works, step by step.\n\n## 🗺️ 1. Preparation\n- The map around the field is converted to a flat plane in meters. For fields a few kilometers across the error of this conversion is negligible.\n- The field is rotated so that passes run left to right. Everything else is computed in this rotated frame.\n- A no-fly zone can lie inside a field, stick out over its edge or stand outside fields. It affects a field if it touches it, and it always affects transits.\n\n## 🚜 2. Passes\n- Passes run in parallel, spaced no wider than the swath.\n- The first and last passes sit one margin away from the field edge. The rest are spread evenly between them, so neighboring passes may overlap slightly but never leave gaps.\n- A pass must be clear across its whole width: within one margin on both sides of its axis there must be neither the field edge nor a no-fly zone.\n- Pass ends are shortened by the margin from the boundary. Pieces shorter than a quarter of the swath are dropped, with a threshold of at least 1 m.\n- 💧 The sprayer works only on passes. It is off on turns and transits.\n\n## 🧩 3. Cells\n- If a no-fly zone or a notch cuts passes into pieces, the field is split into cells. Inside a cell passes follow each other in a zigzag and connect one to one.\n- A cell can be started in four ways: from the first or the last pass, from the left or the right end.\n\n## 🔀 4. Flight order\n- The search looks for the order of cells and the way into each one that makes the path between them the shortest.\n- Up to 10 cells all options are checked (exact Held-Karp method), so the order is guaranteed to be the best.\n- Over 10 cells: first the nearest cell is taken each time, then the order is improved by reversing parts of it (2-opt), up to 50 rounds.\n- ✈️ Transits and turns go around obstacles. Anchor points are placed near the corners of the field and zones slightly beyond the margin, and the shortest path is found through them (Dijkstra\'s algorithm). The path never comes closer to a boundary than the margin.\n- If the takeoff point is outside the field, the path to the field only avoids no-fly zones.\n- ⚠️ Transits from takeoff to a field, between fields and back home avoid all no-fly zones, including those outside fields. So be sure to outline power line poles, towers, trees and buildings between fields as no-fly zones, otherwise the vehicle flies straight and may crash into them.\n- Several fields are flown one by one: each time the one nearest to the current position. Each field has its own angle.\n\n## ⚖️ 5. How angles are compared\nFor each angle the full path is built and its "cost" in seconds is calculated:\n- ⏱️ the whole path (passes, turns, transits) divided by the speed;\n- 🔄 plus 4 s for each turn: the vehicle brakes and speeds up again;\n- 🚫 plus a penalty for missed spraying. The field area divided by the swath gives the pass length for perfect coverage. Each meter short of it counts as three meters of flight: the vehicle would have to come back for the missed piece;\n- ⛔ if some cell cannot be reached, the angle is effectively rejected.\nThe angle with the lowest cost wins.\n\n## 🔍 6. Searching the angle step by step\nThe cost changes in jumps: half a degree more, and a whole pass appears or disappears. So the angle is searched in three rounds:\n- 1️⃣ all directions in 5° steps (36 angles) plus the direction of every field side;\n- 2️⃣ around the three best options, at least 5° apart, 1° steps over ±4°. Three candidates are needed so the search does not get stuck in a false minimum;\n- 3️⃣ around the winner, 0.1° steps over ±0.5°.\nThat is about 75 full calculations per field. The work is split into chunks so the interface does not freeze. Progress is shown on the map.\n\nWhen a field has just been closed, it immediately gets a quick estimate without path routing (3° steps, then 0.5°), and the full search refines it afterwards.\n\n## 🤔 Why passes do not follow a field side\nSide directions are always among the candidates, but they do not always win.\n- If passes follow one side, they cross the other sides and the notch edges at an angle. Each pass stops one margin before the slanted boundary, and untreated wedges remain along it.\n- Example from the small eastern demo field: along the left side (5.6°) the coverage is 78.6 %, while 8.9° chosen by the search gives 84.6 % and one turn less in almost the same time. It is a compromise between the left side and the right sides (about 14-15°).\n- If all long sides of a field are parallel, the search picks the angle along them.\n\n## 💡 Tips\n- Coverage below 100 % is mostly due to the margin from boundaries and zones. To treat more area, reduce the margin.\n- The angle can be set by hand: with the common slider or for each field in the Summary block.\n- After editing an outline, press "Auto-calculate" again.\n- The flight time in the Summary is computed the same way: path at the given speed plus 4 s per turn.',
|
|
766
|
+
zh: "自动计算为每个田块选择作业行方向,使田块喷洒最快、覆盖尽可能完整。下面逐步说明其原理。\n\n## 🗺️ 1. 准备\n- 田块周围的地图被转换为以米为单位的平面。对于几公里大小的田块,这种转换的误差可以忽略。\n- 田块被旋转,使作业行从左到右。之后的计算都在这个旋转后的坐标系中进行。\n- 禁飞区可以位于田块内、跨出田块边缘或位于田块外。只要与田块接触就影响该田块,并且始终影响转场。\n\n## 🚜 2. 作业行\n- 作业行相互平行,间距不大于喷幅。\n- 第一行和最后一行距田块边缘一个边距。其余各行在两者之间均匀分布,因此相邻作业行可能略有重叠,但不会留下空隙。\n- 作业行在整个宽度上必须畅通:轴线两侧一个边距内不能有田块边缘或禁飞区。\n- 作业行两端从边界缩短一个边距。短于四分之一喷幅的片段会被舍弃,阈值不小于 1 m。\n- 💧 喷洒器只在作业行上工作,转弯和转场时关闭。\n\n## 🧩 3. 单元\n- 如果禁飞区或缺口把作业行切成几段,田块会被分成若干单元。单元内作业行呈之字形依次相连,一一对应。\n- 每个单元有四种开始方式:从第一行或最后一行开始,从左端或右端开始。\n\n## 🔀 4. 飞行顺序\n- 寻找单元顺序和每个单元的进入方式,使单元之间的路径最短。\n- 不超过 10 个单元时检查所有方案(精确的 Held-Karp 方法),保证顺序最优。\n- 超过 10 个单元时:先每次选择最近的单元,再通过翻转部分顺序 (2-opt) 改进,最多 50 轮。\n- ✈️ 转场和转弯绕开障碍物。在田块和禁飞区的角附近、略大于边距的位置设置支点,并通过它们寻找最短路径(Dijkstra 算法)。路径与边界的距离不小于边距。\n- 如果起飞点在田块外,前往田块的路径只绕开禁飞区。\n- ⚠️ 从起飞点到田块、田块之间以及返航的转场会绕开所有禁飞区,包括田块外的禁飞区。因此务必把田块之间的电力线杆、塔、树木和建筑圈为禁飞区,否则飞行器会直线飞行并可能撞上它们。\n- 多个田块依次作业:每次选择离当前位置最近的田块。每个田块有自己的角度。\n\n## ⚖️ 5. 如何比较角度\n对每个角度构建完整航线,并以秒为单位计算其“代价”:\n- ⏱️ 全部路径(作业行、转弯、转场)除以速度;\n- 🔄 每次转弯加 4 s:飞行器需要减速再加速;\n- 🚫 加上漏喷惩罚。田块面积除以喷幅得到完全覆盖时的作业行长度。每缺少一米按三米飞行计算:漏掉的部分需要返回补喷;\n- ⛔ 如果某个单元无法到达,该角度实际上被排除。\n代价最低的角度胜出。\n\n## 🔍 6. 逐步逼近搜索角度\n代价是跳跃变化的:角度多半度,就可能多出或少掉一整行。因此角度分三轮搜索:\n- 1️⃣ 以 5° 步长遍历所有方向(36 个角度),并加上田块每条边的方向;\n- 2️⃣ 在三个最佳方案附近(彼此至少相距 5°),以 1° 步长搜索 ±4°。需要三个候选,避免陷入假的最小值;\n- 3️⃣ 在胜出角度附近,以 0.1° 步长搜索 ±0.5°。\n每个田块大约需要 75 次完整计算。计算分批进行,界面不会卡住。进度显示在地图上。\n\n田块刚闭合时,会立即得到一个不规划路径的快速估算(先 3° 步长,再 0.5°),随后由完整搜索进一步精确。\n\n## 🤔 为什么作业行不沿田块的边\n各边的方向总在候选之中,但并不总是最优。\n- 如果作业行沿一条边,就会与其他边和缺口边缘斜交。每行在斜边界前一个边距处中断,沿边界留下未作业的楔形区域。\n- 东侧小演示田块的例子:沿左边 (5.6°) 覆盖率为 78.6 %,而搜索选出的 8.9° 覆盖率为 84.6 %,转弯少一次,时间几乎相同。这是左边与右侧各边(约 14-15°)之间的折中。\n- 如果田块所有长边都平行,搜索会选择沿这些边的角度。\n\n## 💡 提示\n- 覆盖率低于 100 % 主要是由于与边界和禁飞区的边距。要作业更多面积,请减小边距。\n- 角度可以手动设置:使用公共滑块,或在“汇总”中为每个田块单独设置。\n- 修改轮廓后,请再次点击“自动计算”。\n- “汇总”中的飞行时间按同样方式计算:按速度计算的路径时间加每次转弯 4 s。",
|
|
766
767
|
},
|
|
767
768
|
"widget.mission.autoHint": {
|
|
768
769
|
ru: "Для каждого поля перебираются углы полным расчётом траектории: шаг 5°, затем 1° и 0,1°. Лучший - с наименьшим временем облёта, где учитываются путь и развороты",
|