@lumikmz/kmz 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -903,6 +903,12 @@ interface WaylinesFolder {
903
903
  interface PlanOptions {
904
904
  lineSpacing?: Meters;
905
905
  shootInterval?: number;
906
+ resolveExecuteHeight?: (point: {
907
+ lng: number;
908
+ lat: number;
909
+ index: number;
910
+ waylineId: number;
911
+ }) => Meters;
906
912
  }
907
913
  /**
908
914
  * Compile a high-level `Template` into executable `Waylines`.
package/dist/index.mjs CHANGED
@@ -406,6 +406,14 @@ function heightModeToExecuteMode$3(folder) {
406
406
  case "realTimeFollowSurface": return "realTimeFollowSurface";
407
407
  }
408
408
  }
409
+ function executeHeightFor$1(folder, coord, index, waylineId, options) {
410
+ if (folder.waylineCoordinateSysParam.heightMode !== "aboveGroundLevel") return folder.Placemark.height;
411
+ return options?.resolveExecuteHeight?.({
412
+ ...coord,
413
+ index,
414
+ waylineId
415
+ }) ?? folder.Placemark.height;
416
+ }
409
417
  /** Desired damping distance; clamped down when segments are short. */
410
418
  const DESIRED_TURN_DAMPING_M = 10;
411
419
  /** Never consume more than this fraction of either adjacent segment. */
@@ -613,7 +621,7 @@ const COMMON_WP_EXTRAS = {
613
621
  * waypoint — it does NOT detour to the centre (that detour is elevation optimization,
614
622
  * not smart-swing). Mutates `placemarks` (appends two waypoints).
615
623
  */
616
- function appendElevationExcursion(placemarks, folder, origin, enuWps, shootInterval, startGroupId) {
624
+ function appendElevationExcursion(placemarks, folder, origin, enuWps, shootInterval, startGroupId, options) {
617
625
  const lastNadirIdx = enuWps.length - 1;
618
626
  const obliqueStartIdx = placemarks.length;
619
627
  const obliqueEndIdx = obliqueStartIdx + 1;
@@ -658,7 +666,7 @@ function appendElevationExcursion(placemarks, folder, origin, enuWps, shootInter
658
666
  placemarks.push({
659
667
  Point: { coordinates: lastNadirCoord },
660
668
  index: obliqueStartIdx,
661
- executeHeight: folder.Placemark.height,
669
+ executeHeight: executeHeightFor$1(folder, lastNadirCoord, obliqueStartIdx, 0, options),
662
670
  waypointSpeed: folder.autoFlightSpeed,
663
671
  waypointHeadingParam: mappingHeadingParam(obliqueHeading, true),
664
672
  waypointTurnParam: stopTurn(),
@@ -668,7 +676,7 @@ function appendElevationExcursion(placemarks, folder, origin, enuWps, shootInter
668
676
  placemarks.push({
669
677
  Point: { coordinates: centroidCoord },
670
678
  index: obliqueEndIdx,
671
- executeHeight: folder.Placemark.height,
679
+ executeHeight: executeHeightFor$1(folder, centroidCoord, obliqueEndIdx, 0, options),
672
680
  waypointSpeed: folder.autoFlightSpeed,
673
681
  waypointHeadingParam: mappingHeadingParam(0, false),
674
682
  waypointTurnParam: stopTurn(),
@@ -688,7 +696,7 @@ function appendElevationExcursion(placemarks, folder, origin, enuWps, shootInter
688
696
  * When `elevationOptimizeEnable` is set, a centroid excursion is appended (see
689
697
  * `appendElevationExcursion`); otherwise the route ends at the last grid waypoint.
690
698
  */
691
- function emitOrtho(folder, origin, enuWps, shootInterval) {
699
+ function emitOrtho(folder, origin, enuWps, shootInterval, options) {
692
700
  const lastIndex = enuWps.length - 1;
693
701
  const startActions = shootSectionStartActions(-90, shootInterval, 0, lastIndex, 0);
694
702
  const stopGroup = shootSectionStopActions(lastIndex, startActions.nextGroupId);
@@ -699,7 +707,7 @@ function emitOrtho(folder, origin, enuWps, shootInterval) {
699
707
  const placemark = {
700
708
  Point: { coordinates: coord },
701
709
  index,
702
- executeHeight: folder.Placemark.height,
710
+ executeHeight: executeHeightFor$1(folder, coord, index, 0, options),
703
711
  waypointSpeed: folder.autoFlightSpeed,
704
712
  waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
705
713
  waypointTurnParam: index === 0 || isLast ? stopTurn() : {
@@ -712,7 +720,7 @@ function emitOrtho(folder, origin, enuWps, shootInterval) {
712
720
  else if (isLast) placemark.actionGroup = [stopGroup];
713
721
  return placemark;
714
722
  });
715
- if (folder.Placemark.elevationOptimizeEnable) appendElevationExcursion(placemarks, folder, origin, enuWps, shootInterval, startActions.nextGroupId + 1);
723
+ if (folder.Placemark.elevationOptimizeEnable) appendElevationExcursion(placemarks, folder, origin, enuWps, shootInterval, startActions.nextGroupId + 1, options);
716
724
  return {
717
725
  templateId: folder.templateId,
718
726
  waylineId: 0,
@@ -726,7 +734,7 @@ function emitOrtho(folder, origin, enuWps, shootInterval) {
726
734
  * Single-pass smart-oblique / quick-ortho (smartObliqueEnable=true):
727
735
  * The aircraft flies one grid while the gimbal swings (startSmartOblique/stopSmartOblique).
728
736
  */
729
- function emitSmartOblique(folder, origin, enuWps) {
737
+ function emitSmartOblique(folder, origin, enuWps, options) {
730
738
  const lastIndex = enuWps.length - 1;
731
739
  const pitch = folder.Placemark.smartObliqueGimbalPitch ?? degrees(-45);
732
740
  const placemarks = enuWps.map((enu, index) => {
@@ -736,7 +744,7 @@ function emitSmartOblique(folder, origin, enuWps) {
736
744
  const placemark = {
737
745
  Point: { coordinates: coord },
738
746
  index,
739
- executeHeight: folder.Placemark.height,
747
+ executeHeight: executeHeightFor$1(folder, coord, index, 0, options),
740
748
  waypointSpeed: folder.autoFlightSpeed,
741
749
  waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
742
750
  waypointTurnParam: index === 0 || isLast ? stopTurn() : {
@@ -787,11 +795,11 @@ function planMapping2d(template, options) {
787
795
  const shootInterval = options.shootInterval ?? 2;
788
796
  if (folder.Placemark.smartObliqueEnable === true) return {
789
797
  missionConfig: template.missionConfig,
790
- Folder: [emitSmartOblique(folder, origin, enuWps)]
798
+ Folder: [emitSmartOblique(folder, origin, enuWps, options)]
791
799
  };
792
800
  return {
793
801
  missionConfig: template.missionConfig,
794
- Folder: [emitOrtho(folder, origin, enuWps, shootInterval)]
802
+ Folder: [emitOrtho(folder, origin, enuWps, shootInterval, options)]
795
803
  };
796
804
  }
797
805
  //#endregion
@@ -875,18 +883,28 @@ function planMapping3d(template, options) {
875
883
  headingMode: pass.headingMode,
876
884
  headingAngle: degrees(wrap180(baseDirection + pass.headingOffsetDeg)),
877
885
  gimbalPitch: pass.pitch === "nadir" ? degrees(-90) : inclined,
878
- interval: pass.interval
886
+ interval: pass.interval,
887
+ options
879
888
  });
880
889
  })
881
890
  };
882
891
  }
892
+ function executeHeightFor(folder, coord, index, waylineId, options) {
893
+ if (folder.waylineCoordinateSysParam.heightMode !== "aboveGroundLevel") return folder.Placemark.height;
894
+ return options?.resolveExecuteHeight?.({
895
+ ...coord,
896
+ index,
897
+ waylineId
898
+ }) ?? folder.Placemark.height;
899
+ }
883
900
  function buildPass(folder, waylineId, origin, enuWps, cfg) {
884
901
  const lastIndex = enuWps.length - 1;
885
902
  const placemarks = enuWps.map((enu, index) => {
903
+ const coord = enuToLngLat(enu, origin);
886
904
  const placemark = {
887
- Point: { coordinates: enuToLngLat(enu, origin) },
905
+ Point: { coordinates: coord },
888
906
  index,
889
- executeHeight: folder.Placemark.height,
907
+ executeHeight: executeHeightFor(folder, coord, index, waylineId, cfg.options),
890
908
  waypointSpeed: folder.Placemark.inclinedFlightSpeed,
891
909
  waypointHeadingParam: {
892
910
  waypointHeadingMode: cfg.headingMode,
@@ -1043,7 +1061,7 @@ function planWaypoint(template) {
1043
1061
  const placemarks = folder.Placemark.map((p, i) => {
1044
1062
  const heading = p.useGlobalHeadingParam || p.waypointHeadingParam === void 0 ? folder.globalWaypointHeadingParam : p.waypointHeadingParam;
1045
1063
  const turn = p.useGlobalTurnParam || p.waypointTurnParam === void 0 ? { waypointTurnMode: folder.globalWaypointTurnMode } : p.waypointTurnParam;
1046
- const executeHeight = p.useGlobalHeight || p.height === void 0 ? folder.globalHeight : p.height;
1064
+ const executeHeight = folder.waylineCoordinateSysParam.heightMode === "aboveGroundLevel" && p.ellipsoidHeight !== void 0 ? p.ellipsoidHeight : p.useGlobalHeight || p.height === void 0 ? folder.globalHeight : p.height;
1047
1065
  const waypointSpeed = p.useGlobalSpeed || p.waypointSpeed === void 0 ? folder.autoFlightSpeed : p.waypointSpeed;
1048
1066
  return {
1049
1067
  Point: p.Point,
@@ -1072,13 +1090,13 @@ function planWaypoint(template) {
1072
1090
  /**
1073
1091
  * Map a template heightMode to a waylines executeHeightMode.
1074
1092
  * - `EGM96` → `WGS84` (waylines spec doesn't expose EGM96; runtime resolves it).
1075
- * - `aboveGroundLevel` → `realTimeFollowSurface` (closest executable analog).
1093
+ * - `aboveGroundLevel` → `WGS84` (waypoint waylines use per-point ellipsoid heights).
1076
1094
  */
1077
1095
  function heightModeToExecuteMode(mode) {
1078
1096
  switch (mode) {
1079
- case "EGM96": return "WGS84";
1097
+ case "EGM96":
1098
+ case "aboveGroundLevel": return "WGS84";
1080
1099
  case "relativeToStartPoint": return "relativeToStartPoint";
1081
- case "aboveGroundLevel":
1082
1100
  case "realTimeFollowSurface": return "realTimeFollowSurface";
1083
1101
  }
1084
1102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumikmz/kmz",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "DJI KMZ/WPML domain model + plan() algorithm + validate(), spec-aligned types mirroring docs/kmz/",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/plan/index.ts CHANGED
@@ -28,6 +28,12 @@ import { planWaypoint } from "./plan-waypoint.js";
28
28
  export interface PlanOptions {
29
29
  lineSpacing?: Meters;
30
30
  shootInterval?: number;
31
+ resolveExecuteHeight?: (point: {
32
+ lng: number;
33
+ lat: number;
34
+ index: number;
35
+ waylineId: number;
36
+ }) => Meters;
31
37
  }
32
38
 
33
39
  const isWaypoint = (t: Template): t is WaypointTemplate => t.Folder.templateType === "waypoint";
@@ -68,6 +68,19 @@ function heightModeToExecuteMode(folder: Mapping2dFolder): ExecuteHeightMode {
68
68
  }
69
69
  }
70
70
 
71
+ function executeHeightFor(
72
+ folder: Mapping2dFolder,
73
+ coord: { lng: number; lat: number },
74
+ index: number,
75
+ waylineId: number,
76
+ options?: PlanOptions,
77
+ ): Meters {
78
+ if (folder.waylineCoordinateSysParam.heightMode !== "aboveGroundLevel") {
79
+ return folder.Placemark.height;
80
+ }
81
+ return options?.resolveExecuteHeight?.({ ...coord, index, waylineId }) ?? folder.Placemark.height;
82
+ }
83
+
71
84
  /** Desired damping distance; clamped down when segments are short. */
72
85
  const DESIRED_TURN_DAMPING_M = 10;
73
86
 
@@ -295,6 +308,7 @@ function appendElevationExcursion(
295
308
  enuWps: readonly EnuPoint[],
296
309
  shootInterval: number,
297
310
  startGroupId: number,
311
+ options?: PlanOptions,
298
312
  ): void {
299
313
  const lastNadirIdx = enuWps.length - 1;
300
314
  const obliqueStartIdx = placemarks.length;
@@ -352,7 +366,7 @@ function appendElevationExcursion(
352
366
  placemarks.push({
353
367
  Point: { coordinates: lastNadirCoord }, // same position as last nadir
354
368
  index: obliqueStartIdx,
355
- executeHeight: folder.Placemark.height,
369
+ executeHeight: executeHeightFor(folder, lastNadirCoord, obliqueStartIdx, 0, options),
356
370
  waypointSpeed: folder.autoFlightSpeed,
357
371
  waypointHeadingParam: mappingHeadingParam(obliqueHeading, true),
358
372
  waypointTurnParam: stopTurn(),
@@ -363,7 +377,7 @@ function appendElevationExcursion(
363
377
  placemarks.push({
364
378
  Point: { coordinates: centroidCoord },
365
379
  index: obliqueEndIdx,
366
- executeHeight: folder.Placemark.height,
380
+ executeHeight: executeHeightFor(folder, centroidCoord, obliqueEndIdx, 0, options),
367
381
  waypointSpeed: folder.autoFlightSpeed,
368
382
  waypointHeadingParam: mappingHeadingParam(0, false),
369
383
  waypointTurnParam: stopTurn(),
@@ -389,6 +403,7 @@ function emitOrtho(
389
403
  origin: { lng: number; lat: number },
390
404
  enuWps: readonly EnuPoint[],
391
405
  shootInterval: number,
406
+ options?: PlanOptions,
392
407
  ): WaylinesFolder {
393
408
  const lastIndex = enuWps.length - 1;
394
409
  const startActions = shootSectionStartActions(-90, shootInterval, 0, lastIndex, 0);
@@ -401,7 +416,7 @@ function emitOrtho(
401
416
  const placemark: WaylinesPlacemark = {
402
417
  Point: { coordinates: coord },
403
418
  index,
404
- executeHeight: folder.Placemark.height,
419
+ executeHeight: executeHeightFor(folder, coord, index, 0, options),
405
420
  waypointSpeed: folder.autoFlightSpeed,
406
421
  waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
407
422
  waypointTurnParam:
@@ -429,6 +444,7 @@ function emitOrtho(
429
444
  enuWps,
430
445
  shootInterval,
431
446
  startActions.nextGroupId + 1,
447
+ options,
432
448
  );
433
449
  }
434
450
 
@@ -450,6 +466,7 @@ function emitSmartOblique(
450
466
  folder: Mapping2dFolder,
451
467
  origin: { lng: number; lat: number },
452
468
  enuWps: readonly EnuPoint[],
469
+ options?: PlanOptions,
453
470
  ): WaylinesFolder {
454
471
  const lastIndex = enuWps.length - 1;
455
472
  const pitch = folder.Placemark.smartObliqueGimbalPitch ?? degrees(-45);
@@ -461,7 +478,7 @@ function emitSmartOblique(
461
478
  const placemark: WaylinesPlacemark = {
462
479
  Point: { coordinates: coord },
463
480
  index,
464
- executeHeight: folder.Placemark.height,
481
+ executeHeight: executeHeightFor(folder, coord, index, 0, options),
465
482
  waypointSpeed: folder.autoFlightSpeed,
466
483
  waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
467
484
  waypointTurnParam:
@@ -539,13 +556,13 @@ export function planMapping2d(template: Mapping2dTemplate, options?: PlanOptions
539
556
  if (folder.Placemark.smartObliqueEnable === true) {
540
557
  return {
541
558
  missionConfig: template.missionConfig,
542
- Folder: [emitSmartOblique(folder, origin, enuWps)],
559
+ Folder: [emitSmartOblique(folder, origin, enuWps, options)],
543
560
  };
544
561
  }
545
562
  // 正射 and 正射+智能摆拍 share the nadir grid; the centroid detour is driven by
546
563
  // elevationOptimizeEnable inside emitOrtho, not by quickOrthoMappingEnable.
547
564
  return {
548
565
  missionConfig: template.missionConfig,
549
- Folder: [emitOrtho(folder, origin, enuWps, shootInterval)],
566
+ Folder: [emitOrtho(folder, origin, enuWps, shootInterval, options)],
550
567
  };
551
568
  }
@@ -1,4 +1,4 @@
1
- import { degrees } from "../types/branded.js";
1
+ import { degrees, type Meters } from "../types/branded.js";
2
2
  import type { Action, GimbalRotateAction } from "../types/action.js";
3
3
  import type { ExecuteHeightMode, WaypointHeadingMode } from "../types/enums.js";
4
4
  import type { Mapping3dFolder, Mapping3dTemplate } from "../types/template.js";
@@ -131,6 +131,7 @@ export function planMapping3d(template: Mapping3dTemplate, options?: PlanOptions
131
131
  headingAngle: degrees(wrap180(baseDirection + pass.headingOffsetDeg)),
132
132
  gimbalPitch: pass.pitch === "nadir" ? degrees(-90) : inclined,
133
133
  interval: pass.interval,
134
+ options,
134
135
  });
135
136
  }),
136
137
  };
@@ -141,6 +142,20 @@ interface PassConfig {
141
142
  headingAngle: ReturnType<typeof degrees>;
142
143
  gimbalPitch: ReturnType<typeof degrees>;
143
144
  interval: number;
145
+ options?: PlanOptions;
146
+ }
147
+
148
+ function executeHeightFor(
149
+ folder: Mapping3dFolder,
150
+ coord: { lng: number; lat: number },
151
+ index: number,
152
+ waylineId: number,
153
+ options?: PlanOptions,
154
+ ): Meters {
155
+ if (folder.waylineCoordinateSysParam.heightMode !== "aboveGroundLevel") {
156
+ return folder.Placemark.height;
157
+ }
158
+ return options?.resolveExecuteHeight?.({ ...coord, index, waylineId }) ?? folder.Placemark.height;
144
159
  }
145
160
 
146
161
  function buildPass(
@@ -152,10 +167,11 @@ function buildPass(
152
167
  ): WaylinesFolder {
153
168
  const lastIndex = enuWps.length - 1;
154
169
  const placemarks: WaylinesPlacemark[] = enuWps.map((enu, index) => {
170
+ const coord = enuToLngLat(enu, origin);
155
171
  const placemark: WaylinesPlacemark = {
156
- Point: { coordinates: enuToLngLat(enu, origin) },
172
+ Point: { coordinates: coord },
157
173
  index,
158
- executeHeight: folder.Placemark.height,
174
+ executeHeight: executeHeightFor(folder, coord, index, waylineId, cfg.options),
159
175
  waypointSpeed: folder.Placemark.inclinedFlightSpeed,
160
176
  waypointHeadingParam: {
161
177
  waypointHeadingMode: cfg.headingMode,
@@ -25,7 +25,12 @@ export function planWaypoint(template: WaypointTemplate): Waylines {
25
25
  : p.waypointTurnParam;
26
26
 
27
27
  const executeHeight =
28
- p.useGlobalHeight || p.height === undefined ? folder.globalHeight : p.height;
28
+ folder.waylineCoordinateSysParam.heightMode === "aboveGroundLevel" &&
29
+ p.ellipsoidHeight !== undefined
30
+ ? p.ellipsoidHeight
31
+ : p.useGlobalHeight || p.height === undefined
32
+ ? folder.globalHeight
33
+ : p.height;
29
34
 
30
35
  const waypointSpeed =
31
36
  p.useGlobalSpeed || p.waypointSpeed === undefined ? folder.autoFlightSpeed : p.waypointSpeed;
@@ -58,17 +63,17 @@ export function planWaypoint(template: WaypointTemplate): Waylines {
58
63
  /**
59
64
  * Map a template heightMode to a waylines executeHeightMode.
60
65
  * - `EGM96` → `WGS84` (waylines spec doesn't expose EGM96; runtime resolves it).
61
- * - `aboveGroundLevel` → `realTimeFollowSurface` (closest executable analog).
66
+ * - `aboveGroundLevel` → `WGS84` (waypoint waylines use per-point ellipsoid heights).
62
67
  */
63
68
  function heightModeToExecuteMode(
64
69
  mode: WaypointFolder["waylineCoordinateSysParam"]["heightMode"],
65
70
  ): ExecuteHeightMode {
66
71
  switch (mode) {
67
72
  case "EGM96":
73
+ case "aboveGroundLevel":
68
74
  return "WGS84";
69
75
  case "relativeToStartPoint":
70
76
  return "relativeToStartPoint";
71
- case "aboveGroundLevel":
72
77
  case "realTimeFollowSurface":
73
78
  return "realTimeFollowSurface";
74
79
  }
@@ -61,6 +61,52 @@ describe("plan", () => {
61
61
  expect(waylines.missionConfig).toBe(template.missionConfig);
62
62
  });
63
63
 
64
+ it("waypoint aboveGroundLevel uses per-point ellipsoid heights as WGS84 executeHeight", () => {
65
+ const template: WaypointTemplate = {
66
+ missionConfig,
67
+ Folder: {
68
+ templateType: "waypoint",
69
+ templateId: 0,
70
+ waylineCoordinateSysParam: { coordinateMode: "WGS84", heightMode: "aboveGroundLevel" },
71
+ autoFlightSpeed: metersPerSecond(10),
72
+ globalWaypointTurnMode: "toPointAndStopWithDiscontinuityCurvature",
73
+ gimbalPitchMode: "manual",
74
+ globalHeight: meters(50),
75
+ globalWaypointHeadingParam: {
76
+ waypointHeadingMode: "followWayline",
77
+ waypointHeadingPathMode: "followBadArc",
78
+ },
79
+ Placemark: [
80
+ {
81
+ Point: { coordinates: { lng: 113.938, lat: 22.5397 } },
82
+ index: 0,
83
+ useGlobalHeight: false,
84
+ height: meters(80),
85
+ ellipsoidHeight: meters(180),
86
+ useGlobalSpeed: true,
87
+ useGlobalHeadingParam: true,
88
+ useGlobalTurnParam: true,
89
+ },
90
+ {
91
+ Point: { coordinates: { lng: 113.939, lat: 22.5407 } },
92
+ index: 1,
93
+ useGlobalHeight: false,
94
+ height: meters(90),
95
+ ellipsoidHeight: meters(190),
96
+ useGlobalSpeed: true,
97
+ useGlobalHeadingParam: true,
98
+ useGlobalTurnParam: true,
99
+ },
100
+ ],
101
+ },
102
+ };
103
+
104
+ const waylines = plan(template);
105
+
106
+ expect(waylines.Folder[0]!.executeHeightMode).toBe("WGS84");
107
+ expect(waylines.Folder[0]!.Placemark.map((p) => Number(p.executeHeight))).toEqual([180, 190]);
108
+ });
109
+
64
110
  it("compiles a mapping2d template into a serpentine scan", () => {
65
111
  const template: Mapping2dTemplate = {
66
112
  missionConfig,
@@ -130,6 +176,89 @@ describe("plan", () => {
130
176
  expect(plan(template, { lineSpacing: meters(30) }).Folder).toHaveLength(1);
131
177
  });
132
178
 
179
+ it("mapping2d aboveGroundLevel uses per-waypoint HAE from resolveExecuteHeight", () => {
180
+ const template: Mapping2dTemplate = {
181
+ missionConfig,
182
+ Folder: {
183
+ templateType: "mapping2d",
184
+ templateId: 2,
185
+ waylineCoordinateSysParam: { coordinateMode: "WGS84", heightMode: "aboveGroundLevel" },
186
+ autoFlightSpeed: metersPerSecond(10),
187
+ Placemark: {
188
+ elevationOptimizeEnable: false,
189
+ shootType: "distance",
190
+ direction: degrees(0),
191
+ margin: 0,
192
+ overlap: { orthoCameraOverlapH: 80, orthoCameraOverlapW: 70 },
193
+ ellipsoidHeight: meters(150),
194
+ height: meters(100),
195
+ Polygon: {
196
+ outerBoundaryIs: {
197
+ LinearRing: {
198
+ coordinates:
199
+ "113.938,22.5397,0 113.940,22.5397,0 113.940,22.5407,0 113.938,22.5407,0 113.938,22.5397,0",
200
+ },
201
+ },
202
+ },
203
+ },
204
+ },
205
+ };
206
+
207
+ const calls: Array<{ lng: number; lat: number; index: number; waylineId: number }> = [];
208
+ const waylines = plan(template, {
209
+ lineSpacing: meters(45),
210
+ resolveExecuteHeight: (arg) => {
211
+ calls.push(arg);
212
+ return meters(300 + arg.index);
213
+ },
214
+ });
215
+
216
+ expect(calls).toHaveLength(waylines.Folder[0]!.Placemark.length);
217
+ expect(calls[0]!.waylineId).toBe(0);
218
+ expect(calls[0]!.index).toBe(0);
219
+ expect(calls[0]!.lng).toBe(waylines.Folder[0]!.Placemark[0]!.Point.coordinates.lng);
220
+ expect(calls[0]!.lat).toBe(waylines.Folder[0]!.Placemark[0]!.Point.coordinates.lat);
221
+ expect(waylines.Folder[0]!.executeHeightMode).toBe("realTimeFollowSurface");
222
+ expect(waylines.Folder[0]!.Placemark[0]!.executeHeight).toBe(300);
223
+ expect(waylines.Folder[0]!.Placemark[1]!.executeHeight).toBe(301);
224
+ });
225
+
226
+ it("mapping2d realTimeFollowSurface keeps template height and does not call resolveExecuteHeight", () => {
227
+ const template: Mapping2dTemplate = {
228
+ missionConfig,
229
+ Folder: {
230
+ templateType: "mapping2d",
231
+ templateId: 2,
232
+ waylineCoordinateSysParam: { coordinateMode: "WGS84", heightMode: "realTimeFollowSurface" },
233
+ autoFlightSpeed: metersPerSecond(10),
234
+ Placemark: {
235
+ elevationOptimizeEnable: false,
236
+ shootType: "distance",
237
+ direction: degrees(0),
238
+ margin: 0,
239
+ overlap: { orthoCameraOverlapH: 80, orthoCameraOverlapW: 70 },
240
+ ellipsoidHeight: meters(150),
241
+ height: meters(100),
242
+ Polygon: {
243
+ outerBoundaryIs: {
244
+ LinearRing: {
245
+ coordinates:
246
+ "113.938,22.5397,0 113.940,22.5397,0 113.940,22.5407,0 113.938,22.5407,0 113.938,22.5397,0",
247
+ },
248
+ },
249
+ },
250
+ },
251
+ },
252
+ };
253
+
254
+ const waylines = plan(template, {
255
+ lineSpacing: meters(45),
256
+ resolveExecuteHeight: () => meters(300),
257
+ });
258
+
259
+ expect(waylines.Folder[0]!.Placemark[0]!.executeHeight).toBe(100);
260
+ });
261
+
133
262
  it("mapping2d ortho: grid density follows provided lineSpacing", () => {
134
263
  const template: Mapping2dTemplate = {
135
264
  missionConfig,
@@ -347,4 +476,54 @@ describe("plan", () => {
347
476
  // lines than the same-axis nadir pass, despite covering the same width.
348
477
  expect(wl.Folder[1]!.Placemark.length).toBeLessThan(wl.Folder[0]!.Placemark.length);
349
478
  });
479
+
480
+ it("mapping3d aboveGroundLevel uses per-waypoint HAE from resolveExecuteHeight for each pass", () => {
481
+ const template: Mapping3dTemplate = {
482
+ missionConfig,
483
+ Folder: {
484
+ templateType: "mapping3d",
485
+ templateId: 0,
486
+ waylineCoordinateSysParam: { coordinateMode: "WGS84", heightMode: "aboveGroundLevel" },
487
+ autoFlightSpeed: metersPerSecond(10),
488
+ Placemark: {
489
+ inclinedGimbalPitch: degrees(-45),
490
+ inclinedFlightSpeed: metersPerSecond(10),
491
+ shootType: "time",
492
+ direction: degrees(0),
493
+ margin: 0,
494
+ overlap: { orthoCameraOverlapH: 80, orthoCameraOverlapW: 70 },
495
+ ellipsoidHeight: meters(150),
496
+ height: meters(100),
497
+ Polygon: {
498
+ outerBoundaryIs: {
499
+ LinearRing: {
500
+ coordinates:
501
+ "113.9380,22.5397,0 113.9460,22.5397,0 113.9460,22.5417,0 113.9380,22.5417,0 113.9380,22.5397,0",
502
+ },
503
+ },
504
+ },
505
+ },
506
+ },
507
+ };
508
+
509
+ const calls: Array<{ lng: number; lat: number; index: number; waylineId: number }> = [];
510
+ const waylines = plan(template, {
511
+ lineSpacing: meters(45),
512
+ resolveExecuteHeight: (arg) => {
513
+ calls.push(arg);
514
+ return meters(500 + arg.waylineId * 100 + arg.index);
515
+ },
516
+ });
517
+
518
+ const placemarkCount = waylines.Folder.reduce(
519
+ (sum, folder) => sum + folder.Placemark.length,
520
+ 0,
521
+ );
522
+ expect(calls).toHaveLength(placemarkCount);
523
+ expect(waylines.Folder.map((folder) => folder.Placemark[0]!.executeHeight)).toEqual([
524
+ 500, 600, 700, 800, 900,
525
+ ]);
526
+ expect(calls.some((call) => call.waylineId === 4 && call.index === 0)).toBe(true);
527
+ expect(calls.every((call) => "heightMode" in call === false)).toBe(true);
528
+ });
350
529
  });