@lumikmz/kmz 0.2.1 → 0.4.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
package/dist/index.mjs
CHANGED
|
@@ -81,46 +81,57 @@ const DroneEnum = {
|
|
|
81
81
|
const PayloadEnum = {
|
|
82
82
|
M30Camera: {
|
|
83
83
|
payloadEnumValue: 52,
|
|
84
|
+
payloadSubEnumValue: 0,
|
|
84
85
|
payloadPositionIndex: 0
|
|
85
86
|
},
|
|
86
87
|
M30TCamera: {
|
|
87
88
|
payloadEnumValue: 53,
|
|
89
|
+
payloadSubEnumValue: 0,
|
|
88
90
|
payloadPositionIndex: 0
|
|
89
91
|
},
|
|
90
92
|
M3ECamera: {
|
|
91
93
|
payloadEnumValue: 66,
|
|
94
|
+
payloadSubEnumValue: 0,
|
|
92
95
|
payloadPositionIndex: 0
|
|
93
96
|
},
|
|
94
97
|
M3TCamera: {
|
|
95
98
|
payloadEnumValue: 67,
|
|
99
|
+
payloadSubEnumValue: 0,
|
|
96
100
|
payloadPositionIndex: 0
|
|
97
101
|
},
|
|
98
102
|
M3TACamera: {
|
|
99
103
|
payloadEnumValue: 129,
|
|
104
|
+
payloadSubEnumValue: 0,
|
|
100
105
|
payloadPositionIndex: 0
|
|
101
106
|
},
|
|
102
107
|
M3DCamera: {
|
|
103
108
|
payloadEnumValue: 80,
|
|
109
|
+
payloadSubEnumValue: 0,
|
|
104
110
|
payloadPositionIndex: 0
|
|
105
111
|
},
|
|
106
112
|
M3TDCamera: {
|
|
107
113
|
payloadEnumValue: 81,
|
|
114
|
+
payloadSubEnumValue: 0,
|
|
108
115
|
payloadPositionIndex: 0
|
|
109
116
|
},
|
|
110
117
|
M4DCamera: {
|
|
111
118
|
payloadEnumValue: 98,
|
|
119
|
+
payloadSubEnumValue: 0,
|
|
112
120
|
payloadPositionIndex: 0
|
|
113
121
|
},
|
|
114
122
|
M4TDCamera: {
|
|
115
123
|
payloadEnumValue: 99,
|
|
124
|
+
payloadSubEnumValue: 0,
|
|
116
125
|
payloadPositionIndex: 0
|
|
117
126
|
},
|
|
118
127
|
M4ECamera: {
|
|
119
128
|
payloadEnumValue: 88,
|
|
129
|
+
payloadSubEnumValue: 0,
|
|
120
130
|
payloadPositionIndex: 0
|
|
121
131
|
},
|
|
122
132
|
M4TCamera: {
|
|
123
133
|
payloadEnumValue: 89,
|
|
134
|
+
payloadSubEnumValue: 0,
|
|
124
135
|
payloadPositionIndex: 0
|
|
125
136
|
},
|
|
126
137
|
ZenmuseZ30: {
|
|
@@ -207,6 +218,27 @@ function centroid(points) {
|
|
|
207
218
|
function bearingDeg(from, to) {
|
|
208
219
|
return Math.atan2(to[0] - from[0], to[1] - from[1]) * RAD2DEG;
|
|
209
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Orient an ordered survey path so its first point is the end nearest `home`.
|
|
223
|
+
* A boustrophedon grid has two natural entry ends; the aircraft should start at
|
|
224
|
+
* whichever is closer to the take-off point. Returns the input unchanged when
|
|
225
|
+
* `home` is absent or the start is already the nearer end.
|
|
226
|
+
*/
|
|
227
|
+
function orientPathToHome(enuWps, origin, home) {
|
|
228
|
+
if (!home || enuWps.length < 2) return enuWps;
|
|
229
|
+
const h = lngLatToEnu(home, origin);
|
|
230
|
+
const sqDist = (p) => (p[0] - h[0]) ** 2 + (p[1] - h[1]) ** 2;
|
|
231
|
+
return sqDist(enuWps[enuWps.length - 1]) < sqDist(enuWps[0]) ? [...enuWps].reverse() : enuWps;
|
|
232
|
+
}
|
|
233
|
+
/** Great-circle distance between two WGS-84 coordinates, in meters. */
|
|
234
|
+
function haversineMeters(a, b) {
|
|
235
|
+
const dLat = (b.lat - a.lat) * DEG2RAD;
|
|
236
|
+
const dLng = (b.lng - a.lng) * DEG2RAD;
|
|
237
|
+
const lat1 = a.lat * DEG2RAD;
|
|
238
|
+
const lat2 = b.lat * DEG2RAD;
|
|
239
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
|
|
240
|
+
return EARTH_RADIUS * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
241
|
+
}
|
|
210
242
|
/** Clip a horizontal scan line `y = const` against a polygon; returns `[x_enter, x_exit]` pairs. */
|
|
211
243
|
function clipHorizontal(y, polygon) {
|
|
212
244
|
const intersections = [];
|
|
@@ -224,24 +256,6 @@ function clipHorizontal(y, polygon) {
|
|
|
224
256
|
for (let i = 0; i + 1 < intersections.length; i += 2) out.push([intersections[i], intersections[i + 1]]);
|
|
225
257
|
return out;
|
|
226
258
|
}
|
|
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
259
|
/**
|
|
246
260
|
* Parse a KML coordinates string like `"lon,lat lon,lat lon,lat"` or `"lon,lat,alt ..."`
|
|
247
261
|
* into LngLat array (altitude dropped).
|
|
@@ -255,211 +269,672 @@ function parseCoordinatesString(s) {
|
|
|
255
269
|
};
|
|
256
270
|
});
|
|
257
271
|
}
|
|
272
|
+
/** Rotate an ENU point counter-clockwise by `rad` radians about the origin. */
|
|
273
|
+
function rotateEnu([x, y], rad) {
|
|
274
|
+
const c = Math.cos(rad);
|
|
275
|
+
const s = Math.sin(rad);
|
|
276
|
+
return [x * c - y * s, x * s + y * c];
|
|
277
|
+
}
|
|
278
|
+
/** Drop a closing duplicate and any consecutive vertices closer than `epsM`. */
|
|
279
|
+
function dedupeRing(ring, epsM = .5) {
|
|
280
|
+
const pts = [];
|
|
281
|
+
for (const p of ring) {
|
|
282
|
+
const last = pts[pts.length - 1];
|
|
283
|
+
if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < epsM) continue;
|
|
284
|
+
pts.push([p[0], p[1]]);
|
|
285
|
+
}
|
|
286
|
+
const f = pts[0];
|
|
287
|
+
const l = pts[pts.length - 1];
|
|
288
|
+
if (pts.length >= 2 && f && l && Math.hypot(f[0] - l[0], f[1] - l[1]) < epsM) pts.pop();
|
|
289
|
+
return pts;
|
|
290
|
+
}
|
|
291
|
+
/** Beyond this multiple of `distM`, a corner miter is clamped to avoid spikes. */
|
|
292
|
+
const OFFSET_MITER_LIMIT = 2.5;
|
|
293
|
+
/**
|
|
294
|
+
* Outward-offset (dilate) a simple polygon ring by `distM` meters in ENU.
|
|
295
|
+
* Each vertex moves along the bisector of its two adjacent outward edge normals
|
|
296
|
+
* by `distM / cos(halfAngle)`, clamped by a miter limit. The bisector form is
|
|
297
|
+
* stable when adjacent edges are near-collinear (where edge-intersection would
|
|
298
|
+
* shoot a spike far outward); the clamp bounds sharp corners; degenerate/duplicate
|
|
299
|
+
* vertices are removed first. Intended for convex-ish survey areas.
|
|
300
|
+
*/
|
|
301
|
+
function offsetPolygon(ring, distM) {
|
|
302
|
+
const pts = dedupeRing(ring);
|
|
303
|
+
const n = pts.length;
|
|
304
|
+
if (distM === 0 || n < 3) return pts;
|
|
305
|
+
let area2 = 0;
|
|
306
|
+
for (let i = 0; i < n; i++) {
|
|
307
|
+
const a = pts[i];
|
|
308
|
+
const b = pts[(i + 1) % n];
|
|
309
|
+
area2 += a[0] * b[1] - b[0] * a[1];
|
|
310
|
+
}
|
|
311
|
+
const ccw = area2 > 0;
|
|
312
|
+
const outwardNormal = (dx, dy) => ccw ? [dy, -dx] : [-dy, dx];
|
|
313
|
+
const out = [];
|
|
314
|
+
for (let i = 0; i < n; i++) {
|
|
315
|
+
const prev = pts[(i - 1 + n) % n];
|
|
316
|
+
const cur = pts[i];
|
|
317
|
+
const next = pts[(i + 1) % n];
|
|
318
|
+
const inLen = Math.hypot(cur[0] - prev[0], cur[1] - prev[1]) || 1;
|
|
319
|
+
const outLen = Math.hypot(next[0] - cur[0], next[1] - cur[1]) || 1;
|
|
320
|
+
const nIn = outwardNormal((cur[0] - prev[0]) / inLen, (cur[1] - prev[1]) / inLen);
|
|
321
|
+
const nOut = outwardNormal((next[0] - cur[0]) / outLen, (next[1] - cur[1]) / outLen);
|
|
322
|
+
let bx = nIn[0] + nOut[0];
|
|
323
|
+
let by = nIn[1] + nOut[1];
|
|
324
|
+
const bl = Math.hypot(bx, by);
|
|
325
|
+
if (bl < 1e-9) {
|
|
326
|
+
out.push([cur[0] + nOut[0] * distM, cur[1] + nOut[1] * distM]);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
bx /= bl;
|
|
330
|
+
by /= bl;
|
|
331
|
+
const cosHalf = nOut[0] * bx + nOut[1] * by;
|
|
332
|
+
const scale = distM / Math.max(cosHalf, 1 / OFFSET_MITER_LIMIT);
|
|
333
|
+
out.push([cur[0] + bx * scale, cur[1] + by * scale]);
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Boustrophedon survey grid over an arbitrary simple polygon, aligned to `directionDeg`.
|
|
339
|
+
* Flight lines run parallel to the heading; consecutive lines step by `lineSpacing`.
|
|
340
|
+
* `marginM` extends each line lengthwise past the polygon edge.
|
|
341
|
+
* Returns ENU waypoints in flight order — no transit/entry point
|
|
342
|
+
* (entry climb is a missionConfig behaviour, not a grid concern).
|
|
343
|
+
*/
|
|
344
|
+
function surveyGrid(ring, opts) {
|
|
345
|
+
const { directionDeg, marginM, lineSpacing, phaseOffsetM = 0, lengthwiseShiftM = 0 } = opts;
|
|
346
|
+
const t = directionDeg * DEG2RAD;
|
|
347
|
+
const alpha = Math.atan2(Math.cos(t), Math.sin(t));
|
|
348
|
+
const scanRing = ring.map((p) => rotateEnu(p, -alpha));
|
|
349
|
+
let minY = Infinity;
|
|
350
|
+
let maxY = -Infinity;
|
|
351
|
+
for (const [, y] of scanRing) {
|
|
352
|
+
if (y < minY) minY = y;
|
|
353
|
+
if (y > maxY) maxY = y;
|
|
354
|
+
}
|
|
355
|
+
const scanPts = [];
|
|
356
|
+
let leftToRight = true;
|
|
357
|
+
for (let y = minY + phaseOffsetM; y <= maxY + lineSpacing * .5; y += lineSpacing) {
|
|
358
|
+
for (const [x1, x2] of clipHorizontal(y, scanRing)) {
|
|
359
|
+
const a = x1 - marginM + lengthwiseShiftM;
|
|
360
|
+
const b = x2 + marginM + lengthwiseShiftM;
|
|
361
|
+
if (leftToRight) scanPts.push([a, y], [b, y]);
|
|
362
|
+
else scanPts.push([b, y], [a, y]);
|
|
363
|
+
}
|
|
364
|
+
leftToRight = !leftToRight;
|
|
365
|
+
}
|
|
366
|
+
return scanPts.map((p) => rotateEnu(p, alpha));
|
|
367
|
+
}
|
|
258
368
|
//#endregion
|
|
259
369
|
//#region src/plan/plan-mapping2d.ts
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
370
|
+
/**
|
|
371
|
+
* `standoffM` dilates the survey polygon outward before gridding (smart-oblique
|
|
372
|
+
* needs the grid to overfly the boundary on every side; 0 for ortho/quick-ortho).
|
|
373
|
+
*/
|
|
374
|
+
function buildGrid(template, lineSpacing, standoffM = 0) {
|
|
263
375
|
const folder = template.Folder;
|
|
264
376
|
const ring = parseCoordinatesString(folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
265
377
|
const origin = centroid(ring);
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
378
|
+
const enuRing = ring.map((p) => lngLatToEnu(p, origin));
|
|
379
|
+
return {
|
|
380
|
+
folder,
|
|
381
|
+
origin,
|
|
382
|
+
enuWps: orientPathToHome(surveyGrid(standoffM > 0 ? offsetPolygon(enuRing, standoffM) : enuRing, {
|
|
383
|
+
directionDeg: folder.Placemark.direction,
|
|
384
|
+
marginM: folder.Placemark.margin,
|
|
385
|
+
lineSpacing
|
|
386
|
+
}), origin, template.missionConfig.takeOffRefPoint)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Smart-oblique stand-off (meters). The gimbal swings up to |swing|° off nadir in
|
|
391
|
+
* all directions during a single pass, so the most-oblique look images h·tan(|swing|)
|
|
392
|
+
* away from the aircraft. The survey polygon is therefore dilated by this distance so
|
|
393
|
+
* the grid overflies the boundary on every side. Clamp to 3·h.
|
|
394
|
+
*/
|
|
395
|
+
function smartObliqueStandoff(folder) {
|
|
396
|
+
const h = Number(folder.Placemark.height);
|
|
397
|
+
const swingAbs = Math.abs(Number(folder.Placemark.smartObliqueGimbalPitch ?? degrees(-45)));
|
|
398
|
+
if (!(swingAbs > 0 && swingAbs < 90)) return 0;
|
|
399
|
+
return Math.min(h * Math.tan(swingAbs * Math.PI / 180), h * 3);
|
|
400
|
+
}
|
|
401
|
+
function heightModeToExecuteMode$3(folder) {
|
|
402
|
+
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
403
|
+
case "EGM96": return "WGS84";
|
|
404
|
+
case "relativeToStartPoint": return "relativeToStartPoint";
|
|
405
|
+
case "aboveGroundLevel":
|
|
406
|
+
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Desired damping distance; clamped down when segments are short. */
|
|
410
|
+
const DESIRED_TURN_DAMPING_M = 10;
|
|
411
|
+
/** Never consume more than this fraction of either adjacent segment. */
|
|
412
|
+
const MAX_DAMPING_FRAC = .45;
|
|
413
|
+
/** Flat ENU distance between two grid points (metres). */
|
|
414
|
+
function enuSegLen(a, b) {
|
|
415
|
+
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Compute the coordinateTurn damping distance for interior waypoint `i`.
|
|
419
|
+
*
|
|
420
|
+
* Spec constraint: dampingDist[i] + dampingDist[i+1] < segmentLength(i, i+1).
|
|
421
|
+
* Clamping each side to MAX_DAMPING_FRAC × its shorter adjacent segment
|
|
422
|
+
* guarantees the sum is at most 2 × MAX_DAMPING_FRAC < 1 of that segment.
|
|
423
|
+
*/
|
|
424
|
+
function turnDamping(enuWps, i) {
|
|
425
|
+
const dPrev = enuSegLen(enuWps[i - 1], enuWps[i]);
|
|
426
|
+
const dNext = enuSegLen(enuWps[i], enuWps[i + 1]);
|
|
427
|
+
return meters(Math.min(DESIRED_TURN_DAMPING_M, dPrev * MAX_DAMPING_FRAC, dNext * MAX_DAMPING_FRAC));
|
|
428
|
+
}
|
|
429
|
+
/** Nadir gimbal-down, hover, manual focus, infinite focus, hover — per reference WPML. */
|
|
430
|
+
function buildStartActionGroup(pitchDeg) {
|
|
431
|
+
return { action: [
|
|
432
|
+
{
|
|
433
|
+
actionId: 0,
|
|
434
|
+
actionActuatorFunc: "gimbalRotate",
|
|
435
|
+
actionActuatorFuncParam: {
|
|
436
|
+
payloadPositionIndex: 0,
|
|
437
|
+
gimbalHeadingYawBase: "aircraft",
|
|
438
|
+
gimbalRotateMode: "absoluteAngle",
|
|
439
|
+
gimbalPitchRotateEnable: true,
|
|
440
|
+
gimbalPitchRotateAngle: degrees(pitchDeg),
|
|
441
|
+
gimbalRollRotateEnable: false,
|
|
442
|
+
gimbalRollRotateAngle: degrees(0),
|
|
443
|
+
gimbalYawRotateEnable: true,
|
|
444
|
+
gimbalYawRotateAngle: degrees(0),
|
|
445
|
+
gimbalRotateTimeEnable: false,
|
|
446
|
+
gimbalRotateTime: 10
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
actionId: 1,
|
|
451
|
+
actionActuatorFunc: "hover",
|
|
452
|
+
actionActuatorFuncParam: { hoverTime: .5 }
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
actionId: 2,
|
|
456
|
+
actionActuatorFunc: "setFocusType",
|
|
457
|
+
actionActuatorFuncParam: {
|
|
458
|
+
payloadPositionIndex: 0,
|
|
459
|
+
cameraFocusType: "manual"
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
actionId: 3,
|
|
464
|
+
actionActuatorFunc: "focus",
|
|
465
|
+
actionActuatorFuncParam: {
|
|
466
|
+
payloadPositionIndex: 0,
|
|
467
|
+
isPointFocus: false,
|
|
468
|
+
focusX: 0,
|
|
469
|
+
focusY: 0,
|
|
470
|
+
focusRegionWidth: 0,
|
|
471
|
+
focusRegionHeight: 0,
|
|
472
|
+
isInfiniteFocus: true,
|
|
473
|
+
isCalibrationFocus: false
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
actionId: 4,
|
|
478
|
+
actionActuatorFunc: "hover",
|
|
479
|
+
actionActuatorFuncParam: { hoverTime: 1 }
|
|
480
|
+
}
|
|
481
|
+
] };
|
|
482
|
+
}
|
|
483
|
+
/** Build the three actions placed at the first waypoint of a shooting section. */
|
|
484
|
+
function shootSectionStartActions(pitchDeg, shootInterval, sectionStart, sectionEnd, groupIdStart) {
|
|
485
|
+
return {
|
|
486
|
+
groups: [{
|
|
487
|
+
actionGroupId: groupIdStart,
|
|
488
|
+
actionGroupStartIndex: sectionStart,
|
|
489
|
+
actionGroupEndIndex: sectionEnd,
|
|
278
490
|
actionGroupMode: "sequence",
|
|
279
491
|
actionTrigger: { actionTriggerType: "betweenAdjacentPoints" },
|
|
492
|
+
action: [
|
|
493
|
+
{
|
|
494
|
+
actionId: 0,
|
|
495
|
+
actionActuatorFunc: "gimbalAngleLock",
|
|
496
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0 }
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
actionId: 1,
|
|
500
|
+
actionActuatorFunc: "gimbalRotate",
|
|
501
|
+
actionActuatorFuncParam: {
|
|
502
|
+
payloadPositionIndex: 0,
|
|
503
|
+
gimbalHeadingYawBase: "aircraft",
|
|
504
|
+
gimbalRotateMode: "absoluteAngle",
|
|
505
|
+
gimbalPitchRotateEnable: true,
|
|
506
|
+
gimbalPitchRotateAngle: degrees(pitchDeg),
|
|
507
|
+
gimbalRollRotateEnable: false,
|
|
508
|
+
gimbalRollRotateAngle: degrees(0),
|
|
509
|
+
gimbalYawRotateEnable: false,
|
|
510
|
+
gimbalYawRotateAngle: degrees(0),
|
|
511
|
+
gimbalRotateTimeEnable: false,
|
|
512
|
+
gimbalRotateTime: 10
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
actionId: 2,
|
|
517
|
+
actionActuatorFunc: "startTimeLapse",
|
|
518
|
+
actionActuatorFuncParam: {
|
|
519
|
+
payloadPositionIndex: 0,
|
|
520
|
+
useGlobalPayloadLensIndex: false,
|
|
521
|
+
payloadLensIndex: "visable",
|
|
522
|
+
minShootInterval: shootInterval
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
}, {
|
|
527
|
+
actionGroupId: groupIdStart + 1,
|
|
528
|
+
actionGroupStartIndex: sectionStart,
|
|
529
|
+
actionGroupEndIndex: sectionEnd,
|
|
530
|
+
actionGroupMode: "sequence",
|
|
531
|
+
actionTrigger: {
|
|
532
|
+
actionTriggerType: "multipleTiming",
|
|
533
|
+
actionTriggerParam: shootInterval
|
|
534
|
+
},
|
|
280
535
|
action: [{
|
|
281
536
|
actionId: 0,
|
|
282
|
-
actionActuatorFunc: "
|
|
537
|
+
actionActuatorFunc: "gimbalRotate",
|
|
283
538
|
actionActuatorFuncParam: {
|
|
284
539
|
payloadPositionIndex: 0,
|
|
285
|
-
|
|
286
|
-
|
|
540
|
+
gimbalHeadingYawBase: "aircraft",
|
|
541
|
+
gimbalRotateMode: "absoluteAngle",
|
|
542
|
+
gimbalPitchRotateEnable: true,
|
|
543
|
+
gimbalPitchRotateAngle: degrees(pitchDeg),
|
|
544
|
+
gimbalRollRotateEnable: false,
|
|
545
|
+
gimbalRollRotateAngle: degrees(0),
|
|
546
|
+
gimbalYawRotateEnable: false,
|
|
547
|
+
gimbalYawRotateAngle: degrees(0),
|
|
548
|
+
gimbalRotateTimeEnable: false,
|
|
549
|
+
gimbalRotateTime: 10
|
|
287
550
|
}
|
|
288
551
|
}]
|
|
289
|
-
}]
|
|
290
|
-
|
|
552
|
+
}],
|
|
553
|
+
nextGroupId: groupIdStart + 2
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/** Build the stop group placed at the last waypoint of a shooting section. */
|
|
557
|
+
function shootSectionStopActions(sectionEnd, groupId) {
|
|
558
|
+
return {
|
|
559
|
+
actionGroupId: groupId,
|
|
560
|
+
actionGroupStartIndex: sectionEnd,
|
|
561
|
+
actionGroupEndIndex: sectionEnd,
|
|
562
|
+
actionGroupMode: "sequence",
|
|
563
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
564
|
+
action: [{
|
|
565
|
+
actionId: 0,
|
|
566
|
+
actionActuatorFunc: "stopTimeLapse",
|
|
567
|
+
actionActuatorFuncParam: {
|
|
568
|
+
payloadPositionIndex: 0,
|
|
569
|
+
payloadLensIndex: "visable"
|
|
570
|
+
}
|
|
571
|
+
}, {
|
|
572
|
+
actionId: 1,
|
|
573
|
+
actionActuatorFunc: "gimbalAngleUnlock"
|
|
574
|
+
}]
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/** Zero-dampingDist stop-turn (first/last waypoint of a section). */
|
|
578
|
+
function stopTurn() {
|
|
579
|
+
return {
|
|
580
|
+
waypointTurnMode: "toPointAndStopWithDiscontinuityCurvature",
|
|
581
|
+
waypointTurnDampingDist: meters(0)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/** Common heading param fields present on every mapping waypoint. */
|
|
585
|
+
function mappingHeadingParam(headingAngle, angleEnable) {
|
|
586
|
+
return {
|
|
587
|
+
waypointHeadingMode: "followWayline",
|
|
588
|
+
waypointHeadingAngle: degrees(headingAngle),
|
|
589
|
+
waypointPoiPoint: {
|
|
590
|
+
lng: 0,
|
|
591
|
+
lat: 0,
|
|
592
|
+
height: meters(0)
|
|
593
|
+
},
|
|
594
|
+
waypointHeadingAngleEnable: angleEnable,
|
|
595
|
+
waypointHeadingPathMode: "followBadArc",
|
|
596
|
+
waypointHeadingPoiIndex: 0
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/** Common extra fields present on every mapping waypoint per reference WPML. */
|
|
600
|
+
const COMMON_WP_EXTRAS = {
|
|
601
|
+
useStraightLine: true,
|
|
602
|
+
waypointGimbalHeadingParam: {
|
|
603
|
+
waypointGimbalPitchAngle: degrees(0),
|
|
604
|
+
waypointGimbalYawAngle: degrees(0)
|
|
605
|
+
},
|
|
606
|
+
isRisky: false,
|
|
607
|
+
waypointWorkType: 0
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* 高程优化 (elevationOptimizeEnable) excursion — appended to the nadir grid:
|
|
611
|
+
* fly from the last nadir waypoint to the survey centroid and take an oblique shot
|
|
612
|
+
* there to enrich the DSM. WITHOUT this flag the route ends at the last grid
|
|
613
|
+
* waypoint — it does NOT detour to the centre (that detour is elevation optimization,
|
|
614
|
+
* not smart-swing). Mutates `placemarks` (appends two waypoints).
|
|
615
|
+
*/
|
|
616
|
+
function appendElevationExcursion(placemarks, folder, origin, enuWps, shootInterval, startGroupId) {
|
|
617
|
+
const lastNadirIdx = enuWps.length - 1;
|
|
618
|
+
const obliqueStartIdx = placemarks.length;
|
|
619
|
+
const obliqueEndIdx = obliqueStartIdx + 1;
|
|
620
|
+
const pitch = Number(folder.Placemark.quickOrthoMappingPitch ?? degrees(-45));
|
|
621
|
+
let nextGroupId = startGroupId;
|
|
622
|
+
const obliqueGimbalGroup = {
|
|
623
|
+
actionGroupId: nextGroupId++,
|
|
624
|
+
actionGroupStartIndex: obliqueStartIdx,
|
|
625
|
+
actionGroupEndIndex: obliqueStartIdx,
|
|
626
|
+
actionGroupMode: "sequence",
|
|
627
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
628
|
+
action: [{
|
|
629
|
+
actionId: 0,
|
|
630
|
+
actionActuatorFunc: "gimbalRotate",
|
|
631
|
+
actionActuatorFuncParam: {
|
|
632
|
+
payloadPositionIndex: 0,
|
|
633
|
+
gimbalHeadingYawBase: "aircraft",
|
|
634
|
+
gimbalRotateMode: "absoluteAngle",
|
|
635
|
+
gimbalPitchRotateEnable: true,
|
|
636
|
+
gimbalPitchRotateAngle: degrees(pitch),
|
|
637
|
+
gimbalRollRotateEnable: false,
|
|
638
|
+
gimbalRollRotateAngle: degrees(0),
|
|
639
|
+
gimbalYawRotateEnable: true,
|
|
640
|
+
gimbalYawRotateAngle: degrees(0),
|
|
641
|
+
gimbalRotateTimeEnable: false,
|
|
642
|
+
gimbalRotateTime: 10
|
|
643
|
+
}
|
|
644
|
+
}, {
|
|
645
|
+
actionId: 1,
|
|
646
|
+
actionActuatorFunc: "hover",
|
|
647
|
+
actionActuatorFuncParam: { hoverTime: .5 }
|
|
648
|
+
}]
|
|
649
|
+
};
|
|
650
|
+
const obliqueStart = shootSectionStartActions(pitch, shootInterval, obliqueStartIdx, obliqueEndIdx, nextGroupId);
|
|
651
|
+
nextGroupId = obliqueStart.nextGroupId;
|
|
652
|
+
const obliqueStop = shootSectionStopActions(obliqueEndIdx, nextGroupId);
|
|
653
|
+
const lastNadirCoord = enuToLngLat(enuWps[lastNadirIdx], origin);
|
|
654
|
+
const sumEnu = enuWps.reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1]], [0, 0]);
|
|
655
|
+
const centroidEnu = [sumEnu[0] / enuWps.length, sumEnu[1] / enuWps.length];
|
|
656
|
+
const centroidCoord = enuToLngLat(centroidEnu, origin);
|
|
657
|
+
const obliqueHeading = bearingDeg(enuWps[lastNadirIdx], centroidEnu);
|
|
658
|
+
placemarks.push({
|
|
659
|
+
Point: { coordinates: lastNadirCoord },
|
|
660
|
+
index: obliqueStartIdx,
|
|
661
|
+
executeHeight: folder.Placemark.height,
|
|
662
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
663
|
+
waypointHeadingParam: mappingHeadingParam(obliqueHeading, true),
|
|
664
|
+
waypointTurnParam: stopTurn(),
|
|
665
|
+
...COMMON_WP_EXTRAS,
|
|
666
|
+
actionGroup: [obliqueGimbalGroup, ...obliqueStart.groups]
|
|
667
|
+
});
|
|
668
|
+
placemarks.push({
|
|
669
|
+
Point: { coordinates: centroidCoord },
|
|
670
|
+
index: obliqueEndIdx,
|
|
671
|
+
executeHeight: folder.Placemark.height,
|
|
672
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
673
|
+
waypointHeadingParam: mappingHeadingParam(0, false),
|
|
674
|
+
waypointTurnParam: stopTurn(),
|
|
675
|
+
...COMMON_WP_EXTRAS,
|
|
676
|
+
actionGroup: [obliqueStop]
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Ortho (nadir) wayline — covers 正射 and 正射+智能摆拍 (the swing is a folder-level
|
|
681
|
+
* quickOrthoMappingEnable flag over the same grid geometry, not extra waypoints).
|
|
682
|
+
*
|
|
683
|
+
* Action pattern (per reference WPML M4TD 正射采集):
|
|
684
|
+
* - WP 0: two action groups (betweenAdjacentPoints startTimeLapse-init + multipleTiming gimbalCorrect)
|
|
685
|
+
* - WP 1…N-2: no action groups
|
|
686
|
+
* - WP N-1 (last): reachPoint stopTimeLapse group
|
|
687
|
+
*
|
|
688
|
+
* When `elevationOptimizeEnable` is set, a centroid excursion is appended (see
|
|
689
|
+
* `appendElevationExcursion`); otherwise the route ends at the last grid waypoint.
|
|
690
|
+
*/
|
|
691
|
+
function emitOrtho(folder, origin, enuWps, shootInterval) {
|
|
692
|
+
const lastIndex = enuWps.length - 1;
|
|
693
|
+
const startActions = shootSectionStartActions(-90, shootInterval, 0, lastIndex, 0);
|
|
694
|
+
const stopGroup = shootSectionStopActions(lastIndex, startActions.nextGroupId);
|
|
695
|
+
const placemarks = enuWps.map((enu, index) => {
|
|
696
|
+
const coord = enuToLngLat(enu, origin);
|
|
697
|
+
const isLast = index === lastIndex;
|
|
698
|
+
const headingAngle = isLast ? 0 : bearingDeg(enuWps[index], enuWps[index + 1]);
|
|
699
|
+
const placemark = {
|
|
291
700
|
Point: { coordinates: coord },
|
|
292
701
|
index,
|
|
293
|
-
executeHeight: folder.height,
|
|
702
|
+
executeHeight: folder.Placemark.height,
|
|
294
703
|
waypointSpeed: folder.autoFlightSpeed,
|
|
295
|
-
waypointHeadingParam:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
704
|
+
waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
|
|
705
|
+
waypointTurnParam: index === 0 || isLast ? stopTurn() : {
|
|
706
|
+
waypointTurnMode: "coordinateTurn",
|
|
707
|
+
waypointTurnDampingDist: turnDamping(enuWps, index)
|
|
299
708
|
},
|
|
300
|
-
|
|
301
|
-
...actionGroup.length > 0 ? { actionGroup } : {}
|
|
709
|
+
...COMMON_WP_EXTRAS
|
|
302
710
|
};
|
|
711
|
+
if (index === 0) placemark.actionGroup = startActions.groups;
|
|
712
|
+
else if (isLast) placemark.actionGroup = [stopGroup];
|
|
713
|
+
return placemark;
|
|
303
714
|
});
|
|
304
|
-
|
|
715
|
+
if (folder.Placemark.elevationOptimizeEnable) appendElevationExcursion(placemarks, folder, origin, enuWps, shootInterval, startActions.nextGroupId + 1);
|
|
716
|
+
return {
|
|
305
717
|
templateId: folder.templateId,
|
|
306
718
|
waylineId: 0,
|
|
307
719
|
executeHeightMode: heightModeToExecuteMode$3(folder),
|
|
308
720
|
autoFlightSpeed: folder.autoFlightSpeed,
|
|
309
|
-
startActionGroup:
|
|
310
|
-
|
|
721
|
+
startActionGroup: buildStartActionGroup(-90),
|
|
722
|
+
Placemark: placemarks
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Single-pass smart-oblique / quick-ortho (smartObliqueEnable=true):
|
|
727
|
+
* The aircraft flies one grid while the gimbal swings (startSmartOblique/stopSmartOblique).
|
|
728
|
+
*/
|
|
729
|
+
function emitSmartOblique(folder, origin, enuWps) {
|
|
730
|
+
const lastIndex = enuWps.length - 1;
|
|
731
|
+
const pitch = folder.Placemark.smartObliqueGimbalPitch ?? degrees(-45);
|
|
732
|
+
const placemarks = enuWps.map((enu, index) => {
|
|
733
|
+
const coord = enuToLngLat(enu, origin);
|
|
734
|
+
const isLast = index === lastIndex;
|
|
735
|
+
const headingAngle = isLast ? 0 : bearingDeg(enuWps[index], enuWps[index + 1]);
|
|
736
|
+
const placemark = {
|
|
737
|
+
Point: { coordinates: coord },
|
|
738
|
+
index,
|
|
739
|
+
executeHeight: folder.Placemark.height,
|
|
740
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
741
|
+
waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
|
|
742
|
+
waypointTurnParam: index === 0 || isLast ? stopTurn() : {
|
|
743
|
+
waypointTurnMode: "coordinateTurn",
|
|
744
|
+
waypointTurnDampingDist: turnDamping(enuWps, index)
|
|
745
|
+
},
|
|
746
|
+
...COMMON_WP_EXTRAS
|
|
747
|
+
};
|
|
748
|
+
if (index === 0) placemark.actionGroup = [{
|
|
749
|
+
actionGroupId: 0,
|
|
311
750
|
actionGroupStartIndex: 0,
|
|
312
|
-
actionGroupEndIndex:
|
|
751
|
+
actionGroupEndIndex: lastIndex,
|
|
313
752
|
actionGroupMode: "sequence",
|
|
314
753
|
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
315
754
|
action: [{
|
|
316
755
|
actionId: 0,
|
|
317
|
-
actionActuatorFunc: "
|
|
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
|
-
}
|
|
756
|
+
actionActuatorFunc: "startSmartOblique",
|
|
757
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0 }
|
|
331
758
|
}]
|
|
332
|
-
}
|
|
759
|
+
}];
|
|
760
|
+
else if (isLast) placemark.actionGroup = [{
|
|
761
|
+
actionGroupId: 1,
|
|
762
|
+
actionGroupStartIndex: lastIndex,
|
|
763
|
+
actionGroupEndIndex: lastIndex,
|
|
764
|
+
actionGroupMode: "sequence",
|
|
765
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
766
|
+
action: [{
|
|
767
|
+
actionId: 0,
|
|
768
|
+
actionActuatorFunc: "stopSmartOblique",
|
|
769
|
+
actionActuatorFuncParam: { payloadPositionIndex: 0 }
|
|
770
|
+
}]
|
|
771
|
+
}];
|
|
772
|
+
return placemark;
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
templateId: folder.templateId,
|
|
776
|
+
waylineId: 0,
|
|
777
|
+
executeHeightMode: heightModeToExecuteMode$3(folder),
|
|
778
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
779
|
+
startActionGroup: buildStartActionGroup(Number(pitch)),
|
|
333
780
|
Placemark: placemarks
|
|
334
781
|
};
|
|
782
|
+
}
|
|
783
|
+
function planMapping2d(template, options) {
|
|
784
|
+
if (options?.lineSpacing == null) throw new PlannerError("mapping2d requires options.lineSpacing — calculate 2·h·tan(HFOV/2)·(1−sideOverlap) in the demo and pass it in.");
|
|
785
|
+
const standoff = template.Folder.Placemark.smartObliqueEnable === true ? smartObliqueStandoff(template.Folder) : 0;
|
|
786
|
+
const { folder, origin, enuWps } = buildGrid(template, Number(options.lineSpacing), standoff);
|
|
787
|
+
const shootInterval = options.shootInterval ?? 2;
|
|
788
|
+
if (folder.Placemark.smartObliqueEnable === true) return {
|
|
789
|
+
missionConfig: template.missionConfig,
|
|
790
|
+
Folder: [emitSmartOblique(folder, origin, enuWps)]
|
|
791
|
+
};
|
|
335
792
|
return {
|
|
336
793
|
missionConfig: template.missionConfig,
|
|
337
|
-
Folder: [
|
|
794
|
+
Folder: [emitOrtho(folder, origin, enuWps, shootInterval)]
|
|
338
795
|
};
|
|
339
796
|
}
|
|
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
797
|
//#endregion
|
|
349
798
|
//#region src/plan/plan-mapping3d.ts
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
799
|
+
const PASSES = [
|
|
800
|
+
{
|
|
801
|
+
axisOffsetDeg: 0,
|
|
802
|
+
headingOffsetDeg: 0,
|
|
803
|
+
pitch: "nadir",
|
|
804
|
+
headingMode: "followWayline",
|
|
805
|
+
halfPhase: false,
|
|
806
|
+
lookSign: 0,
|
|
807
|
+
interval: 2
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
axisOffsetDeg: 0,
|
|
811
|
+
headingOffsetDeg: 0,
|
|
812
|
+
pitch: "inclined",
|
|
813
|
+
headingMode: "fixed",
|
|
814
|
+
halfPhase: false,
|
|
815
|
+
lookSign: 1,
|
|
816
|
+
interval: 2.5
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
axisOffsetDeg: 90,
|
|
820
|
+
headingOffsetDeg: 90,
|
|
821
|
+
pitch: "inclined",
|
|
822
|
+
headingMode: "fixed",
|
|
823
|
+
halfPhase: false,
|
|
824
|
+
lookSign: 1,
|
|
825
|
+
interval: 2.5
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
axisOffsetDeg: 0,
|
|
829
|
+
headingOffsetDeg: 180,
|
|
830
|
+
pitch: "inclined",
|
|
831
|
+
headingMode: "fixed",
|
|
832
|
+
halfPhase: true,
|
|
833
|
+
lookSign: -1,
|
|
834
|
+
interval: 2.5
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
axisOffsetDeg: 90,
|
|
838
|
+
headingOffsetDeg: -90,
|
|
839
|
+
pitch: "inclined",
|
|
840
|
+
headingMode: "fixed",
|
|
841
|
+
halfPhase: true,
|
|
842
|
+
lookSign: -1,
|
|
843
|
+
interval: 2.5
|
|
844
|
+
}
|
|
845
|
+
];
|
|
846
|
+
/** Normalize an angle to (−180, 180]. */
|
|
847
|
+
function wrap180(deg) {
|
|
848
|
+
return ((deg + 180) % 360 + 360) % 360 - 180;
|
|
849
|
+
}
|
|
850
|
+
function planMapping3d(template, options) {
|
|
851
|
+
if (options?.lineSpacing == null) throw new PlannerError("mapping3d requires options.lineSpacing — calculate 2·h·tan(HFOV/2)·(1−sideOverlap) in the demo and pass it in.");
|
|
356
852
|
const folder = template.Folder;
|
|
357
853
|
const ring = parseCoordinatesString(folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
358
854
|
const origin = centroid(ring);
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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));
|
|
855
|
+
const ringEnu = ring.map((p) => lngLatToEnu(p, origin));
|
|
856
|
+
const baseDirection = Number(folder.Placemark.direction);
|
|
857
|
+
const lineSpacing = Number(options.lineSpacing);
|
|
858
|
+
const inclined = folder.Placemark.inclinedGimbalPitch;
|
|
859
|
+
const pitchAbs = Math.abs(Number(inclined));
|
|
860
|
+
const sinP = pitchAbs > 0 && pitchAbs < 90 ? Math.sin(pitchAbs * Math.PI / 180) : 1;
|
|
861
|
+
const tanP = pitchAbs > 0 && pitchAbs < 90 ? Math.tan(pitchAbs * Math.PI / 180) : Infinity;
|
|
862
|
+
const standoff = Math.min(Number(folder.Placemark.height) / tanP, Number(folder.Placemark.height) * 3);
|
|
863
|
+
const obliqueSpacing = lineSpacing / sinP;
|
|
389
864
|
return {
|
|
390
865
|
missionConfig: template.missionConfig,
|
|
391
|
-
Folder
|
|
866
|
+
Folder: PASSES.map((pass, i) => {
|
|
867
|
+
const passSpacing = pass.pitch === "inclined" ? obliqueSpacing : lineSpacing;
|
|
868
|
+
return buildPass(folder, i, origin, orientPathToHome(surveyGrid(ringEnu, {
|
|
869
|
+
directionDeg: baseDirection + pass.axisOffsetDeg,
|
|
870
|
+
marginM: folder.Placemark.margin,
|
|
871
|
+
lineSpacing: passSpacing,
|
|
872
|
+
phaseOffsetM: pass.halfPhase ? passSpacing / 2 : 0,
|
|
873
|
+
lengthwiseShiftM: -pass.lookSign * standoff
|
|
874
|
+
}), origin, template.missionConfig.takeOffRefPoint), {
|
|
875
|
+
headingMode: pass.headingMode,
|
|
876
|
+
headingAngle: degrees(wrap180(baseDirection + pass.headingOffsetDeg)),
|
|
877
|
+
gimbalPitch: pass.pitch === "nadir" ? degrees(-90) : inclined,
|
|
878
|
+
interval: pass.interval
|
|
879
|
+
});
|
|
880
|
+
})
|
|
392
881
|
};
|
|
393
882
|
}
|
|
394
|
-
function
|
|
883
|
+
function buildPass(folder, waylineId, origin, enuWps, cfg) {
|
|
395
884
|
const lastIndex = enuWps.length - 1;
|
|
396
885
|
const placemarks = enuWps.map((enu, index) => {
|
|
397
886
|
const placemark = {
|
|
398
887
|
Point: { coordinates: enuToLngLat(enu, origin) },
|
|
399
888
|
index,
|
|
400
|
-
executeHeight: folder.height,
|
|
401
|
-
waypointSpeed: folder.
|
|
889
|
+
executeHeight: folder.Placemark.height,
|
|
890
|
+
waypointSpeed: folder.Placemark.inclinedFlightSpeed,
|
|
402
891
|
waypointHeadingParam: {
|
|
403
|
-
waypointHeadingMode:
|
|
404
|
-
waypointHeadingAngle:
|
|
892
|
+
waypointHeadingMode: cfg.headingMode,
|
|
893
|
+
waypointHeadingAngle: cfg.headingAngle,
|
|
405
894
|
waypointHeadingPathMode: "followBadArc"
|
|
406
895
|
},
|
|
407
896
|
waypointTurnParam: { waypointTurnMode: "toPointAndPassWithContinuityCurvature" }
|
|
408
897
|
};
|
|
409
|
-
|
|
410
|
-
|
|
898
|
+
if (index === 0) placemark.actionGroup = [{
|
|
899
|
+
actionGroupId: 0,
|
|
900
|
+
actionGroupStartIndex: 0,
|
|
901
|
+
actionGroupEndIndex: lastIndex,
|
|
902
|
+
actionGroupMode: "sequence",
|
|
903
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
904
|
+
action: [gimbalRotate(0, cfg.gimbalPitch)]
|
|
905
|
+
}, {
|
|
906
|
+
actionGroupId: 1,
|
|
907
|
+
actionGroupStartIndex: 0,
|
|
908
|
+
actionGroupEndIndex: lastIndex,
|
|
909
|
+
actionGroupMode: "sequence",
|
|
910
|
+
actionTrigger: {
|
|
911
|
+
actionTriggerType: "multipleTiming",
|
|
912
|
+
actionTriggerParam: cfg.interval
|
|
913
|
+
},
|
|
914
|
+
action: [takePhoto(0)]
|
|
915
|
+
}];
|
|
411
916
|
return placemark;
|
|
412
917
|
});
|
|
413
918
|
return {
|
|
414
919
|
templateId: folder.templateId,
|
|
415
920
|
waylineId,
|
|
416
921
|
executeHeightMode: heightModeToExecuteMode$2(folder.waylineCoordinateSysParam.heightMode),
|
|
417
|
-
autoFlightSpeed: folder.
|
|
418
|
-
startActionGroup:
|
|
922
|
+
autoFlightSpeed: folder.Placemark.inclinedFlightSpeed,
|
|
923
|
+
startActionGroup: { action: [gimbalRotate(0, cfg.gimbalPitch)] },
|
|
419
924
|
Placemark: placemarks
|
|
420
925
|
};
|
|
421
926
|
}
|
|
422
|
-
|
|
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
|
-
}
|
|
927
|
+
/** Oblique gimbal: pitched down relative to the aircraft nose (yaw base = aircraft). */
|
|
453
928
|
function gimbalRotate(actionId, pitch) {
|
|
454
929
|
return {
|
|
455
930
|
actionId,
|
|
456
931
|
actionActuatorFunc: "gimbalRotate",
|
|
457
932
|
actionActuatorFuncParam: {
|
|
458
933
|
payloadPositionIndex: 0,
|
|
459
|
-
gimbalHeadingYawBase: "
|
|
934
|
+
gimbalHeadingYawBase: "aircraft",
|
|
460
935
|
gimbalRotateMode: "absoluteAngle",
|
|
461
936
|
gimbalPitchRotateEnable: true,
|
|
462
|
-
gimbalPitchRotateAngle:
|
|
937
|
+
gimbalPitchRotateAngle: pitch,
|
|
463
938
|
gimbalRollRotateEnable: false,
|
|
464
939
|
gimbalRollRotateAngle: degrees(0),
|
|
465
940
|
gimbalYawRotateEnable: false,
|
|
@@ -510,7 +985,7 @@ function planMappingStrip(template) {
|
|
|
510
985
|
}, origin));
|
|
511
986
|
const placemarks = coords.map((c, index) => {
|
|
512
987
|
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;
|
|
988
|
+
const executeHeight = folder.Placemark.stripUseTemplateAltitude && c.alt !== void 0 ? c.alt : folder.Placemark.height;
|
|
514
989
|
return {
|
|
515
990
|
Point: { coordinates: {
|
|
516
991
|
lng: c.lng,
|
|
@@ -616,11 +1091,13 @@ const isMappingStrip = (t) => t.Folder.templateType === "mappingStrip";
|
|
|
616
1091
|
/**
|
|
617
1092
|
* Compile a high-level `Template` into executable `Waylines`.
|
|
618
1093
|
* Dispatch is by `template.Folder.templateType` per the DJI WPML spec.
|
|
1094
|
+
*
|
|
1095
|
+
* Mapping templates require `options.lineSpacing` (meters between flight lines).
|
|
619
1096
|
*/
|
|
620
|
-
function plan(template) {
|
|
1097
|
+
function plan(template, options) {
|
|
621
1098
|
if (isWaypoint(template)) return planWaypoint(template);
|
|
622
|
-
if (isMapping2d(template)) return planMapping2d(template);
|
|
623
|
-
if (isMapping3d(template)) return planMapping3d(template);
|
|
1099
|
+
if (isMapping2d(template)) return planMapping2d(template, options);
|
|
1100
|
+
if (isMapping3d(template)) return planMapping3d(template, options);
|
|
624
1101
|
if (isMappingStrip(template)) return planMappingStrip(template);
|
|
625
1102
|
throw new PlannerError(`Unknown templateType: ${template.Folder.templateType}`);
|
|
626
1103
|
}
|
|
@@ -776,4 +1253,4 @@ function validate(doc) {
|
|
|
776
1253
|
return issues;
|
|
777
1254
|
}
|
|
778
1255
|
//#endregion
|
|
779
|
-
export { DroneEnum, KmzError, PayloadEnum, PlannerError, degrees, meters, metersPerSecond, plan, validate };
|
|
1256
|
+
export { DroneEnum, KmzError, PayloadEnum, PlannerError, degrees, haversineMeters, meters, metersPerSecond, plan, validate };
|