@lumikmz/kmz 0.2.1 → 0.3.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/README.md +69 -66
- package/dist/index.d.mts +215 -76
- package/dist/index.mjs +634 -157
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/plan/geometry.test.ts +96 -0
- package/src/plan/geometry.ts +161 -25
- package/src/plan/index.ts +23 -3
- package/src/plan/plan-mapping-strip.ts +3 -3
- package/src/plan/plan-mapping2d.ts +512 -89
- package/src/plan/plan-mapping3d.ts +157 -98
- package/src/plan/plan.test.ts +263 -9
- package/src/types/action.ts +65 -1
- package/src/types/drone-payload.ts +18 -12
- package/src/types/enums.ts +17 -3
- package/src/types/mission-config.ts +5 -0
- package/src/types/placemark.ts +92 -15
- package/src/types/template.ts +12 -76
- package/src/types/waylines.ts +12 -3
- package/src/types/waypoint-params.ts +18 -0
|
@@ -1,128 +1,551 @@
|
|
|
1
|
-
import { degrees,
|
|
2
|
-
import type {
|
|
1
|
+
import { degrees, meters, type Meters } from "../types/branded.js";
|
|
2
|
+
import type { Action } from "../types/action.js";
|
|
3
3
|
import type { ExecuteHeightMode } from "../types/enums.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { Mapping2dFolder, Mapping2dTemplate } from "../types/template.js";
|
|
5
|
+
import type { StartActionGroup } from "../types/waylines.js";
|
|
5
6
|
import type { Waylines, WaylinesFolder } from "../types/waylines.js";
|
|
6
7
|
import type { WaylinesPlacemark } from "../types/placemark.js";
|
|
8
|
+
import { PlannerError } from "../types/errors.js";
|
|
7
9
|
import {
|
|
8
10
|
bearingDeg,
|
|
9
11
|
centroid,
|
|
10
12
|
enuToLngLat,
|
|
13
|
+
type EnuPoint,
|
|
11
14
|
lngLatToEnu,
|
|
15
|
+
offsetPolygon,
|
|
16
|
+
orientPathToHome,
|
|
12
17
|
parseCoordinatesString,
|
|
13
|
-
|
|
18
|
+
surveyGrid,
|
|
14
19
|
} from "./geometry.js";
|
|
20
|
+
import type { PlanOptions } from "./index.js";
|
|
15
21
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
/**
|
|
23
|
+
* `standoffM` dilates the survey polygon outward before gridding (smart-oblique
|
|
24
|
+
* needs the grid to overfly the boundary on every side; 0 for ortho/quick-ortho).
|
|
25
|
+
*/
|
|
26
|
+
function buildGrid(template: Mapping2dTemplate, lineSpacing: number, standoffM = 0) {
|
|
20
27
|
const folder = template.Folder;
|
|
21
28
|
const ring = parseCoordinatesString(
|
|
22
29
|
folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates,
|
|
23
30
|
);
|
|
24
31
|
const origin = centroid(ring);
|
|
25
|
-
const
|
|
32
|
+
const enuRing = ring.map((p) => lngLatToEnu(p, origin));
|
|
33
|
+
const gridRing = standoffM > 0 ? offsetPolygon(enuRing, standoffM) : enuRing;
|
|
34
|
+
const enuWps = orientPathToHome(
|
|
35
|
+
surveyGrid(gridRing, {
|
|
36
|
+
directionDeg: folder.Placemark.direction,
|
|
37
|
+
marginM: folder.Placemark.margin,
|
|
38
|
+
lineSpacing,
|
|
39
|
+
}),
|
|
40
|
+
origin,
|
|
41
|
+
template.missionConfig.takeOffRefPoint,
|
|
42
|
+
);
|
|
43
|
+
return { folder, origin, enuWps };
|
|
44
|
+
}
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Smart-oblique stand-off (meters). The gimbal swings up to |swing|° off nadir in
|
|
48
|
+
* all directions during a single pass, so the most-oblique look images h·tan(|swing|)
|
|
49
|
+
* away from the aircraft. The survey polygon is therefore dilated by this distance so
|
|
50
|
+
* the grid overflies the boundary on every side. Clamp to 3·h.
|
|
51
|
+
*/
|
|
52
|
+
function smartObliqueStandoff(folder: Mapping2dFolder): number {
|
|
53
|
+
const h = Number(folder.Placemark.height);
|
|
54
|
+
const swingAbs = Math.abs(Number(folder.Placemark.smartObliqueGimbalPitch ?? degrees(-45)));
|
|
55
|
+
if (!(swingAbs > 0 && swingAbs < 90)) return 0;
|
|
56
|
+
return Math.min(h * Math.tan((swingAbs * Math.PI) / 180), h * 3);
|
|
57
|
+
}
|
|
31
58
|
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
function heightModeToExecuteMode(folder: Mapping2dFolder): ExecuteHeightMode {
|
|
60
|
+
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
61
|
+
case "EGM96":
|
|
62
|
+
return "WGS84";
|
|
63
|
+
case "relativeToStartPoint":
|
|
64
|
+
return "relativeToStartPoint";
|
|
65
|
+
case "aboveGroundLevel":
|
|
66
|
+
case "realTimeFollowSurface":
|
|
67
|
+
return "realTimeFollowSurface";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
34
70
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const isLast = index === lngLatWps.length - 1;
|
|
38
|
-
const headingAngle = isLast ? 0 : bearingDeg(enuWps[index]!, enuWps[index + 1]!);
|
|
71
|
+
/** Desired damping distance; clamped down when segments are short. */
|
|
72
|
+
const DESIRED_TURN_DAMPING_M = 10;
|
|
39
73
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: [
|
|
43
|
-
{
|
|
44
|
-
actionGroupId: actionGroupId++,
|
|
45
|
-
actionGroupStartIndex: index,
|
|
46
|
-
actionGroupEndIndex: index + 1,
|
|
47
|
-
actionGroupMode: "sequence",
|
|
48
|
-
actionTrigger: { actionTriggerType: "betweenAdjacentPoints" },
|
|
49
|
-
action: [
|
|
50
|
-
{
|
|
51
|
-
actionId: 0,
|
|
52
|
-
actionActuatorFunc: "takePhoto",
|
|
53
|
-
actionActuatorFuncParam: {
|
|
54
|
-
payloadPositionIndex: 0,
|
|
55
|
-
payloadLensIndex: ["wide"],
|
|
56
|
-
useGlobalPayloadLensIndex: false,
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
];
|
|
74
|
+
/** Never consume more than this fraction of either adjacent segment. */
|
|
75
|
+
const MAX_DAMPING_FRAC = 0.45;
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
/** Flat ENU distance between two grid points (metres). */
|
|
78
|
+
function enuSegLen(a: EnuPoint, b: EnuPoint): number {
|
|
79
|
+
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compute the coordinateTurn damping distance for interior waypoint `i`.
|
|
84
|
+
*
|
|
85
|
+
* Spec constraint: dampingDist[i] + dampingDist[i+1] < segmentLength(i, i+1).
|
|
86
|
+
* Clamping each side to MAX_DAMPING_FRAC × its shorter adjacent segment
|
|
87
|
+
* guarantees the sum is at most 2 × MAX_DAMPING_FRAC < 1 of that segment.
|
|
88
|
+
*/
|
|
89
|
+
function turnDamping(enuWps: readonly EnuPoint[], i: number): Meters {
|
|
90
|
+
const dPrev = enuSegLen(enuWps[i - 1]!, enuWps[i]!);
|
|
91
|
+
const dNext = enuSegLen(enuWps[i]!, enuWps[i + 1]!);
|
|
92
|
+
return meters(
|
|
93
|
+
Math.min(DESIRED_TURN_DAMPING_M, dPrev * MAX_DAMPING_FRAC, dNext * MAX_DAMPING_FRAC),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Nadir gimbal-down, hover, manual focus, infinite focus, hover — per reference WPML. */
|
|
98
|
+
function buildStartActionGroup(pitchDeg: number): StartActionGroup {
|
|
99
|
+
return {
|
|
100
|
+
action: [
|
|
101
|
+
{
|
|
102
|
+
actionId: 0,
|
|
103
|
+
actionActuatorFunc: "gimbalRotate",
|
|
104
|
+
actionActuatorFuncParam: {
|
|
105
|
+
payloadPositionIndex: 0,
|
|
106
|
+
gimbalHeadingYawBase: "aircraft",
|
|
107
|
+
gimbalRotateMode: "absoluteAngle",
|
|
108
|
+
gimbalPitchRotateEnable: true,
|
|
109
|
+
gimbalPitchRotateAngle: degrees(pitchDeg),
|
|
110
|
+
gimbalRollRotateEnable: false,
|
|
111
|
+
gimbalRollRotateAngle: degrees(0),
|
|
112
|
+
gimbalYawRotateEnable: true,
|
|
113
|
+
gimbalYawRotateAngle: degrees(0),
|
|
114
|
+
gimbalRotateTimeEnable: false,
|
|
115
|
+
gimbalRotateTime: 10,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
actionId: 1,
|
|
120
|
+
actionActuatorFunc: "hover",
|
|
121
|
+
actionActuatorFuncParam: { hoverTime: 0.5 },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
actionId: 2,
|
|
125
|
+
actionActuatorFunc: "setFocusType",
|
|
126
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0, cameraFocusType: "manual" },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
actionId: 3,
|
|
130
|
+
actionActuatorFunc: "focus",
|
|
131
|
+
actionActuatorFuncParam: {
|
|
132
|
+
payloadPositionIndex: 0,
|
|
133
|
+
isPointFocus: false,
|
|
134
|
+
focusX: 0,
|
|
135
|
+
focusY: 0,
|
|
136
|
+
focusRegionWidth: 0,
|
|
137
|
+
focusRegionHeight: 0,
|
|
138
|
+
isInfiniteFocus: true,
|
|
139
|
+
isCalibrationFocus: false,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
actionId: 4,
|
|
144
|
+
actionActuatorFunc: "hover",
|
|
145
|
+
actionActuatorFuncParam: { hoverTime: 1 },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Build the three actions placed at the first waypoint of a shooting section. */
|
|
152
|
+
function shootSectionStartActions(
|
|
153
|
+
pitchDeg: number,
|
|
154
|
+
shootInterval: number,
|
|
155
|
+
sectionStart: number,
|
|
156
|
+
sectionEnd: number,
|
|
157
|
+
groupIdStart: number,
|
|
158
|
+
): { groups: import("../types/action.js").ActionGroup[]; nextGroupId: number } {
|
|
159
|
+
const betweenGroup: import("../types/action.js").ActionGroup = {
|
|
160
|
+
actionGroupId: groupIdStart,
|
|
161
|
+
actionGroupStartIndex: sectionStart,
|
|
162
|
+
actionGroupEndIndex: sectionEnd,
|
|
163
|
+
actionGroupMode: "sequence",
|
|
164
|
+
actionTrigger: { actionTriggerType: "betweenAdjacentPoints" },
|
|
165
|
+
action: [
|
|
166
|
+
{
|
|
167
|
+
actionId: 0,
|
|
168
|
+
actionActuatorFunc: "gimbalAngleLock",
|
|
169
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0 },
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
actionId: 1,
|
|
173
|
+
actionActuatorFunc: "gimbalRotate",
|
|
174
|
+
actionActuatorFuncParam: {
|
|
175
|
+
payloadPositionIndex: 0,
|
|
176
|
+
gimbalHeadingYawBase: "aircraft",
|
|
177
|
+
gimbalRotateMode: "absoluteAngle",
|
|
178
|
+
gimbalPitchRotateEnable: true,
|
|
179
|
+
gimbalPitchRotateAngle: degrees(pitchDeg),
|
|
180
|
+
gimbalRollRotateEnable: false,
|
|
181
|
+
gimbalRollRotateAngle: degrees(0),
|
|
182
|
+
gimbalYawRotateEnable: false,
|
|
183
|
+
gimbalYawRotateAngle: degrees(0),
|
|
184
|
+
gimbalRotateTimeEnable: false,
|
|
185
|
+
gimbalRotateTime: 10,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
actionId: 2,
|
|
190
|
+
actionActuatorFunc: "startTimeLapse",
|
|
191
|
+
actionActuatorFuncParam: {
|
|
192
|
+
payloadPositionIndex: 0,
|
|
193
|
+
useGlobalPayloadLensIndex: false,
|
|
194
|
+
payloadLensIndex: "visable",
|
|
195
|
+
minShootInterval: shootInterval,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const timingGroup: import("../types/action.js").ActionGroup = {
|
|
202
|
+
actionGroupId: groupIdStart + 1,
|
|
203
|
+
actionGroupStartIndex: sectionStart,
|
|
204
|
+
actionGroupEndIndex: sectionEnd,
|
|
205
|
+
actionGroupMode: "sequence",
|
|
206
|
+
actionTrigger: { actionTriggerType: "multipleTiming", actionTriggerParam: shootInterval },
|
|
207
|
+
action: [
|
|
208
|
+
{
|
|
209
|
+
actionId: 0,
|
|
210
|
+
actionActuatorFunc: "gimbalRotate",
|
|
211
|
+
actionActuatorFuncParam: {
|
|
212
|
+
payloadPositionIndex: 0,
|
|
213
|
+
gimbalHeadingYawBase: "aircraft",
|
|
214
|
+
gimbalRotateMode: "absoluteAngle",
|
|
215
|
+
gimbalPitchRotateEnable: true,
|
|
216
|
+
gimbalPitchRotateAngle: degrees(pitchDeg),
|
|
217
|
+
gimbalRollRotateEnable: false,
|
|
218
|
+
gimbalRollRotateAngle: degrees(0),
|
|
219
|
+
gimbalYawRotateEnable: false,
|
|
220
|
+
gimbalYawRotateAngle: degrees(0),
|
|
221
|
+
gimbalRotateTimeEnable: false,
|
|
222
|
+
gimbalRotateTime: 10,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return { groups: [betweenGroup, timingGroup], nextGroupId: groupIdStart + 2 };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Build the stop group placed at the last waypoint of a shooting section. */
|
|
232
|
+
function shootSectionStopActions(
|
|
233
|
+
sectionEnd: number,
|
|
234
|
+
groupId: number,
|
|
235
|
+
): import("../types/action.js").ActionGroup {
|
|
236
|
+
return {
|
|
237
|
+
actionGroupId: groupId,
|
|
238
|
+
actionGroupStartIndex: sectionEnd,
|
|
239
|
+
actionGroupEndIndex: sectionEnd,
|
|
240
|
+
actionGroupMode: "sequence",
|
|
241
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
242
|
+
action: [
|
|
243
|
+
{
|
|
244
|
+
actionId: 0,
|
|
245
|
+
actionActuatorFunc: "stopTimeLapse",
|
|
246
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0, payloadLensIndex: "visable" },
|
|
247
|
+
},
|
|
248
|
+
{ actionId: 1, actionActuatorFunc: "gimbalAngleUnlock" } satisfies Action,
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Zero-dampingDist stop-turn (first/last waypoint of a section). */
|
|
254
|
+
function stopTurn() {
|
|
255
|
+
return {
|
|
256
|
+
waypointTurnMode: "toPointAndStopWithDiscontinuityCurvature" as const,
|
|
257
|
+
waypointTurnDampingDist: meters(0),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Common heading param fields present on every mapping waypoint. */
|
|
262
|
+
function mappingHeadingParam(headingAngle: number, angleEnable: boolean) {
|
|
263
|
+
return {
|
|
264
|
+
waypointHeadingMode: "followWayline" as const,
|
|
265
|
+
waypointHeadingAngle: degrees(headingAngle),
|
|
266
|
+
waypointPoiPoint: { lng: 0, lat: 0, height: meters(0) },
|
|
267
|
+
waypointHeadingAngleEnable: angleEnable,
|
|
268
|
+
waypointHeadingPathMode: "followBadArc" as const,
|
|
269
|
+
waypointHeadingPoiIndex: 0,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Common extra fields present on every mapping waypoint per reference WPML. */
|
|
274
|
+
const COMMON_WP_EXTRAS = {
|
|
275
|
+
useStraightLine: true as const,
|
|
276
|
+
waypointGimbalHeadingParam: {
|
|
277
|
+
waypointGimbalPitchAngle: degrees(0),
|
|
278
|
+
waypointGimbalYawAngle: degrees(0),
|
|
279
|
+
},
|
|
280
|
+
isRisky: false as const,
|
|
281
|
+
waypointWorkType: 0,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 高程优化 (elevationOptimizeEnable) excursion — appended to the nadir grid:
|
|
286
|
+
* fly from the last nadir waypoint to the survey centroid and take an oblique shot
|
|
287
|
+
* there to enrich the DSM. WITHOUT this flag the route ends at the last grid
|
|
288
|
+
* waypoint — it does NOT detour to the centre (that detour is elevation optimization,
|
|
289
|
+
* not smart-swing). Mutates `placemarks` (appends two waypoints).
|
|
290
|
+
*/
|
|
291
|
+
function appendElevationExcursion(
|
|
292
|
+
placemarks: WaylinesPlacemark[],
|
|
293
|
+
folder: Mapping2dFolder,
|
|
294
|
+
origin: { lng: number; lat: number },
|
|
295
|
+
enuWps: readonly EnuPoint[],
|
|
296
|
+
shootInterval: number,
|
|
297
|
+
startGroupId: number,
|
|
298
|
+
): void {
|
|
299
|
+
const lastNadirIdx = enuWps.length - 1;
|
|
300
|
+
const obliqueStartIdx = placemarks.length;
|
|
301
|
+
const obliqueEndIdx = obliqueStartIdx + 1;
|
|
302
|
+
const pitch = Number(folder.Placemark.quickOrthoMappingPitch ?? degrees(-45));
|
|
303
|
+
let nextGroupId = startGroupId;
|
|
304
|
+
|
|
305
|
+
// reachPoint — tilt gimbal to the oblique pitch then hover
|
|
306
|
+
const obliqueGimbalGroup: import("../types/action.js").ActionGroup = {
|
|
307
|
+
actionGroupId: nextGroupId++,
|
|
308
|
+
actionGroupStartIndex: obliqueStartIdx,
|
|
309
|
+
actionGroupEndIndex: obliqueStartIdx,
|
|
310
|
+
actionGroupMode: "sequence",
|
|
311
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
312
|
+
action: [
|
|
313
|
+
{
|
|
314
|
+
actionId: 0,
|
|
315
|
+
actionActuatorFunc: "gimbalRotate",
|
|
316
|
+
actionActuatorFuncParam: {
|
|
317
|
+
payloadPositionIndex: 0,
|
|
318
|
+
gimbalHeadingYawBase: "aircraft",
|
|
319
|
+
gimbalRotateMode: "absoluteAngle",
|
|
320
|
+
gimbalPitchRotateEnable: true,
|
|
321
|
+
gimbalPitchRotateAngle: degrees(pitch),
|
|
322
|
+
gimbalRollRotateEnable: false,
|
|
323
|
+
gimbalRollRotateAngle: degrees(0),
|
|
324
|
+
gimbalYawRotateEnable: true,
|
|
325
|
+
gimbalYawRotateAngle: degrees(0),
|
|
326
|
+
gimbalRotateTimeEnable: false,
|
|
327
|
+
gimbalRotateTime: 10,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{ actionId: 1, actionActuatorFunc: "hover", actionActuatorFuncParam: { hoverTime: 0.5 } },
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const obliqueStart = shootSectionStartActions(
|
|
335
|
+
pitch,
|
|
336
|
+
shootInterval,
|
|
337
|
+
obliqueStartIdx,
|
|
338
|
+
obliqueEndIdx,
|
|
339
|
+
nextGroupId,
|
|
340
|
+
);
|
|
341
|
+
nextGroupId = obliqueStart.nextGroupId;
|
|
342
|
+
const obliqueStop = shootSectionStopActions(obliqueEndIdx, nextGroupId);
|
|
343
|
+
|
|
344
|
+
const lastNadirCoord = enuToLngLat(enuWps[lastNadirIdx]!, origin);
|
|
345
|
+
const sumEnu = enuWps.reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1]] as EnuPoint, [
|
|
346
|
+
0, 0,
|
|
347
|
+
] as EnuPoint);
|
|
348
|
+
const centroidEnu: EnuPoint = [sumEnu[0] / enuWps.length, sumEnu[1] / enuWps.length];
|
|
349
|
+
const centroidCoord = enuToLngLat(centroidEnu, origin);
|
|
350
|
+
const obliqueHeading = bearingDeg(enuWps[lastNadirIdx]!, centroidEnu);
|
|
351
|
+
|
|
352
|
+
placemarks.push({
|
|
353
|
+
Point: { coordinates: lastNadirCoord }, // same position as last nadir
|
|
354
|
+
index: obliqueStartIdx,
|
|
355
|
+
executeHeight: folder.Placemark.height,
|
|
356
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
357
|
+
waypointHeadingParam: mappingHeadingParam(obliqueHeading, true),
|
|
358
|
+
waypointTurnParam: stopTurn(),
|
|
359
|
+
...COMMON_WP_EXTRAS,
|
|
360
|
+
actionGroup: [obliqueGimbalGroup, ...obliqueStart.groups],
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
placemarks.push({
|
|
364
|
+
Point: { coordinates: centroidCoord },
|
|
365
|
+
index: obliqueEndIdx,
|
|
366
|
+
executeHeight: folder.Placemark.height,
|
|
367
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
368
|
+
waypointHeadingParam: mappingHeadingParam(0, false),
|
|
369
|
+
waypointTurnParam: stopTurn(),
|
|
370
|
+
...COMMON_WP_EXTRAS,
|
|
371
|
+
actionGroup: [obliqueStop],
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Ortho (nadir) wayline — covers 正射 and 正射+智能摆拍 (the swing is a folder-level
|
|
377
|
+
* quickOrthoMappingEnable flag over the same grid geometry, not extra waypoints).
|
|
378
|
+
*
|
|
379
|
+
* Action pattern (per reference WPML M4TD 正射采集):
|
|
380
|
+
* - WP 0: two action groups (betweenAdjacentPoints startTimeLapse-init + multipleTiming gimbalCorrect)
|
|
381
|
+
* - WP 1…N-2: no action groups
|
|
382
|
+
* - WP N-1 (last): reachPoint stopTimeLapse group
|
|
383
|
+
*
|
|
384
|
+
* When `elevationOptimizeEnable` is set, a centroid excursion is appended (see
|
|
385
|
+
* `appendElevationExcursion`); otherwise the route ends at the last grid waypoint.
|
|
386
|
+
*/
|
|
387
|
+
function emitOrtho(
|
|
388
|
+
folder: Mapping2dFolder,
|
|
389
|
+
origin: { lng: number; lat: number },
|
|
390
|
+
enuWps: readonly EnuPoint[],
|
|
391
|
+
shootInterval: number,
|
|
392
|
+
): WaylinesFolder {
|
|
393
|
+
const lastIndex = enuWps.length - 1;
|
|
394
|
+
const startActions = shootSectionStartActions(-90, shootInterval, 0, lastIndex, 0);
|
|
395
|
+
const stopGroup = shootSectionStopActions(lastIndex, startActions.nextGroupId);
|
|
396
|
+
|
|
397
|
+
const placemarks: WaylinesPlacemark[] = enuWps.map((enu, index) => {
|
|
398
|
+
const coord = enuToLngLat(enu, origin);
|
|
399
|
+
const isLast = index === lastIndex;
|
|
400
|
+
const headingAngle = isLast ? 0 : bearingDeg(enuWps[index]!, enuWps[index + 1]!);
|
|
401
|
+
const placemark: WaylinesPlacemark = {
|
|
64
402
|
Point: { coordinates: coord },
|
|
65
403
|
index,
|
|
66
|
-
executeHeight: folder.height,
|
|
404
|
+
executeHeight: folder.Placemark.height,
|
|
67
405
|
waypointSpeed: folder.autoFlightSpeed,
|
|
68
|
-
waypointHeadingParam:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
406
|
+
waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
|
|
407
|
+
waypointTurnParam:
|
|
408
|
+
index === 0 || isLast
|
|
409
|
+
? stopTurn()
|
|
410
|
+
: {
|
|
411
|
+
waypointTurnMode: "coordinateTurn",
|
|
412
|
+
waypointTurnDampingDist: turnDamping(enuWps, index),
|
|
413
|
+
},
|
|
414
|
+
...COMMON_WP_EXTRAS,
|
|
75
415
|
};
|
|
416
|
+
if (index === 0) {
|
|
417
|
+
placemark.actionGroup = startActions.groups;
|
|
418
|
+
} else if (isLast) {
|
|
419
|
+
placemark.actionGroup = [stopGroup];
|
|
420
|
+
}
|
|
421
|
+
return placemark;
|
|
76
422
|
});
|
|
77
423
|
|
|
78
|
-
|
|
424
|
+
if (folder.Placemark.elevationOptimizeEnable) {
|
|
425
|
+
appendElevationExcursion(
|
|
426
|
+
placemarks,
|
|
427
|
+
folder,
|
|
428
|
+
origin,
|
|
429
|
+
enuWps,
|
|
430
|
+
shootInterval,
|
|
431
|
+
startActions.nextGroupId + 1,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
79
436
|
templateId: folder.templateId,
|
|
80
437
|
waylineId: 0,
|
|
81
438
|
executeHeightMode: heightModeToExecuteMode(folder),
|
|
82
439
|
autoFlightSpeed: folder.autoFlightSpeed,
|
|
83
|
-
startActionGroup:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
440
|
+
startActionGroup: buildStartActionGroup(-90),
|
|
441
|
+
Placemark: placemarks,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Single-pass smart-oblique / quick-ortho (smartObliqueEnable=true):
|
|
447
|
+
* The aircraft flies one grid while the gimbal swings (startSmartOblique/stopSmartOblique).
|
|
448
|
+
*/
|
|
449
|
+
function emitSmartOblique(
|
|
450
|
+
folder: Mapping2dFolder,
|
|
451
|
+
origin: { lng: number; lat: number },
|
|
452
|
+
enuWps: readonly EnuPoint[],
|
|
453
|
+
): WaylinesFolder {
|
|
454
|
+
const lastIndex = enuWps.length - 1;
|
|
455
|
+
const pitch = folder.Placemark.smartObliqueGimbalPitch ?? degrees(-45);
|
|
456
|
+
|
|
457
|
+
const placemarks: WaylinesPlacemark[] = enuWps.map((enu, index) => {
|
|
458
|
+
const coord = enuToLngLat(enu, origin);
|
|
459
|
+
const isLast = index === lastIndex;
|
|
460
|
+
const headingAngle = isLast ? 0 : bearingDeg(enuWps[index]!, enuWps[index + 1]!);
|
|
461
|
+
const placemark: WaylinesPlacemark = {
|
|
462
|
+
Point: { coordinates: coord },
|
|
463
|
+
index,
|
|
464
|
+
executeHeight: folder.Placemark.height,
|
|
465
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
466
|
+
waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
|
|
467
|
+
waypointTurnParam:
|
|
468
|
+
index === 0 || isLast
|
|
469
|
+
? stopTurn()
|
|
470
|
+
: {
|
|
471
|
+
waypointTurnMode: "coordinateTurn",
|
|
472
|
+
waypointTurnDampingDist: turnDamping(enuWps, index),
|
|
473
|
+
},
|
|
474
|
+
...COMMON_WP_EXTRAS,
|
|
475
|
+
};
|
|
476
|
+
if (index === 0) {
|
|
477
|
+
placemark.actionGroup = [
|
|
90
478
|
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
gimbalRotateTimeEnable: false,
|
|
104
|
-
gimbalRotateTime: 10,
|
|
105
|
-
},
|
|
479
|
+
actionGroupId: 0,
|
|
480
|
+
actionGroupStartIndex: 0,
|
|
481
|
+
actionGroupEndIndex: lastIndex,
|
|
482
|
+
actionGroupMode: "sequence",
|
|
483
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
484
|
+
action: [
|
|
485
|
+
{
|
|
486
|
+
actionId: 0,
|
|
487
|
+
actionActuatorFunc: "startSmartOblique",
|
|
488
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0 },
|
|
489
|
+
},
|
|
490
|
+
],
|
|
106
491
|
},
|
|
107
|
-
]
|
|
108
|
-
}
|
|
492
|
+
];
|
|
493
|
+
} else if (isLast) {
|
|
494
|
+
placemark.actionGroup = [
|
|
495
|
+
{
|
|
496
|
+
actionGroupId: 1,
|
|
497
|
+
actionGroupStartIndex: lastIndex,
|
|
498
|
+
actionGroupEndIndex: lastIndex,
|
|
499
|
+
actionGroupMode: "sequence",
|
|
500
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
501
|
+
action: [
|
|
502
|
+
{
|
|
503
|
+
actionId: 0,
|
|
504
|
+
actionActuatorFunc: "stopSmartOblique",
|
|
505
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0 },
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
return placemark;
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
templateId: folder.templateId,
|
|
516
|
+
waylineId: 0,
|
|
517
|
+
executeHeightMode: heightModeToExecuteMode(folder),
|
|
518
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
519
|
+
startActionGroup: buildStartActionGroup(Number(pitch)),
|
|
109
520
|
Placemark: placemarks,
|
|
110
521
|
};
|
|
111
|
-
|
|
112
|
-
return { missionConfig: template.missionConfig, Folder: [waylineFolder] };
|
|
113
522
|
}
|
|
114
523
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return "relativeToStartPoint";
|
|
121
|
-
case "aboveGroundLevel":
|
|
122
|
-
case "realTimeFollowSurface":
|
|
123
|
-
return "realTimeFollowSurface";
|
|
524
|
+
export function planMapping2d(template: Mapping2dTemplate, options?: PlanOptions): Waylines {
|
|
525
|
+
if (options?.lineSpacing == null) {
|
|
526
|
+
throw new PlannerError(
|
|
527
|
+
"mapping2d requires options.lineSpacing — calculate 2·h·tan(HFOV/2)·(1−sideOverlap) in the demo and pass it in.",
|
|
528
|
+
);
|
|
124
529
|
}
|
|
125
|
-
|
|
530
|
+
// Smart-oblique flies a single omnidirectional swinging pass that must overfly the
|
|
531
|
+
// boundary on every side; ortho / quick-ortho stay tight to the area.
|
|
532
|
+
const standoff =
|
|
533
|
+
template.Folder.Placemark.smartObliqueEnable === true
|
|
534
|
+
? smartObliqueStandoff(template.Folder)
|
|
535
|
+
: 0;
|
|
536
|
+
const { folder, origin, enuWps } = buildGrid(template, Number(options.lineSpacing), standoff);
|
|
537
|
+
const shootInterval = options.shootInterval ?? 2;
|
|
126
538
|
|
|
127
|
-
|
|
128
|
-
|
|
539
|
+
if (folder.Placemark.smartObliqueEnable === true) {
|
|
540
|
+
return {
|
|
541
|
+
missionConfig: template.missionConfig,
|
|
542
|
+
Folder: [emitSmartOblique(folder, origin, enuWps)],
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
// 正射 and 正射+智能摆拍 share the nadir grid; the centroid detour is driven by
|
|
546
|
+
// elevationOptimizeEnable inside emitOrtho, not by quickOrthoMappingEnable.
|
|
547
|
+
return {
|
|
548
|
+
missionConfig: template.missionConfig,
|
|
549
|
+
Folder: [emitOrtho(folder, origin, enuWps, shootInterval)],
|
|
550
|
+
};
|
|
551
|
+
}
|