@lumikmz/kmz 0.2.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/LICENSE +21 -0
- package/README.md +540 -0
- package/dist/index.d.mts +789 -0
- package/dist/index.mjs +779 -0
- package/package.json +24 -0
- package/src/index.ts +3 -0
- package/src/plan/geometry.ts +109 -0
- package/src/plan/index.ts +31 -0
- package/src/plan/plan-mapping-strip.ts +84 -0
- package/src/plan/plan-mapping2d.ts +128 -0
- package/src/plan/plan-mapping3d.ts +188 -0
- package/src/plan/plan-waypoint.ts +80 -0
- package/src/plan/plan.test.ts +96 -0
- package/src/types/action.ts +253 -0
- package/src/types/branded.ts +20 -0
- package/src/types/drone-payload.ts +59 -0
- package/src/types/enums.ts +97 -0
- package/src/types/errors.ts +19 -0
- package/src/types/index.ts +10 -0
- package/src/types/mission-config.ts +58 -0
- package/src/types/placemark.ts +96 -0
- package/src/types/template.ts +170 -0
- package/src/types/waylines.ts +32 -0
- package/src/types/waypoint-params.ts +78 -0
- package/src/validate/index.ts +25 -0
- package/src/validate/validate-mission-config.ts +48 -0
- package/src/validate/validate-template.ts +101 -0
- package/src/validate/validate-waylines.ts +59 -0
- package/tsconfig.json +4 -0
- package/vite.config.ts +9 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
//#region src/types/branded.ts
|
|
2
|
+
const meters = (n) => n;
|
|
3
|
+
const metersPerSecond = (n) => n;
|
|
4
|
+
const degrees = (n) => n;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/types/errors.ts
|
|
7
|
+
var KmzError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "KmzError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var PlannerError = class extends KmzError {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "PlannerError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/types/drone-payload.ts
|
|
21
|
+
/** Aircraft `{ droneEnumValue, droneSubEnumValue }` tuples. */
|
|
22
|
+
const DroneEnum = {
|
|
23
|
+
M400: {
|
|
24
|
+
droneEnumValue: 103,
|
|
25
|
+
droneSubEnumValue: 0
|
|
26
|
+
},
|
|
27
|
+
M350RTK: {
|
|
28
|
+
droneEnumValue: 89,
|
|
29
|
+
droneSubEnumValue: 0
|
|
30
|
+
},
|
|
31
|
+
M300RTK: {
|
|
32
|
+
droneEnumValue: 60,
|
|
33
|
+
droneSubEnumValue: 0
|
|
34
|
+
},
|
|
35
|
+
M30: {
|
|
36
|
+
droneEnumValue: 67,
|
|
37
|
+
droneSubEnumValue: 0
|
|
38
|
+
},
|
|
39
|
+
M30T: {
|
|
40
|
+
droneEnumValue: 67,
|
|
41
|
+
droneSubEnumValue: 1
|
|
42
|
+
},
|
|
43
|
+
M3E: {
|
|
44
|
+
droneEnumValue: 77,
|
|
45
|
+
droneSubEnumValue: 0
|
|
46
|
+
},
|
|
47
|
+
M3T: {
|
|
48
|
+
droneEnumValue: 77,
|
|
49
|
+
droneSubEnumValue: 1
|
|
50
|
+
},
|
|
51
|
+
M3TA: {
|
|
52
|
+
droneEnumValue: 77,
|
|
53
|
+
droneSubEnumValue: 3
|
|
54
|
+
},
|
|
55
|
+
M3D: {
|
|
56
|
+
droneEnumValue: 91,
|
|
57
|
+
droneSubEnumValue: 0
|
|
58
|
+
},
|
|
59
|
+
M3TD: {
|
|
60
|
+
droneEnumValue: 91,
|
|
61
|
+
droneSubEnumValue: 1
|
|
62
|
+
},
|
|
63
|
+
M4D: {
|
|
64
|
+
droneEnumValue: 100,
|
|
65
|
+
droneSubEnumValue: 0
|
|
66
|
+
},
|
|
67
|
+
M4TD: {
|
|
68
|
+
droneEnumValue: 100,
|
|
69
|
+
droneSubEnumValue: 1
|
|
70
|
+
},
|
|
71
|
+
M4E: {
|
|
72
|
+
droneEnumValue: 99,
|
|
73
|
+
droneSubEnumValue: 0
|
|
74
|
+
},
|
|
75
|
+
M4T: {
|
|
76
|
+
droneEnumValue: 99,
|
|
77
|
+
droneSubEnumValue: 1
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/** Payload `{ payloadEnumValue, payloadPositionIndex }` tuples for common cameras. */
|
|
81
|
+
const PayloadEnum = {
|
|
82
|
+
M30Camera: {
|
|
83
|
+
payloadEnumValue: 52,
|
|
84
|
+
payloadPositionIndex: 0
|
|
85
|
+
},
|
|
86
|
+
M30TCamera: {
|
|
87
|
+
payloadEnumValue: 53,
|
|
88
|
+
payloadPositionIndex: 0
|
|
89
|
+
},
|
|
90
|
+
M3ECamera: {
|
|
91
|
+
payloadEnumValue: 66,
|
|
92
|
+
payloadPositionIndex: 0
|
|
93
|
+
},
|
|
94
|
+
M3TCamera: {
|
|
95
|
+
payloadEnumValue: 67,
|
|
96
|
+
payloadPositionIndex: 0
|
|
97
|
+
},
|
|
98
|
+
M3TACamera: {
|
|
99
|
+
payloadEnumValue: 129,
|
|
100
|
+
payloadPositionIndex: 0
|
|
101
|
+
},
|
|
102
|
+
M3DCamera: {
|
|
103
|
+
payloadEnumValue: 80,
|
|
104
|
+
payloadPositionIndex: 0
|
|
105
|
+
},
|
|
106
|
+
M3TDCamera: {
|
|
107
|
+
payloadEnumValue: 81,
|
|
108
|
+
payloadPositionIndex: 0
|
|
109
|
+
},
|
|
110
|
+
M4DCamera: {
|
|
111
|
+
payloadEnumValue: 98,
|
|
112
|
+
payloadPositionIndex: 0
|
|
113
|
+
},
|
|
114
|
+
M4TDCamera: {
|
|
115
|
+
payloadEnumValue: 99,
|
|
116
|
+
payloadPositionIndex: 0
|
|
117
|
+
},
|
|
118
|
+
M4ECamera: {
|
|
119
|
+
payloadEnumValue: 88,
|
|
120
|
+
payloadPositionIndex: 0
|
|
121
|
+
},
|
|
122
|
+
M4TCamera: {
|
|
123
|
+
payloadEnumValue: 89,
|
|
124
|
+
payloadPositionIndex: 0
|
|
125
|
+
},
|
|
126
|
+
ZenmuseZ30: {
|
|
127
|
+
payloadEnumValue: 20,
|
|
128
|
+
payloadPositionIndex: 0
|
|
129
|
+
},
|
|
130
|
+
ZenmuseXT2: {
|
|
131
|
+
payloadEnumValue: 26,
|
|
132
|
+
payloadPositionIndex: 0
|
|
133
|
+
},
|
|
134
|
+
ZenmuseXTS: {
|
|
135
|
+
payloadEnumValue: 41,
|
|
136
|
+
payloadPositionIndex: 0
|
|
137
|
+
},
|
|
138
|
+
ZenmuseH20: {
|
|
139
|
+
payloadEnumValue: 42,
|
|
140
|
+
payloadPositionIndex: 0
|
|
141
|
+
},
|
|
142
|
+
ZenmuseH20T: {
|
|
143
|
+
payloadEnumValue: 43,
|
|
144
|
+
payloadPositionIndex: 0
|
|
145
|
+
},
|
|
146
|
+
ZenmuseH20N: {
|
|
147
|
+
payloadEnumValue: 61,
|
|
148
|
+
payloadPositionIndex: 0
|
|
149
|
+
},
|
|
150
|
+
ZenmuseH30: {
|
|
151
|
+
payloadEnumValue: 82,
|
|
152
|
+
payloadPositionIndex: 0
|
|
153
|
+
},
|
|
154
|
+
ZenmuseH30T: {
|
|
155
|
+
payloadEnumValue: 83,
|
|
156
|
+
payloadPositionIndex: 0
|
|
157
|
+
},
|
|
158
|
+
FPVMatriceSeries: {
|
|
159
|
+
payloadEnumValue: 39,
|
|
160
|
+
payloadPositionIndex: 7
|
|
161
|
+
},
|
|
162
|
+
AuxiliaryM3xM4x: {
|
|
163
|
+
payloadEnumValue: 176,
|
|
164
|
+
payloadPositionIndex: 0
|
|
165
|
+
},
|
|
166
|
+
DockCamera: {
|
|
167
|
+
payloadEnumValue: 165,
|
|
168
|
+
payloadPositionIndex: 7
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/plan/geometry.ts
|
|
173
|
+
const EARTH_RADIUS = 6371e3;
|
|
174
|
+
const DEG2RAD = Math.PI / 180;
|
|
175
|
+
const RAD2DEG = 180 / Math.PI;
|
|
176
|
+
/** Project lng/lat to local ENU meters around `origin`. */
|
|
177
|
+
function lngLatToEnu(point, origin) {
|
|
178
|
+
const dLat = (point.lat - origin.lat) * DEG2RAD;
|
|
179
|
+
const dLng = (point.lng - origin.lng) * DEG2RAD;
|
|
180
|
+
const avgLat = (point.lat + origin.lat) / 2 * DEG2RAD;
|
|
181
|
+
return [dLng * EARTH_RADIUS * Math.cos(avgLat), dLat * EARTH_RADIUS];
|
|
182
|
+
}
|
|
183
|
+
/** Inverse of `lngLatToEnu`. */
|
|
184
|
+
function enuToLngLat([x, y], origin) {
|
|
185
|
+
const avgLat = origin.lat * DEG2RAD;
|
|
186
|
+
return {
|
|
187
|
+
lng: origin.lng + x / (EARTH_RADIUS * Math.cos(avgLat)) * RAD2DEG,
|
|
188
|
+
lat: origin.lat + y / EARTH_RADIUS * RAD2DEG
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function centroid(points) {
|
|
192
|
+
let sumLng = 0;
|
|
193
|
+
let sumLat = 0;
|
|
194
|
+
for (const p of points) {
|
|
195
|
+
sumLng += p.lng;
|
|
196
|
+
sumLat += p.lat;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
lng: sumLng / points.length,
|
|
200
|
+
lat: sumLat / points.length
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Bearing from `from` → `to` in degrees clockwise from North.
|
|
205
|
+
* ENU axes: x=east, y=north.
|
|
206
|
+
*/
|
|
207
|
+
function bearingDeg(from, to) {
|
|
208
|
+
return Math.atan2(to[0] - from[0], to[1] - from[1]) * RAD2DEG;
|
|
209
|
+
}
|
|
210
|
+
/** Clip a horizontal scan line `y = const` against a polygon; returns `[x_enter, x_exit]` pairs. */
|
|
211
|
+
function clipHorizontal(y, polygon) {
|
|
212
|
+
const intersections = [];
|
|
213
|
+
const n = polygon.length;
|
|
214
|
+
for (let i = 0; i < n; i++) {
|
|
215
|
+
const [x1, y1] = polygon[i];
|
|
216
|
+
const [x2, y2] = polygon[(i + 1) % n];
|
|
217
|
+
if (y1 <= y && y < y2 || y2 <= y && y < y1) {
|
|
218
|
+
const t = (y - y1) / (y2 - y1);
|
|
219
|
+
intersections.push(x1 + t * (x2 - x1));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
intersections.sort((a, b) => a - b);
|
|
223
|
+
const out = [];
|
|
224
|
+
for (let i = 0; i + 1 < intersections.length; i += 2) out.push([intersections[i], intersections[i + 1]]);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/** Serpentine boustrophedon scan over a polygon; returns ENU waypoints. */
|
|
228
|
+
function scanWaypoints(polygon, lineSpacing) {
|
|
229
|
+
let minY = Infinity;
|
|
230
|
+
let maxY = -Infinity;
|
|
231
|
+
for (const [, y] of polygon) {
|
|
232
|
+
if (y < minY) minY = y;
|
|
233
|
+
if (y > maxY) maxY = y;
|
|
234
|
+
}
|
|
235
|
+
const waypoints = [];
|
|
236
|
+
let leftToRight = true;
|
|
237
|
+
for (let y = minY; y <= maxY + lineSpacing * .5; y += lineSpacing) {
|
|
238
|
+
const segments = clipHorizontal(y, polygon);
|
|
239
|
+
for (const [x1, x2] of segments) if (leftToRight) waypoints.push([x1, y], [x2, y]);
|
|
240
|
+
else waypoints.push([x2, y], [x1, y]);
|
|
241
|
+
leftToRight = !leftToRight;
|
|
242
|
+
}
|
|
243
|
+
return waypoints;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Parse a KML coordinates string like `"lon,lat lon,lat lon,lat"` or `"lon,lat,alt ..."`
|
|
247
|
+
* into LngLat array (altitude dropped).
|
|
248
|
+
*/
|
|
249
|
+
function parseCoordinatesString(s) {
|
|
250
|
+
return s.trim().split(/\s+/).map((tuple) => {
|
|
251
|
+
const [lng, lat] = tuple.split(",").map(Number);
|
|
252
|
+
return {
|
|
253
|
+
lng,
|
|
254
|
+
lat
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/plan/plan-mapping2d.ts
|
|
260
|
+
/** Default HFOV (degrees) used to estimate ground footprint when payload-specific FOV unknown. */
|
|
261
|
+
const DEFAULT_HFOV_DEG$1 = 84;
|
|
262
|
+
function planMapping2d(template) {
|
|
263
|
+
const folder = template.Folder;
|
|
264
|
+
const ring = parseCoordinatesString(folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
265
|
+
const origin = centroid(ring);
|
|
266
|
+
const enuPolygon = ring.map((p) => lngLatToEnu(p, origin));
|
|
267
|
+
const hfovRad = DEFAULT_HFOV_DEG$1 / 2 * (Math.PI / 180);
|
|
268
|
+
const enuWps = scanWaypoints(enuPolygon, 2 * folder.height * Math.tan(hfovRad) * (1 - (folder.overlap.orthoCameraOverlapW ?? 70) / 100));
|
|
269
|
+
const lngLatWps = enuWps.map((p) => enuToLngLat(p, origin));
|
|
270
|
+
let actionGroupId = 0;
|
|
271
|
+
const placemarks = lngLatWps.map((coord, index) => {
|
|
272
|
+
const isLast = index === lngLatWps.length - 1;
|
|
273
|
+
const headingAngle = isLast ? 0 : bearingDeg(enuWps[index], enuWps[index + 1]);
|
|
274
|
+
const actionGroup = isLast ? [] : [{
|
|
275
|
+
actionGroupId: actionGroupId++,
|
|
276
|
+
actionGroupStartIndex: index,
|
|
277
|
+
actionGroupEndIndex: index + 1,
|
|
278
|
+
actionGroupMode: "sequence",
|
|
279
|
+
actionTrigger: { actionTriggerType: "betweenAdjacentPoints" },
|
|
280
|
+
action: [{
|
|
281
|
+
actionId: 0,
|
|
282
|
+
actionActuatorFunc: "takePhoto",
|
|
283
|
+
actionActuatorFuncParam: {
|
|
284
|
+
payloadPositionIndex: 0,
|
|
285
|
+
payloadLensIndex: ["wide"],
|
|
286
|
+
useGlobalPayloadLensIndex: false
|
|
287
|
+
}
|
|
288
|
+
}]
|
|
289
|
+
}];
|
|
290
|
+
return {
|
|
291
|
+
Point: { coordinates: coord },
|
|
292
|
+
index,
|
|
293
|
+
executeHeight: folder.height,
|
|
294
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
295
|
+
waypointHeadingParam: {
|
|
296
|
+
waypointHeadingMode: "followWayline",
|
|
297
|
+
waypointHeadingAngle: degrees(headingAngle),
|
|
298
|
+
waypointHeadingPathMode: "followBadArc"
|
|
299
|
+
},
|
|
300
|
+
waypointTurnParam: { waypointTurnMode: "toPointAndPassWithContinuityCurvature" },
|
|
301
|
+
...actionGroup.length > 0 ? { actionGroup } : {}
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
const waylineFolder = {
|
|
305
|
+
templateId: folder.templateId,
|
|
306
|
+
waylineId: 0,
|
|
307
|
+
executeHeightMode: heightModeToExecuteMode$3(folder),
|
|
308
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
309
|
+
startActionGroup: {
|
|
310
|
+
actionGroupId: 65535,
|
|
311
|
+
actionGroupStartIndex: 0,
|
|
312
|
+
actionGroupEndIndex: 0,
|
|
313
|
+
actionGroupMode: "sequence",
|
|
314
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
315
|
+
action: [{
|
|
316
|
+
actionId: 0,
|
|
317
|
+
actionActuatorFunc: "gimbalRotate",
|
|
318
|
+
actionActuatorFuncParam: {
|
|
319
|
+
payloadPositionIndex: 0,
|
|
320
|
+
gimbalHeadingYawBase: "north",
|
|
321
|
+
gimbalRotateMode: "absoluteAngle",
|
|
322
|
+
gimbalPitchRotateEnable: true,
|
|
323
|
+
gimbalPitchRotateAngle: degrees(-90),
|
|
324
|
+
gimbalRollRotateEnable: false,
|
|
325
|
+
gimbalRollRotateAngle: degrees(0),
|
|
326
|
+
gimbalYawRotateEnable: true,
|
|
327
|
+
gimbalYawRotateAngle: degrees(0),
|
|
328
|
+
gimbalRotateTimeEnable: false,
|
|
329
|
+
gimbalRotateTime: 10
|
|
330
|
+
}
|
|
331
|
+
}]
|
|
332
|
+
},
|
|
333
|
+
Placemark: placemarks
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
missionConfig: template.missionConfig,
|
|
337
|
+
Folder: [waylineFolder]
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function heightModeToExecuteMode$3(folder) {
|
|
341
|
+
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
342
|
+
case "EGM96": return "WGS84";
|
|
343
|
+
case "relativeToStartPoint": return "relativeToStartPoint";
|
|
344
|
+
case "aboveGroundLevel":
|
|
345
|
+
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/plan/plan-mapping3d.ts
|
|
350
|
+
const DEFAULT_HFOV_DEG = 84;
|
|
351
|
+
/**
|
|
352
|
+
* Compile a `mapping3d` (oblique photography) template into 5 waylines:
|
|
353
|
+
* one nadir (ortho) pass + four oblique passes (front/back/left/right).
|
|
354
|
+
*/
|
|
355
|
+
function planMapping3d(template) {
|
|
356
|
+
const folder = template.Folder;
|
|
357
|
+
const ring = parseCoordinatesString(folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
358
|
+
const origin = centroid(ring);
|
|
359
|
+
const enuPolygon = ring.map((p) => lngLatToEnu(p, origin));
|
|
360
|
+
const hfovRad = DEFAULT_HFOV_DEG / 2 * (Math.PI / 180);
|
|
361
|
+
const enuWps = scanWaypoints(enuPolygon, 2 * folder.height * Math.tan(hfovRad) * (1 - (folder.overlap.orthoCameraOverlapW ?? 70) / 100));
|
|
362
|
+
const Folder = [
|
|
363
|
+
{
|
|
364
|
+
headingAngle: 0,
|
|
365
|
+
gimbalPitch: -90,
|
|
366
|
+
minShootInterval: 2
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
headingAngle: 0,
|
|
370
|
+
gimbalPitch: folder.inclinedGimbalPitch,
|
|
371
|
+
minShootInterval: 2.5
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
headingAngle: 180,
|
|
375
|
+
gimbalPitch: folder.inclinedGimbalPitch,
|
|
376
|
+
minShootInterval: 2.5
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
headingAngle: 90,
|
|
380
|
+
gimbalPitch: folder.inclinedGimbalPitch,
|
|
381
|
+
minShootInterval: 2.5
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
headingAngle: -90,
|
|
385
|
+
gimbalPitch: folder.inclinedGimbalPitch,
|
|
386
|
+
minShootInterval: 2.5
|
|
387
|
+
}
|
|
388
|
+
].map((pass, i) => buildObliquePass(folder, i, origin, enuWps, pass.headingAngle, pass.gimbalPitch, pass.minShootInterval));
|
|
389
|
+
return {
|
|
390
|
+
missionConfig: template.missionConfig,
|
|
391
|
+
Folder
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function buildObliquePass(folder, waylineId, origin, enuWps, headingAngle, gimbalPitch, minShootInterval) {
|
|
395
|
+
const lastIndex = enuWps.length - 1;
|
|
396
|
+
const placemarks = enuWps.map((enu, index) => {
|
|
397
|
+
const placemark = {
|
|
398
|
+
Point: { coordinates: enuToLngLat(enu, origin) },
|
|
399
|
+
index,
|
|
400
|
+
executeHeight: folder.height,
|
|
401
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
402
|
+
waypointHeadingParam: {
|
|
403
|
+
waypointHeadingMode: "fixed",
|
|
404
|
+
waypointHeadingAngle: degrees(headingAngle),
|
|
405
|
+
waypointHeadingPathMode: "followBadArc"
|
|
406
|
+
},
|
|
407
|
+
waypointTurnParam: { waypointTurnMode: "toPointAndPassWithContinuityCurvature" }
|
|
408
|
+
};
|
|
409
|
+
const groups = waypointActionGroups(index, lastIndex, gimbalPitch, minShootInterval);
|
|
410
|
+
if (groups.length > 0) placemark.actionGroup = groups;
|
|
411
|
+
return placemark;
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
templateId: folder.templateId,
|
|
415
|
+
waylineId,
|
|
416
|
+
executeHeightMode: heightModeToExecuteMode$2(folder.waylineCoordinateSysParam.heightMode),
|
|
417
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
418
|
+
startActionGroup: startGimbalRotateGroup(gimbalPitch),
|
|
419
|
+
Placemark: placemarks
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function waypointActionGroups(index, lastIndex, gimbalPitch, minShootInterval) {
|
|
423
|
+
if (index === 0) return [{
|
|
424
|
+
actionGroupId: 0,
|
|
425
|
+
actionGroupStartIndex: 0,
|
|
426
|
+
actionGroupEndIndex: lastIndex,
|
|
427
|
+
actionGroupMode: "sequence",
|
|
428
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
429
|
+
action: [gimbalRotate(0, gimbalPitch)]
|
|
430
|
+
}, {
|
|
431
|
+
actionGroupId: 1,
|
|
432
|
+
actionGroupStartIndex: 0,
|
|
433
|
+
actionGroupEndIndex: lastIndex,
|
|
434
|
+
actionGroupMode: "sequence",
|
|
435
|
+
actionTrigger: {
|
|
436
|
+
actionTriggerType: "multipleTiming",
|
|
437
|
+
actionTriggerParam: minShootInterval
|
|
438
|
+
},
|
|
439
|
+
action: [takePhoto(0)]
|
|
440
|
+
}];
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
function startGimbalRotateGroup(gimbalPitch) {
|
|
444
|
+
return {
|
|
445
|
+
actionGroupId: 65535,
|
|
446
|
+
actionGroupStartIndex: 0,
|
|
447
|
+
actionGroupEndIndex: 0,
|
|
448
|
+
actionGroupMode: "sequence",
|
|
449
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
450
|
+
action: [gimbalRotate(0, gimbalPitch)]
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function gimbalRotate(actionId, pitch) {
|
|
454
|
+
return {
|
|
455
|
+
actionId,
|
|
456
|
+
actionActuatorFunc: "gimbalRotate",
|
|
457
|
+
actionActuatorFuncParam: {
|
|
458
|
+
payloadPositionIndex: 0,
|
|
459
|
+
gimbalHeadingYawBase: "north",
|
|
460
|
+
gimbalRotateMode: "absoluteAngle",
|
|
461
|
+
gimbalPitchRotateEnable: true,
|
|
462
|
+
gimbalPitchRotateAngle: degrees(pitch),
|
|
463
|
+
gimbalRollRotateEnable: false,
|
|
464
|
+
gimbalRollRotateAngle: degrees(0),
|
|
465
|
+
gimbalYawRotateEnable: false,
|
|
466
|
+
gimbalYawRotateAngle: degrees(0),
|
|
467
|
+
gimbalRotateTimeEnable: false,
|
|
468
|
+
gimbalRotateTime: 0
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function takePhoto(actionId) {
|
|
473
|
+
return {
|
|
474
|
+
actionId,
|
|
475
|
+
actionActuatorFunc: "takePhoto",
|
|
476
|
+
actionActuatorFuncParam: {
|
|
477
|
+
payloadPositionIndex: 0,
|
|
478
|
+
payloadLensIndex: ["wide"],
|
|
479
|
+
useGlobalPayloadLensIndex: false
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function heightModeToExecuteMode$2(mode) {
|
|
484
|
+
switch (mode) {
|
|
485
|
+
case "EGM96": return "WGS84";
|
|
486
|
+
case "relativeToStartPoint": return "relativeToStartPoint";
|
|
487
|
+
case "aboveGroundLevel":
|
|
488
|
+
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/plan/plan-mapping-strip.ts
|
|
493
|
+
/**
|
|
494
|
+
* Compile a `mappingStrip` template into a single wayline that follows the
|
|
495
|
+
* supplied LineString. Each LineString vertex becomes one waypoint.
|
|
496
|
+
*
|
|
497
|
+
* When `stripUseTemplateAltitude` is true the altitude in each LineString
|
|
498
|
+
* triple is used as the executeHeight; otherwise the folder's `height` is used.
|
|
499
|
+
*/
|
|
500
|
+
function planMappingStrip(template) {
|
|
501
|
+
const folder = template.Folder;
|
|
502
|
+
const coords = parseTriples(folder.Placemark.LineString.coordinates);
|
|
503
|
+
const origin = centroid(coords.map((c) => ({
|
|
504
|
+
lng: c.lng,
|
|
505
|
+
lat: c.lat
|
|
506
|
+
})));
|
|
507
|
+
const enuPath = coords.map((c) => lngLatToEnu({
|
|
508
|
+
lng: c.lng,
|
|
509
|
+
lat: c.lat
|
|
510
|
+
}, origin));
|
|
511
|
+
const placemarks = coords.map((c, index) => {
|
|
512
|
+
const headingAngle = index === coords.length - 1 ? 0 : bearingDeg(enuPath[index], enuPath[index + 1]);
|
|
513
|
+
const executeHeight = folder.stripUseTemplateAltitude && c.alt !== void 0 ? c.alt : folder.height;
|
|
514
|
+
return {
|
|
515
|
+
Point: { coordinates: {
|
|
516
|
+
lng: c.lng,
|
|
517
|
+
lat: c.lat
|
|
518
|
+
} },
|
|
519
|
+
index,
|
|
520
|
+
executeHeight,
|
|
521
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
522
|
+
waypointHeadingParam: {
|
|
523
|
+
waypointHeadingMode: "followWayline",
|
|
524
|
+
waypointHeadingAngle: degrees(headingAngle),
|
|
525
|
+
waypointHeadingPathMode: "followBadArc"
|
|
526
|
+
},
|
|
527
|
+
waypointTurnParam: { waypointTurnMode: "toPointAndPassWithContinuityCurvature" }
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
const waylineFolder = {
|
|
531
|
+
templateId: folder.templateId,
|
|
532
|
+
waylineId: 0,
|
|
533
|
+
executeHeightMode: heightModeToExecuteMode$1(folder),
|
|
534
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
535
|
+
Placemark: placemarks
|
|
536
|
+
};
|
|
537
|
+
return {
|
|
538
|
+
missionConfig: template.missionConfig,
|
|
539
|
+
Folder: [waylineFolder]
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function parseTriples(s) {
|
|
543
|
+
return s.trim().split(/\s+/).map((tuple) => {
|
|
544
|
+
const parts = tuple.split(",").map(Number);
|
|
545
|
+
return {
|
|
546
|
+
lng: parts[0],
|
|
547
|
+
lat: parts[1],
|
|
548
|
+
...parts[2] !== void 0 ? { alt: parts[2] } : {}
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function heightModeToExecuteMode$1(folder) {
|
|
553
|
+
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
554
|
+
case "EGM96": return "WGS84";
|
|
555
|
+
case "relativeToStartPoint": return "relativeToStartPoint";
|
|
556
|
+
case "aboveGroundLevel":
|
|
557
|
+
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plan/plan-waypoint.ts
|
|
562
|
+
/**
|
|
563
|
+
* Compile a `waypoint` template directly: each Placemark in the template becomes
|
|
564
|
+
* one waypoint in the wayline, resolving `useGlobalXxx` flags against folder defaults.
|
|
565
|
+
*/
|
|
566
|
+
function planWaypoint(template) {
|
|
567
|
+
const folder = template.Folder;
|
|
568
|
+
const placemarks = folder.Placemark.map((p, i) => {
|
|
569
|
+
const heading = p.useGlobalHeadingParam || p.waypointHeadingParam === void 0 ? folder.globalWaypointHeadingParam : p.waypointHeadingParam;
|
|
570
|
+
const turn = p.useGlobalTurnParam || p.waypointTurnParam === void 0 ? { waypointTurnMode: folder.globalWaypointTurnMode } : p.waypointTurnParam;
|
|
571
|
+
const executeHeight = p.useGlobalHeight || p.height === void 0 ? folder.globalHeight : p.height;
|
|
572
|
+
const waypointSpeed = p.useGlobalSpeed || p.waypointSpeed === void 0 ? folder.autoFlightSpeed : p.waypointSpeed;
|
|
573
|
+
return {
|
|
574
|
+
Point: p.Point,
|
|
575
|
+
index: i,
|
|
576
|
+
executeHeight,
|
|
577
|
+
waypointSpeed,
|
|
578
|
+
waypointHeadingParam: heading,
|
|
579
|
+
waypointTurnParam: turn,
|
|
580
|
+
...p.useStraightLine !== void 0 ? { useStraightLine: p.useStraightLine } : {},
|
|
581
|
+
...p.isRisky !== void 0 ? { isRisky: p.isRisky } : {},
|
|
582
|
+
...p.actionGroup !== void 0 ? { actionGroup: p.actionGroup } : {}
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
const waylineFolder = {
|
|
586
|
+
templateId: folder.templateId,
|
|
587
|
+
waylineId: 0,
|
|
588
|
+
executeHeightMode: heightModeToExecuteMode(folder.waylineCoordinateSysParam.heightMode),
|
|
589
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
590
|
+
Placemark: placemarks
|
|
591
|
+
};
|
|
592
|
+
return {
|
|
593
|
+
missionConfig: template.missionConfig,
|
|
594
|
+
Folder: [waylineFolder]
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Map a template heightMode to a waylines executeHeightMode.
|
|
599
|
+
* - `EGM96` → `WGS84` (waylines spec doesn't expose EGM96; runtime resolves it).
|
|
600
|
+
* - `aboveGroundLevel` → `realTimeFollowSurface` (closest executable analog).
|
|
601
|
+
*/
|
|
602
|
+
function heightModeToExecuteMode(mode) {
|
|
603
|
+
switch (mode) {
|
|
604
|
+
case "EGM96": return "WGS84";
|
|
605
|
+
case "relativeToStartPoint": return "relativeToStartPoint";
|
|
606
|
+
case "aboveGroundLevel":
|
|
607
|
+
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/plan/index.ts
|
|
612
|
+
const isWaypoint = (t) => t.Folder.templateType === "waypoint";
|
|
613
|
+
const isMapping2d = (t) => t.Folder.templateType === "mapping2d";
|
|
614
|
+
const isMapping3d = (t) => t.Folder.templateType === "mapping3d";
|
|
615
|
+
const isMappingStrip = (t) => t.Folder.templateType === "mappingStrip";
|
|
616
|
+
/**
|
|
617
|
+
* Compile a high-level `Template` into executable `Waylines`.
|
|
618
|
+
* Dispatch is by `template.Folder.templateType` per the DJI WPML spec.
|
|
619
|
+
*/
|
|
620
|
+
function plan(template) {
|
|
621
|
+
if (isWaypoint(template)) return planWaypoint(template);
|
|
622
|
+
if (isMapping2d(template)) return planMapping2d(template);
|
|
623
|
+
if (isMapping3d(template)) return planMapping3d(template);
|
|
624
|
+
if (isMappingStrip(template)) return planMappingStrip(template);
|
|
625
|
+
throw new PlannerError(`Unknown templateType: ${template.Folder.templateType}`);
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/validate/validate-mission-config.ts
|
|
629
|
+
function validateMissionConfig(config, path) {
|
|
630
|
+
const issues = [];
|
|
631
|
+
if (config.exitOnRCLost === "executeLostAction" && config.executeRCLostAction === void 0) issues.push({
|
|
632
|
+
path: `${path}.executeRCLostAction`,
|
|
633
|
+
message: "executeRCLostAction is required when exitOnRCLost === 'executeLostAction'",
|
|
634
|
+
severity: "error"
|
|
635
|
+
});
|
|
636
|
+
if (config.globalTransitionalSpeed < 1 || config.globalTransitionalSpeed > 15) issues.push({
|
|
637
|
+
path: `${path}.globalTransitionalSpeed`,
|
|
638
|
+
message: `globalTransitionalSpeed must be in [1,15] m/s, got ${config.globalTransitionalSpeed}`,
|
|
639
|
+
severity: "error"
|
|
640
|
+
});
|
|
641
|
+
if (config.globalRTHHeight < 2 || config.globalRTHHeight > 1500) issues.push({
|
|
642
|
+
path: `${path}.globalRTHHeight`,
|
|
643
|
+
message: `globalRTHHeight must be in [2,1500] m, got ${config.globalRTHHeight}`,
|
|
644
|
+
severity: "error"
|
|
645
|
+
});
|
|
646
|
+
if (config.takeOffSecurityHeight < 1.2 || config.takeOffSecurityHeight > 1500) issues.push({
|
|
647
|
+
path: `${path}.takeOffSecurityHeight`,
|
|
648
|
+
message: `takeOffSecurityHeight must be in [1.2,1500] m, got ${config.takeOffSecurityHeight}`,
|
|
649
|
+
severity: "error"
|
|
650
|
+
});
|
|
651
|
+
if (config.payloadInfo.length === 0) issues.push({
|
|
652
|
+
path: `${path}.payloadInfo`,
|
|
653
|
+
message: "payloadInfo must contain at least one payload",
|
|
654
|
+
severity: "warning"
|
|
655
|
+
});
|
|
656
|
+
return issues;
|
|
657
|
+
}
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/validate/validate-template.ts
|
|
660
|
+
function validateTemplate(template) {
|
|
661
|
+
const folder = template.Folder;
|
|
662
|
+
const issues = [];
|
|
663
|
+
const path = "Folder";
|
|
664
|
+
if (folder.templateId < 0 || folder.templateId > 65535) issues.push({
|
|
665
|
+
path: `${path}.templateId`,
|
|
666
|
+
message: `templateId must be in [0,65535], got ${folder.templateId}`,
|
|
667
|
+
severity: "error"
|
|
668
|
+
});
|
|
669
|
+
if (folder.autoFlightSpeed < 1 || folder.autoFlightSpeed > 15) issues.push({
|
|
670
|
+
path: `${path}.autoFlightSpeed`,
|
|
671
|
+
message: `autoFlightSpeed must be in [1,15] m/s, got ${folder.autoFlightSpeed}`,
|
|
672
|
+
severity: "error"
|
|
673
|
+
});
|
|
674
|
+
switch (folder.templateType) {
|
|
675
|
+
case "waypoint":
|
|
676
|
+
issues.push(...validateWaypointFolder(template, path));
|
|
677
|
+
break;
|
|
678
|
+
case "mapping2d":
|
|
679
|
+
issues.push(...validateMapping2dFolder(template, path));
|
|
680
|
+
break;
|
|
681
|
+
case "mapping3d":
|
|
682
|
+
issues.push(...validateMapping3dFolder(template, path));
|
|
683
|
+
break;
|
|
684
|
+
case "mappingStrip":
|
|
685
|
+
issues.push(...validateMappingStripFolder(template, path));
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
return issues;
|
|
689
|
+
}
|
|
690
|
+
function validateWaypointFolder(t, path) {
|
|
691
|
+
const issues = [];
|
|
692
|
+
if (t.Folder.Placemark.length === 0) issues.push({
|
|
693
|
+
path: `${path}.Placemark`,
|
|
694
|
+
message: "waypoint template must have at least one Placemark",
|
|
695
|
+
severity: "error"
|
|
696
|
+
});
|
|
697
|
+
return issues;
|
|
698
|
+
}
|
|
699
|
+
function validateMapping2dFolder(t, path) {
|
|
700
|
+
return validatePolygonRing(t.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates, `${path}.Placemark.Polygon`);
|
|
701
|
+
}
|
|
702
|
+
function validateMapping3dFolder(t, path) {
|
|
703
|
+
return validatePolygonRing(t.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates, `${path}.Placemark.Polygon`);
|
|
704
|
+
}
|
|
705
|
+
function validateMappingStripFolder(t, path) {
|
|
706
|
+
const tuples = t.Folder.Placemark.LineString.coordinates.trim().split(/\s+/);
|
|
707
|
+
if (tuples.length < 2) return [{
|
|
708
|
+
path: `${path}.Placemark.LineString`,
|
|
709
|
+
message: `LineString must have at least 2 vertices, got ${tuples.length}`,
|
|
710
|
+
severity: "error"
|
|
711
|
+
}];
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
function validatePolygonRing(coords, path) {
|
|
715
|
+
const tuples = coords.trim().split(/\s+/);
|
|
716
|
+
if (tuples.length < 3) return [{
|
|
717
|
+
path,
|
|
718
|
+
message: `Polygon boundary must have at least 3 vertices, got ${tuples.length}`,
|
|
719
|
+
severity: "error"
|
|
720
|
+
}];
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/validate/validate-waylines.ts
|
|
725
|
+
function validateWaylines(waylines) {
|
|
726
|
+
const issues = [];
|
|
727
|
+
if (waylines.Folder.length === 0) issues.push({
|
|
728
|
+
path: "Folder",
|
|
729
|
+
message: "Waylines must contain at least one Folder",
|
|
730
|
+
severity: "error"
|
|
731
|
+
});
|
|
732
|
+
waylines.Folder.forEach((folder, i) => {
|
|
733
|
+
issues.push(...validateFolder(folder, `Folder[${i}]`));
|
|
734
|
+
});
|
|
735
|
+
return issues;
|
|
736
|
+
}
|
|
737
|
+
function validateFolder(folder, path) {
|
|
738
|
+
const issues = [];
|
|
739
|
+
if (folder.autoFlightSpeed < 1 || folder.autoFlightSpeed > 15) issues.push({
|
|
740
|
+
path: `${path}.autoFlightSpeed`,
|
|
741
|
+
message: `autoFlightSpeed must be in [1,15] m/s, got ${folder.autoFlightSpeed}`,
|
|
742
|
+
severity: "error"
|
|
743
|
+
});
|
|
744
|
+
if (folder.Placemark.length === 0) issues.push({
|
|
745
|
+
path: `${path}.Placemark`,
|
|
746
|
+
message: "Folder must contain at least one Placemark",
|
|
747
|
+
severity: "error"
|
|
748
|
+
});
|
|
749
|
+
folder.Placemark.forEach((p, i) => {
|
|
750
|
+
if (p.index !== i) issues.push({
|
|
751
|
+
path: `${path}.Placemark[${i}].index`,
|
|
752
|
+
message: `Placemark.index must equal its array position (expected ${i}, got ${p.index})`,
|
|
753
|
+
severity: "error"
|
|
754
|
+
});
|
|
755
|
+
if (p.waypointSpeed < 1 || p.waypointSpeed > 15) issues.push({
|
|
756
|
+
path: `${path}.Placemark[${i}].waypointSpeed`,
|
|
757
|
+
message: `waypointSpeed must be in [1,15] m/s, got ${p.waypointSpeed}`,
|
|
758
|
+
severity: "error"
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
return issues;
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/validate/index.ts
|
|
765
|
+
/**
|
|
766
|
+
* Run semantic checks against a `Template` or `Waylines` document.
|
|
767
|
+
* Returns a list of issues (`severity: "error" | "warning"`). Empty array = valid.
|
|
768
|
+
*
|
|
769
|
+
* The function is non-throwing by design — callers decide how to react.
|
|
770
|
+
*/
|
|
771
|
+
function validate(doc) {
|
|
772
|
+
const issues = [];
|
|
773
|
+
issues.push(...validateMissionConfig(doc.missionConfig, "missionConfig"));
|
|
774
|
+
if ("Folder" in doc && Array.isArray(doc.Folder)) issues.push(...validateWaylines(doc));
|
|
775
|
+
else issues.push(...validateTemplate(doc));
|
|
776
|
+
return issues;
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
export { DroneEnum, KmzError, PayloadEnum, PlannerError, degrees, meters, metersPerSecond, plan, validate };
|