@lumikmz/kmz 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +63 -6
- package/dist/index.mjs +190 -19
- package/package.json +1 -1
- package/src/plan/geometry.ts +1 -1
- package/src/plan/index.ts +8 -0
- package/src/plan/plan-target-detection.test.ts +143 -0
- package/src/plan/plan-target-detection.ts +233 -0
- package/src/types/action.ts +26 -0
- package/src/types/drone-payload.ts +2 -0
- package/src/types/enums.ts +8 -1
- package/src/types/mission-config.ts +2 -0
- package/src/types/placemark.ts +18 -1
- package/src/types/template.ts +18 -1
- package/src/validate/validate-template.ts +11 -0
package/dist/index.d.mts
CHANGED
|
@@ -36,7 +36,7 @@ type FlyToWaylineMode = "safely" | "pointToPoint";
|
|
|
36
36
|
type FinishAction = "goHome" | "noAction" | "autoLand" | "gotoFirstWaypoint";
|
|
37
37
|
type ExitOnRCLost = "goContinue" | "executeLostAction";
|
|
38
38
|
type ExecuteRCLostAction = "goBack" | "landing" | "hover";
|
|
39
|
-
type TemplateType = "waypoint" | "mapping2d" | "mapping3d" | "mappingStrip";
|
|
39
|
+
type TemplateType = "waypoint" | "mapping2d" | "mapping3d" | "mappingStrip" | "targetdetection";
|
|
40
40
|
type CoordinateMode = "WGS84";
|
|
41
41
|
/** Used in template.kml `waylineCoordinateSysParam.heightMode`. */
|
|
42
42
|
type TemplateHeightMode = "EGM96" | "relativeToStartPoint" | "aboveGroundLevel" | "realTimeFollowSurface";
|
|
@@ -58,8 +58,8 @@ type MeteringMode = "average" | "spot";
|
|
|
58
58
|
type LidarReturnMode = "singleReturnStrongest" | "dualReturn" | "tripleReturn";
|
|
59
59
|
type LidarScanningMode = "repetitive" | "nonRepetitive";
|
|
60
60
|
type ActionGroupMode = "sequence";
|
|
61
|
-
type ActionTriggerType = "reachPoint" | "betweenAdjacentPoints" | "multipleTiming" | "multipleDistance";
|
|
62
|
-
type ActionActuatorFunc = "takePhoto" | "startRecord" | "stopRecord" | "focus" | "zoom" | "customDirName" | "gimbalRotate" | "rotateYaw" | "hover" | "gimbalEvenlyRotate" | "accurateShoot" | "orientedShoot" | "panoShot" | "recordPointCloud" | "megaphone" | "searchlight" /** ⚠️ NON-SPEC — observed in real DJI KMZ exports, absent from WPML 1.15 docs. */ | "startSmartOblique" /** ⚠️ NON-SPEC — observed in real DJI KMZ exports, absent from WPML 1.15 docs. */ | "stopSmartOblique" /** Starts time-lapse (interval) shooting for a mapping strip. */ | "startTimeLapse" /** Stops time-lapse shooting. */ | "stopTimeLapse" /** Locks gimbal angle during a mapping strip. */ | "gimbalAngleLock" /** Unlocks gimbal angle (no params in XML). */ | "gimbalAngleUnlock" /** Sets camera focus mode (manual / auto). */ | "setFocusType";
|
|
61
|
+
type ActionTriggerType = "reachPoint" | "betweenAdjacentPoints" | "betweenAdjacentPointsIncludeFirstPoint" | "multipleTiming" | "multipleDistance";
|
|
62
|
+
type ActionActuatorFunc = "targetDetection" | "takePhoto" | "startRecord" | "stopRecord" | "focus" | "zoom" | "customDirName" | "gimbalRotate" | "rotateYaw" | "hover" | "gimbalEvenlyRotate" | "accurateShoot" | "orientedShoot" | "panoShot" | "recordPointCloud" | "megaphone" | "searchlight" /** ⚠️ NON-SPEC — observed in real DJI KMZ exports, absent from WPML 1.15 docs. */ | "startSmartOblique" /** ⚠️ NON-SPEC — observed in real DJI KMZ exports, absent from WPML 1.15 docs. */ | "stopSmartOblique" /** Starts time-lapse (interval) shooting for a mapping strip. */ | "startTimeLapse" /** Stops time-lapse shooting. */ | "stopTimeLapse" /** Locks gimbal angle during a mapping strip. */ | "gimbalAngleLock" /** Unlocks gimbal angle (no params in XML). */ | "gimbalAngleUnlock" /** Sets camera focus mode (manual / auto). */ | "setFocusType";
|
|
63
63
|
type GimbalHeadingYawBase = "north" | "aircraft";
|
|
64
64
|
type GimbalRotateMode = "absoluteAngle";
|
|
65
65
|
type AircraftPathMode = "clockwise" | "counterClockwise";
|
|
@@ -81,6 +81,8 @@ interface PayloadInfo {
|
|
|
81
81
|
*/
|
|
82
82
|
payloadSubEnumValue?: number;
|
|
83
83
|
payloadPositionIndex: number;
|
|
84
|
+
/** FlightHub 2 patrol-camera selection, in 35mm-equivalent millimeters. */
|
|
85
|
+
equivalentFocalLength?: number;
|
|
84
86
|
}
|
|
85
87
|
/** Aircraft `{ droneEnumValue, droneSubEnumValue }` tuples. */
|
|
86
88
|
declare const DroneEnum: {
|
|
@@ -264,6 +266,8 @@ interface MissionConfig {
|
|
|
264
266
|
globalRTHHeight: Meters;
|
|
265
267
|
droneInfo: DroneInfo;
|
|
266
268
|
payloadInfo: PayloadInfo[];
|
|
269
|
+
/** FlightHub 2 custom restricted-area avoidance switch. */
|
|
270
|
+
waylineAvoidLimitAreaMode?: boolean;
|
|
267
271
|
/** Only M3D/M3TD, M4D/M4TD, M4E/M4T. */
|
|
268
272
|
autoRerouteInfo?: AutoRerouteInfo;
|
|
269
273
|
}
|
|
@@ -397,12 +401,36 @@ interface ActionTrigger {
|
|
|
397
401
|
actionTriggerParam?: number;
|
|
398
402
|
}
|
|
399
403
|
/** Discriminated by `actionActuatorFunc`. */
|
|
400
|
-
type Action = TakePhotoAction | StartRecordAction | StopRecordAction | FocusAction | ZoomAction | CustomDirNameAction | GimbalRotateAction | RotateYawAction | HoverAction | GimbalEvenlyRotateAction | AccurateShootAction | OrientedShootAction | PanoShotAction | RecordPointCloudAction | MegaphoneAction | SearchlightAction | StartSmartObliqueAction | StopSmartObliqueAction | StartTimeLapseAction | StopTimeLapseAction | GimbalAngleLockAction | GimbalAngleUnlockAction | SetFocusTypeAction;
|
|
404
|
+
type Action = TargetDetectionAction | TakePhotoAction | StartRecordAction | StopRecordAction | FocusAction | ZoomAction | CustomDirNameAction | GimbalRotateAction | RotateYawAction | HoverAction | GimbalEvenlyRotateAction | AccurateShootAction | OrientedShootAction | PanoShotAction | RecordPointCloudAction | MegaphoneAction | SearchlightAction | StartSmartObliqueAction | StopSmartObliqueAction | StartTimeLapseAction | StopTimeLapseAction | GimbalAngleLockAction | GimbalAngleUnlockAction | SetFocusTypeAction;
|
|
401
405
|
interface ActionBase<F extends string, P> {
|
|
402
406
|
actionId: number;
|
|
403
407
|
actionActuatorFunc: F;
|
|
404
408
|
actionActuatorFuncParam: P;
|
|
405
409
|
}
|
|
410
|
+
interface TargetDetectionParam {
|
|
411
|
+
focalLength?: number;
|
|
412
|
+
payloadPositionIndex: number;
|
|
413
|
+
useGlobalPayloadLensIndex: boolean;
|
|
414
|
+
payloadLensIndex: LensType[];
|
|
415
|
+
actionUUID: string;
|
|
416
|
+
irGainMode?: string;
|
|
417
|
+
irColorPalette?: string;
|
|
418
|
+
customTitle: string;
|
|
419
|
+
customText: string;
|
|
420
|
+
modelIndex: number;
|
|
421
|
+
modelScore: number;
|
|
422
|
+
alarmInterval: number;
|
|
423
|
+
useThirdModel: boolean;
|
|
424
|
+
algorithmType: string;
|
|
425
|
+
targetParam: {
|
|
426
|
+
maxThreshold: number;
|
|
427
|
+
minThreshold: number;
|
|
428
|
+
useMaxThreshold: boolean;
|
|
429
|
+
useMinThreshold: boolean;
|
|
430
|
+
targetType: string;
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
type TargetDetectionAction = ActionBase<"targetDetection", TargetDetectionParam>;
|
|
406
434
|
interface TakePhotoParam {
|
|
407
435
|
payloadPositionIndex: number;
|
|
408
436
|
fileSuffix?: string;
|
|
@@ -749,6 +777,22 @@ interface Mapping3dPlacemark {
|
|
|
749
777
|
ellipsoidHeight: Meters;
|
|
750
778
|
height: Meters;
|
|
751
779
|
}
|
|
780
|
+
/** `Placemark` inside a FlightHub 2 patrol-area `targetdetection` Folder. */
|
|
781
|
+
interface TargetDetectionPlacemark {
|
|
782
|
+
direction: Degrees;
|
|
783
|
+
margin: number;
|
|
784
|
+
gimbalPitchMode: "fixed";
|
|
785
|
+
overlap: Overlap;
|
|
786
|
+
Polygon: TemplatePolygon;
|
|
787
|
+
ellipsoidHeight: Meters;
|
|
788
|
+
height: Meters;
|
|
789
|
+
mappingHeadingParam: MappingHeadingParam;
|
|
790
|
+
gimbalPitchAngle: Degrees;
|
|
791
|
+
recordEnable: boolean;
|
|
792
|
+
/** FlightHub 2 always emits this action for patrol-area routes. */
|
|
793
|
+
targetDetectionActionEnable?: boolean;
|
|
794
|
+
action?: TargetDetectionAction;
|
|
795
|
+
}
|
|
752
796
|
/** `Placemark` inside a `mappingStrip` Folder — uses a LineString; params live here. */
|
|
753
797
|
interface MappingStripPlacemark {
|
|
754
798
|
caliFlightEnable: boolean;
|
|
@@ -810,7 +854,7 @@ interface WaylinesPlacemark {
|
|
|
810
854
|
* `template.kml` document. Mirrors `<kml><Document>` structure.
|
|
811
855
|
* See `docs/kmz/template-kml.html`.
|
|
812
856
|
*/
|
|
813
|
-
type Template = WaypointTemplate | Mapping2dTemplate | Mapping3dTemplate | MappingStripTemplate;
|
|
857
|
+
type Template = WaypointTemplate | Mapping2dTemplate | Mapping3dTemplate | MappingStripTemplate | TargetDetectionTemplate;
|
|
814
858
|
interface TemplateDocumentBase {
|
|
815
859
|
/** Unix milliseconds. */
|
|
816
860
|
author?: string;
|
|
@@ -870,6 +914,17 @@ interface Mapping3dFolder {
|
|
|
870
914
|
/** Holds the survey polygon AND all shooting/mapping params. */
|
|
871
915
|
Placemark: Mapping3dPlacemark;
|
|
872
916
|
}
|
|
917
|
+
interface TargetDetectionTemplate extends TemplateDocumentBase {
|
|
918
|
+
Folder: TargetDetectionFolder;
|
|
919
|
+
}
|
|
920
|
+
interface TargetDetectionFolder {
|
|
921
|
+
templateType: "targetdetection";
|
|
922
|
+
templateId: number;
|
|
923
|
+
waylineCoordinateSysParam: WaylineCoordinateSysParam;
|
|
924
|
+
autoFlightSpeed: MetersPerSecond;
|
|
925
|
+
payloadParam?: PayloadParam;
|
|
926
|
+
Placemark: TargetDetectionPlacemark;
|
|
927
|
+
}
|
|
873
928
|
interface MappingStripTemplate extends TemplateDocumentBase {
|
|
874
929
|
Folder: MappingStripFolder;
|
|
875
930
|
}
|
|
@@ -940,6 +995,8 @@ interface PlanOptions {
|
|
|
940
995
|
stripDirection?: "parallel" | "perpendicular";
|
|
941
996
|
/** Reverse mappingStrip region execution without changing region identities. */
|
|
942
997
|
regionOrderReversed?: boolean;
|
|
998
|
+
/** Preferred entry location for path variants that can be reversed as a whole. */
|
|
999
|
+
preferredStartPoint?: LngLat;
|
|
943
1000
|
resolveExecuteHeight?: (point: {
|
|
944
1001
|
lng: number;
|
|
945
1002
|
lat: number;
|
|
@@ -1003,4 +1060,4 @@ declare function validate(doc: Template | Waylines): Issue[];
|
|
|
1003
1060
|
/** Great-circle distance between two WGS-84 coordinates, in meters. */
|
|
1004
1061
|
declare function haversineMeters(a: LngLat, b: LngLat): number;
|
|
1005
1062
|
//#endregion
|
|
1006
|
-
export { AccurateShootAction, AccurateShootParam, Action, ActionActuatorFunc, ActionGroup, ActionGroupMode, ActionTrigger, ActionTriggerType, AircraftPathMode, AutoRerouteInfo, CoordinateMode, CustomDirNameAction, CustomDirNameParam, Degrees, DroneEnum, DroneInfo, ExecuteHeightMode, ExecuteRCLostAction, ExitOnRCLost, FinishAction, FlyToWaylineMode, FocusAction, FocusMode, FocusParam, GimbalAngleLockAction, GimbalAngleLockParam, GimbalAngleUnlockAction, GimbalEvenlyRotateAction, GimbalEvenlyRotateParam, GimbalHeadingYawBase, GimbalPitchMode, GimbalRotateAction, GimbalRotateMode, GimbalRotateParam, HoverAction, HoverParam, Issue, KmzError, LensType, LidarReturnMode, LidarScanningMode, LngLat, LngLatHeight, Mapping2dFolder, Mapping2dPlacemark, Mapping2dTemplate, Mapping3dFolder, Mapping3dPlacemark, Mapping3dTemplate, MappingHeadingMode, MappingHeadingParam, type MappingStripDerived, MappingStripFolder, MappingStripPlacemark, type MappingStripRegion, type MappingStripRegionRoute, MappingStripTemplate, MegaphoneAction, MegaphoneParam, MegaphoneTtsParam, MeteringMode, Meters, MetersPerSecond, MissionConfig, OrientedPhotoMode, OrientedShootAction, OrientedShootParam, Overlap, PanoShotAction, PanoShotParam, PanoShotSubMode, PayloadEnum, PayloadInfo, PayloadParam, type PlanOptions, PlannerError, PositioningType, RecordPointCloudAction, RecordPointCloudOperate, RecordPointCloudParam, RotateYawAction, RotateYawParam, SearchlightAction, SearchlightParam, SetFocusTypeAction, SetFocusTypeParam, ShootType, SmartObliquePoint, StartActionGroup, StartRecordAction, StartRecordParam, StartSmartObliqueAction, StartSmartObliqueParam, StartTimeLapseAction, StartTimeLapseParam, StopRecordAction, StopRecordParam, StopSmartObliqueAction, StopSmartObliqueParam, StopTimeLapseAction, StopTimeLapseParam, TakePhotoAction, TakePhotoParam, Template, TemplateHeightMode, TemplatePolygon, TemplateType, TemplateWaypointPlacemark, WaylineCoordinateSysParam, Waylines, WaylinesFolder, WaylinesPlacemark, WaypointFolder, WaypointHeadingMode, WaypointHeadingParam, WaypointHeadingPathMode, WaypointTemplate, WaypointTurnMode, WaypointTurnParam, ZoomAction, ZoomParam, degrees, deriveMappingStrip, haversineMeters, meters, metersPerSecond, plan, validate };
|
|
1063
|
+
export { AccurateShootAction, AccurateShootParam, Action, ActionActuatorFunc, ActionGroup, ActionGroupMode, ActionTrigger, ActionTriggerType, AircraftPathMode, AutoRerouteInfo, CoordinateMode, CustomDirNameAction, CustomDirNameParam, Degrees, DroneEnum, DroneInfo, ExecuteHeightMode, ExecuteRCLostAction, ExitOnRCLost, FinishAction, FlyToWaylineMode, FocusAction, FocusMode, FocusParam, GimbalAngleLockAction, GimbalAngleLockParam, GimbalAngleUnlockAction, GimbalEvenlyRotateAction, GimbalEvenlyRotateParam, GimbalHeadingYawBase, GimbalPitchMode, GimbalRotateAction, GimbalRotateMode, GimbalRotateParam, HoverAction, HoverParam, Issue, KmzError, LensType, LidarReturnMode, LidarScanningMode, LngLat, LngLatHeight, Mapping2dFolder, Mapping2dPlacemark, Mapping2dTemplate, Mapping3dFolder, Mapping3dPlacemark, Mapping3dTemplate, MappingHeadingMode, MappingHeadingParam, type MappingStripDerived, MappingStripFolder, MappingStripPlacemark, type MappingStripRegion, type MappingStripRegionRoute, MappingStripTemplate, MegaphoneAction, MegaphoneParam, MegaphoneTtsParam, MeteringMode, Meters, MetersPerSecond, MissionConfig, OrientedPhotoMode, OrientedShootAction, OrientedShootParam, Overlap, PanoShotAction, PanoShotParam, PanoShotSubMode, PayloadEnum, PayloadInfo, PayloadParam, type PlanOptions, PlannerError, PositioningType, RecordPointCloudAction, RecordPointCloudOperate, RecordPointCloudParam, RotateYawAction, RotateYawParam, SearchlightAction, SearchlightParam, SetFocusTypeAction, SetFocusTypeParam, ShootType, SmartObliquePoint, StartActionGroup, StartRecordAction, StartRecordParam, StartSmartObliqueAction, StartSmartObliqueParam, StartTimeLapseAction, StartTimeLapseParam, StopRecordAction, StopRecordParam, StopSmartObliqueAction, StopSmartObliqueParam, StopTimeLapseAction, StopTimeLapseParam, TakePhotoAction, TakePhotoParam, TargetDetectionAction, TargetDetectionFolder, TargetDetectionParam, TargetDetectionPlacemark, TargetDetectionTemplate, Template, TemplateHeightMode, TemplatePolygon, TemplateType, TemplateWaypointPlacemark, WaylineCoordinateSysParam, Waylines, WaylinesFolder, WaylinesPlacemark, WaypointFolder, WaypointHeadingMode, WaypointHeadingParam, WaypointHeadingPathMode, WaypointTemplate, WaypointTurnMode, WaypointTurnParam, ZoomAction, ZoomParam, degrees, deriveMappingStrip, haversineMeters, meters, metersPerSecond, plan, validate };
|
package/dist/index.mjs
CHANGED
|
@@ -182,22 +182,22 @@ const PayloadEnum = {
|
|
|
182
182
|
//#endregion
|
|
183
183
|
//#region src/plan/geometry.ts
|
|
184
184
|
const EARTH_RADIUS = 6371e3;
|
|
185
|
-
const DEG2RAD = Math.PI / 180;
|
|
186
|
-
const RAD2DEG = 180 / Math.PI;
|
|
185
|
+
const DEG2RAD$1 = Math.PI / 180;
|
|
186
|
+
const RAD2DEG$1 = 180 / Math.PI;
|
|
187
187
|
const ANGLE_EPS_RAD = 1e-12;
|
|
188
188
|
/** Project lng/lat to local ENU meters around `origin`. */
|
|
189
189
|
function lngLatToEnu(point, origin) {
|
|
190
|
-
const dLat = (point.lat - origin.lat) * DEG2RAD;
|
|
191
|
-
const dLng = (point.lng - origin.lng) * DEG2RAD;
|
|
192
|
-
const avgLat = (point.lat + origin.lat) / 2 * DEG2RAD;
|
|
190
|
+
const dLat = (point.lat - origin.lat) * DEG2RAD$1;
|
|
191
|
+
const dLng = (point.lng - origin.lng) * DEG2RAD$1;
|
|
192
|
+
const avgLat = (point.lat + origin.lat) / 2 * DEG2RAD$1;
|
|
193
193
|
return [dLng * EARTH_RADIUS * Math.cos(avgLat), dLat * EARTH_RADIUS];
|
|
194
194
|
}
|
|
195
195
|
/** Inverse of `lngLatToEnu`. */
|
|
196
196
|
function enuToLngLat([x, y], origin) {
|
|
197
|
-
const avgLat = origin.lat * DEG2RAD;
|
|
197
|
+
const avgLat = origin.lat * DEG2RAD$1;
|
|
198
198
|
return {
|
|
199
|
-
lng: origin.lng + x / (EARTH_RADIUS * Math.cos(avgLat)) * RAD2DEG,
|
|
200
|
-
lat: origin.lat + y / EARTH_RADIUS * RAD2DEG
|
|
199
|
+
lng: origin.lng + x / (EARTH_RADIUS * Math.cos(avgLat)) * RAD2DEG$1,
|
|
200
|
+
lat: origin.lat + y / EARTH_RADIUS * RAD2DEG$1
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
function centroid(points) {
|
|
@@ -217,7 +217,7 @@ function centroid(points) {
|
|
|
217
217
|
* ENU axes: x=east, y=north.
|
|
218
218
|
*/
|
|
219
219
|
function bearingDeg(from, to) {
|
|
220
|
-
return Math.atan2(to[0] - from[0], to[1] - from[1]) * RAD2DEG;
|
|
220
|
+
return Math.atan2(to[0] - from[0], to[1] - from[1]) * RAD2DEG$1;
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
223
|
* Orient an ordered survey path so its first point is the end nearest `home`.
|
|
@@ -233,10 +233,10 @@ function orientPathToHome(enuWps, origin, home) {
|
|
|
233
233
|
}
|
|
234
234
|
/** Great-circle distance between two WGS-84 coordinates, in meters. */
|
|
235
235
|
function haversineMeters(a, b) {
|
|
236
|
-
const dLat = (b.lat - a.lat) * DEG2RAD;
|
|
237
|
-
const dLng = (b.lng - a.lng) * DEG2RAD;
|
|
238
|
-
const lat1 = a.lat * DEG2RAD;
|
|
239
|
-
const lat2 = b.lat * DEG2RAD;
|
|
236
|
+
const dLat = (b.lat - a.lat) * DEG2RAD$1;
|
|
237
|
+
const dLng = (b.lng - a.lng) * DEG2RAD$1;
|
|
238
|
+
const lat1 = a.lat * DEG2RAD$1;
|
|
239
|
+
const lat2 = b.lat * DEG2RAD$1;
|
|
240
240
|
const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
|
|
241
241
|
return EARTH_RADIUS * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
242
242
|
}
|
|
@@ -367,7 +367,7 @@ function stabilizeTurnCorridors(segments) {
|
|
|
367
367
|
*/
|
|
368
368
|
function surveyGrid(ring, opts) {
|
|
369
369
|
const { directionDeg, marginM, lineSpacing, phaseOffsetM = 0, lengthwiseShiftM = 0 } = opts;
|
|
370
|
-
const t = directionDeg * DEG2RAD;
|
|
370
|
+
const t = directionDeg * DEG2RAD$1;
|
|
371
371
|
const rawAlpha = Math.atan2(Math.cos(t), Math.sin(t));
|
|
372
372
|
const alpha = Math.abs(rawAlpha) < ANGLE_EPS_RAD ? 0 : rawAlpha;
|
|
373
373
|
const scanRing = ring.map((p) => rotateEnu(p, -alpha));
|
|
@@ -461,7 +461,7 @@ function enuSegLen(a, b) {
|
|
|
461
461
|
* Clamping each side to MAX_DAMPING_FRAC × its shorter adjacent segment
|
|
462
462
|
* guarantees the sum is at most 2 × MAX_DAMPING_FRAC < 1 of that segment.
|
|
463
463
|
*/
|
|
464
|
-
function turnDamping$
|
|
464
|
+
function turnDamping$2(enuWps, i) {
|
|
465
465
|
const dPrev = enuSegLen(enuWps[i - 1], enuWps[i]);
|
|
466
466
|
const dNext = enuSegLen(enuWps[i], enuWps[i + 1]);
|
|
467
467
|
return meters(Math.min(DESIRED_TURN_DAMPING_M, dPrev * MAX_DAMPING_FRAC, dNext * MAX_DAMPING_FRAC));
|
|
@@ -744,7 +744,7 @@ function emitOrtho(folder, origin, enuWps, shootInterval, options) {
|
|
|
744
744
|
waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
|
|
745
745
|
waypointTurnParam: index === 0 || isLast ? stopTurn() : {
|
|
746
746
|
waypointTurnMode: "coordinateTurn",
|
|
747
|
-
waypointTurnDampingDist: turnDamping$
|
|
747
|
+
waypointTurnDampingDist: turnDamping$2(enuWps, index)
|
|
748
748
|
},
|
|
749
749
|
...COMMON_WP_EXTRAS
|
|
750
750
|
};
|
|
@@ -783,7 +783,7 @@ function emitSmartOblique(folder, origin, enuWps, shootInterval, options) {
|
|
|
783
783
|
waypointHeadingParam: mappingHeadingParam(headingAngle, !isLast),
|
|
784
784
|
waypointTurnParam: index === 0 || isLast ? stopTurn() : {
|
|
785
785
|
waypointTurnMode: "coordinateTurn",
|
|
786
|
-
waypointTurnDampingDist: turnDamping$
|
|
786
|
+
waypointTurnDampingDist: turnDamping$2(enuWps, index)
|
|
787
787
|
},
|
|
788
788
|
...COMMON_WP_EXTRAS
|
|
789
789
|
};
|
|
@@ -1507,7 +1507,7 @@ function heightModeToExecuteMode$1(folder) {
|
|
|
1507
1507
|
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
1508
1508
|
}
|
|
1509
1509
|
}
|
|
1510
|
-
function turnDamping(points, index) {
|
|
1510
|
+
function turnDamping$1(points, index) {
|
|
1511
1511
|
const previous = haversineMeters(points[index - 1], points[index]);
|
|
1512
1512
|
const next = haversineMeters(points[index], points[index + 1]);
|
|
1513
1513
|
return meters(Math.min(TURN_DAMPING_M, previous * .45, next * .45));
|
|
@@ -1755,7 +1755,7 @@ function planMappingStrip(template, options) {
|
|
|
1755
1755
|
waypointTurnDampingDist: meters(0)
|
|
1756
1756
|
} : {
|
|
1757
1757
|
waypointTurnMode: "coordinateTurn",
|
|
1758
|
-
waypointTurnDampingDist: turnDamping(points, index)
|
|
1758
|
+
waypointTurnDampingDist: turnDamping$1(points, index)
|
|
1759
1759
|
},
|
|
1760
1760
|
useStraightLine: true,
|
|
1761
1761
|
isRisky: false,
|
|
@@ -1826,11 +1826,175 @@ function heightModeToExecuteMode(mode) {
|
|
|
1826
1826
|
}
|
|
1827
1827
|
}
|
|
1828
1828
|
//#endregion
|
|
1829
|
+
//#region src/plan/plan-target-detection.ts
|
|
1830
|
+
const EARTH_RADIUS_M = 6378137;
|
|
1831
|
+
const DEG2RAD = Math.PI / 180;
|
|
1832
|
+
const RAD2DEG = 180 / Math.PI;
|
|
1833
|
+
function project({ lng, lat }) {
|
|
1834
|
+
return [EARTH_RADIUS_M * lng * DEG2RAD, EARTH_RADIUS_M * Math.log(Math.tan(Math.PI / 4 + lat * DEG2RAD / 2))];
|
|
1835
|
+
}
|
|
1836
|
+
function unproject([x, y]) {
|
|
1837
|
+
return {
|
|
1838
|
+
lng: x / EARTH_RADIUS_M * RAD2DEG,
|
|
1839
|
+
lat: (2 * Math.atan(Math.exp(y / EARTH_RADIUS_M)) - Math.PI / 2) * RAD2DEG
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function executeHeightMode(folder) {
|
|
1843
|
+
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
1844
|
+
case "EGM96": return "WGS84";
|
|
1845
|
+
case "relativeToStartPoint": return "relativeToStartPoint";
|
|
1846
|
+
case "aboveGroundLevel":
|
|
1847
|
+
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function gimbalAction(pitch) {
|
|
1851
|
+
return {
|
|
1852
|
+
actionId: 0,
|
|
1853
|
+
actionActuatorFunc: "gimbalRotate",
|
|
1854
|
+
actionActuatorFuncParam: {
|
|
1855
|
+
payloadPositionIndex: 0,
|
|
1856
|
+
gimbalHeadingYawBase: "aircraft",
|
|
1857
|
+
gimbalRotateMode: "absoluteAngle",
|
|
1858
|
+
gimbalPitchRotateEnable: true,
|
|
1859
|
+
gimbalPitchRotateAngle: degrees(pitch),
|
|
1860
|
+
gimbalRollRotateEnable: false,
|
|
1861
|
+
gimbalRollRotateAngle: degrees(0),
|
|
1862
|
+
gimbalYawRotateEnable: false,
|
|
1863
|
+
gimbalYawRotateAngle: degrees(0),
|
|
1864
|
+
gimbalRotateTimeEnable: false,
|
|
1865
|
+
gimbalRotateTime: 10
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function startRecordAction(payloadPositionIndex, actionId) {
|
|
1870
|
+
return {
|
|
1871
|
+
actionId,
|
|
1872
|
+
actionActuatorFunc: "startRecord",
|
|
1873
|
+
actionActuatorFuncParam: {
|
|
1874
|
+
payloadPositionIndex,
|
|
1875
|
+
useGlobalPayloadLensIndex: true,
|
|
1876
|
+
payloadLensIndex: ["visable"]
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function pathPoints(template, lineSpacingM, preferredStartPoint) {
|
|
1881
|
+
const placemark = template.Folder.Placemark;
|
|
1882
|
+
const ring = parseCoordinatesString(placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
1883
|
+
if (ring.length < 3) throw new PlannerError("targetdetection requires at least 3 polygon vertices");
|
|
1884
|
+
const centerLat = ring.reduce((sum, point) => sum + point.lat, 0) / ring.length;
|
|
1885
|
+
const scale = 1 / Math.cos(centerLat * DEG2RAD);
|
|
1886
|
+
const theta = Number(placemark.direction) * DEG2RAD;
|
|
1887
|
+
const u = [Math.sin(theta), Math.cos(theta)];
|
|
1888
|
+
const n = [-Math.cos(theta), Math.sin(theta)];
|
|
1889
|
+
const height = Number(placemark.height);
|
|
1890
|
+
const pitch = Math.abs(Number(placemark.gimbalPitchAngle));
|
|
1891
|
+
const lookAheadM = pitch >= 89.999999 ? 0 : height / Math.tan(pitch * DEG2RAD);
|
|
1892
|
+
const scanRing = offsetPolygon(ring.map((point) => {
|
|
1893
|
+
const [x, y] = project(point);
|
|
1894
|
+
return [x - u[0] * lookAheadM * scale, y - u[1] * lookAheadM * scale];
|
|
1895
|
+
}), placemark.margin * scale).map(([x, y]) => [x * u[0] + y * u[1], x * n[0] + y * n[1]]);
|
|
1896
|
+
const cross = scanRing.map((point) => point[1]);
|
|
1897
|
+
const min = Math.min(...cross);
|
|
1898
|
+
const max = Math.max(...cross);
|
|
1899
|
+
const spacing = lineSpacingM * scale;
|
|
1900
|
+
const stripCount = Math.max(1, Math.floor((max - min) / spacing));
|
|
1901
|
+
const center = (min + max) / 2;
|
|
1902
|
+
const segments = [];
|
|
1903
|
+
for (let i = 0; i < stripCount; i++) {
|
|
1904
|
+
const q = center + (i - (stripCount - 1) / 2) * spacing;
|
|
1905
|
+
for (const [a, b] of clipHorizontal(q, scanRing)) segments.push([[u[0] * a + n[0] * q, u[1] * a + n[1] * q], [u[0] * b + n[0] * q, u[1] * b + n[1] * q]]);
|
|
1906
|
+
}
|
|
1907
|
+
let points = segments.flatMap(([a, b], index) => index % 2 === 0 ? [a, b] : [b, a]);
|
|
1908
|
+
if (preferredStartPoint && points.length > 1) {
|
|
1909
|
+
const preferred = project(preferredStartPoint);
|
|
1910
|
+
const distance2 = ([x, y]) => (x - preferred[0]) ** 2 + (y - preferred[1]) ** 2;
|
|
1911
|
+
if (distance2(points.at(-1)) < distance2(points[0])) points = points.reverse();
|
|
1912
|
+
}
|
|
1913
|
+
return points.map(unproject);
|
|
1914
|
+
}
|
|
1915
|
+
function turnDamping(points, index) {
|
|
1916
|
+
if (index === 0 || index === points.length - 1) return meters(0);
|
|
1917
|
+
const groundDistance = (a, b) => {
|
|
1918
|
+
const [ax, ay] = project(a);
|
|
1919
|
+
const [bx, by] = project(b);
|
|
1920
|
+
return Math.hypot(bx - ax, by - ay) * Math.cos((a.lat + b.lat) / 2 * DEG2RAD);
|
|
1921
|
+
};
|
|
1922
|
+
return meters(Math.min(10, groundDistance(points[index - 1], points[index]) / 3, groundDistance(points[index], points[index + 1]) / 3));
|
|
1923
|
+
}
|
|
1924
|
+
function planTargetDetection(template, options) {
|
|
1925
|
+
const spacing = Number(options?.lineSpacing);
|
|
1926
|
+
if (!(spacing > 0 && Number.isFinite(spacing))) throw new PlannerError("targetdetection requires options.lineSpacing");
|
|
1927
|
+
const folder = template.Folder;
|
|
1928
|
+
const pitch = Number(folder.Placemark.gimbalPitchAngle);
|
|
1929
|
+
const direction = Number(folder.Placemark.direction);
|
|
1930
|
+
const points = pathPoints(template, spacing, options?.preferredStartPoint);
|
|
1931
|
+
if (points.length < 2) throw new PlannerError("targetdetection polygon produced no flight path");
|
|
1932
|
+
const lastIndex = points.length - 1;
|
|
1933
|
+
const action = gimbalAction(pitch);
|
|
1934
|
+
const payloadPositionIndex = folder.payloadParam?.payloadPositionIndex ?? 0;
|
|
1935
|
+
const routeActions = [action];
|
|
1936
|
+
if (folder.Placemark.recordEnable) routeActions.push(startRecordAction(payloadPositionIndex, routeActions.length));
|
|
1937
|
+
if (folder.Placemark.action) routeActions.push({
|
|
1938
|
+
...folder.Placemark.action,
|
|
1939
|
+
actionId: routeActions.length
|
|
1940
|
+
});
|
|
1941
|
+
const placemarks = points.map((point, index) => ({
|
|
1942
|
+
Point: { coordinates: point },
|
|
1943
|
+
index,
|
|
1944
|
+
executeHeight: folder.waylineCoordinateSysParam.heightMode === "aboveGroundLevel" ? options?.resolveExecuteHeight?.({
|
|
1945
|
+
...point,
|
|
1946
|
+
index,
|
|
1947
|
+
waylineId: 0
|
|
1948
|
+
}) ?? folder.Placemark.height : folder.Placemark.height,
|
|
1949
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
1950
|
+
waypointHeadingParam: index === lastIndex ? {
|
|
1951
|
+
waypointHeadingMode: "followWayline",
|
|
1952
|
+
waypointHeadingPathMode: "followBadArc"
|
|
1953
|
+
} : {
|
|
1954
|
+
waypointHeadingMode: "fixed",
|
|
1955
|
+
waypointHeadingAngle: degrees(direction),
|
|
1956
|
+
waypointHeadingAngleEnable: true,
|
|
1957
|
+
waypointHeadingPathMode: "followBadArc",
|
|
1958
|
+
waypointHeadingPoiIndex: 0
|
|
1959
|
+
},
|
|
1960
|
+
waypointTurnParam: index === 0 || index === lastIndex ? {
|
|
1961
|
+
waypointTurnMode: "toPointAndStopWithDiscontinuityCurvature",
|
|
1962
|
+
waypointTurnDampingDist: meters(0)
|
|
1963
|
+
} : {
|
|
1964
|
+
waypointTurnMode: "coordinateTurn",
|
|
1965
|
+
waypointTurnDampingDist: turnDamping(points, index)
|
|
1966
|
+
},
|
|
1967
|
+
useStraightLine: true,
|
|
1968
|
+
isRisky: false,
|
|
1969
|
+
waypointWorkType: 0,
|
|
1970
|
+
...index === 0 ? { actionGroup: [{
|
|
1971
|
+
actionGroupId: 0,
|
|
1972
|
+
actionGroupStartIndex: 0,
|
|
1973
|
+
actionGroupEndIndex: lastIndex,
|
|
1974
|
+
actionGroupMode: "sequence",
|
|
1975
|
+
actionTrigger: { actionTriggerType: "betweenAdjacentPointsIncludeFirstPoint" },
|
|
1976
|
+
action: routeActions
|
|
1977
|
+
}] } : {}
|
|
1978
|
+
}));
|
|
1979
|
+
return {
|
|
1980
|
+
missionConfig: template.missionConfig,
|
|
1981
|
+
Folder: [{
|
|
1982
|
+
templateId: folder.templateId,
|
|
1983
|
+
waylineId: 0,
|
|
1984
|
+
executeHeightMode: executeHeightMode(folder),
|
|
1985
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
1986
|
+
startActionGroup: { action: folder.Placemark.recordEnable ? [gimbalAction(pitch), startRecordAction(payloadPositionIndex, 1)] : [gimbalAction(pitch)] },
|
|
1987
|
+
Placemark: placemarks
|
|
1988
|
+
}]
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
//#endregion
|
|
1829
1992
|
//#region src/plan/index.ts
|
|
1830
1993
|
const isWaypoint = (t) => t.Folder.templateType === "waypoint";
|
|
1831
1994
|
const isMapping2d = (t) => t.Folder.templateType === "mapping2d";
|
|
1832
1995
|
const isMapping3d = (t) => t.Folder.templateType === "mapping3d";
|
|
1833
1996
|
const isMappingStrip = (t) => t.Folder.templateType === "mappingStrip";
|
|
1997
|
+
const isTargetDetection = (t) => t.Folder.templateType === "targetdetection";
|
|
1834
1998
|
/**
|
|
1835
1999
|
* Compile a high-level `Template` into executable `Waylines`.
|
|
1836
2000
|
* Dispatch is by `template.Folder.templateType` per the DJI WPML spec.
|
|
@@ -1842,6 +2006,7 @@ function plan(template, options) {
|
|
|
1842
2006
|
if (isMapping2d(template)) return planMapping2d(template, options);
|
|
1843
2007
|
if (isMapping3d(template)) return planMapping3d(template, options);
|
|
1844
2008
|
if (isMappingStrip(template)) return planMappingStrip(template, options);
|
|
2009
|
+
if (isTargetDetection(template)) return planTargetDetection(template, options);
|
|
1845
2010
|
throw new PlannerError(`Unknown templateType: ${template.Folder.templateType}`);
|
|
1846
2011
|
}
|
|
1847
2012
|
//#endregion
|
|
@@ -1904,6 +2069,9 @@ function validateTemplate(template) {
|
|
|
1904
2069
|
case "mappingStrip":
|
|
1905
2070
|
issues.push(...validateMappingStripFolder(template, path));
|
|
1906
2071
|
break;
|
|
2072
|
+
case "targetdetection":
|
|
2073
|
+
issues.push(...validateTargetDetectionFolder(template, path));
|
|
2074
|
+
break;
|
|
1907
2075
|
}
|
|
1908
2076
|
return issues;
|
|
1909
2077
|
}
|
|
@@ -1922,6 +2090,9 @@ function validateMapping2dFolder(t, path) {
|
|
|
1922
2090
|
function validateMapping3dFolder(t, path) {
|
|
1923
2091
|
return validatePolygonRing(t.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates, `${path}.Placemark.Polygon`);
|
|
1924
2092
|
}
|
|
2093
|
+
function validateTargetDetectionFolder(t, path) {
|
|
2094
|
+
return validatePolygonRing(t.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates, `${path}.Placemark.Polygon`);
|
|
2095
|
+
}
|
|
1925
2096
|
function validateMappingStripFolder(t, path) {
|
|
1926
2097
|
const placemark = t.Folder.Placemark;
|
|
1927
2098
|
const tuples = placemark.LineString.coordinates.trim().split(/\s+/).filter(Boolean);
|
package/package.json
CHANGED
package/src/plan/geometry.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function haversineMeters(a: LngLat, b: LngLat): number {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** Clip a horizontal scan line `y = const` against a polygon; returns `[x_enter, x_exit]` pairs. */
|
|
73
|
-
function clipHorizontal(y: number, polygon: readonly EnuPoint[]): [number, number][] {
|
|
73
|
+
export function clipHorizontal(y: number, polygon: readonly EnuPoint[]): [number, number][] {
|
|
74
74
|
const intersections: number[] = [];
|
|
75
75
|
const n = polygon.length;
|
|
76
76
|
for (let i = 0; i < n; i++) {
|
package/src/plan/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
Mapping2dTemplate,
|
|
5
5
|
Mapping3dTemplate,
|
|
6
6
|
MappingStripTemplate,
|
|
7
|
+
TargetDetectionTemplate,
|
|
7
8
|
Template,
|
|
8
9
|
WaypointTemplate,
|
|
9
10
|
} from "../types/template.js";
|
|
@@ -12,6 +13,8 @@ import { planMapping2d } from "./plan-mapping2d.js";
|
|
|
12
13
|
import { planMapping3d } from "./plan-mapping3d.js";
|
|
13
14
|
import { planMappingStrip } from "./plan-mapping-strip.js";
|
|
14
15
|
import { planWaypoint } from "./plan-waypoint.js";
|
|
16
|
+
import { planTargetDetection } from "./plan-target-detection.js";
|
|
17
|
+
import type { LngLat } from "../types/branded.js";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Options for `plan()`.
|
|
@@ -34,6 +37,8 @@ export interface PlanOptions {
|
|
|
34
37
|
stripDirection?: "parallel" | "perpendicular";
|
|
35
38
|
/** Reverse mappingStrip region execution without changing region identities. */
|
|
36
39
|
regionOrderReversed?: boolean;
|
|
40
|
+
/** Preferred entry location for path variants that can be reversed as a whole. */
|
|
41
|
+
preferredStartPoint?: LngLat;
|
|
37
42
|
resolveExecuteHeight?: (point: {
|
|
38
43
|
lng: number;
|
|
39
44
|
lat: number;
|
|
@@ -47,6 +52,8 @@ const isMapping2d = (t: Template): t is Mapping2dTemplate => t.Folder.templateTy
|
|
|
47
52
|
const isMapping3d = (t: Template): t is Mapping3dTemplate => t.Folder.templateType === "mapping3d";
|
|
48
53
|
const isMappingStrip = (t: Template): t is MappingStripTemplate =>
|
|
49
54
|
t.Folder.templateType === "mappingStrip";
|
|
55
|
+
const isTargetDetection = (t: Template): t is TargetDetectionTemplate =>
|
|
56
|
+
t.Folder.templateType === "targetdetection";
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* Compile a high-level `Template` into executable `Waylines`.
|
|
@@ -59,5 +66,6 @@ export function plan(template: Template, options?: PlanOptions): Waylines {
|
|
|
59
66
|
if (isMapping2d(template)) return planMapping2d(template, options);
|
|
60
67
|
if (isMapping3d(template)) return planMapping3d(template, options);
|
|
61
68
|
if (isMappingStrip(template)) return planMappingStrip(template, options);
|
|
69
|
+
if (isTargetDetection(template)) return planTargetDetection(template, options);
|
|
62
70
|
throw new PlannerError(`Unknown templateType: ${(template as Template).Folder.templateType}`);
|
|
63
71
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { degrees, meters, metersPerSecond } from "../types/branded.js";
|
|
3
|
+
import type { TargetDetectionTemplate } from "../types/template.js";
|
|
4
|
+
import { plan } from "./index.js";
|
|
5
|
+
|
|
6
|
+
function template(overrides: { pitch?: number; direction?: number } = {}): TargetDetectionTemplate {
|
|
7
|
+
const pitch = overrides.pitch ?? -90;
|
|
8
|
+
const direction = overrides.direction ?? 0;
|
|
9
|
+
return {
|
|
10
|
+
missionConfig: {
|
|
11
|
+
flyToWaylineMode: "safely",
|
|
12
|
+
finishAction: "goHome",
|
|
13
|
+
exitOnRCLost: "goContinue",
|
|
14
|
+
takeOffSecurityHeight: meters(20),
|
|
15
|
+
globalTransitionalSpeed: metersPerSecond(15),
|
|
16
|
+
globalRTHHeight: meters(100),
|
|
17
|
+
droneInfo: { droneEnumValue: 100, droneSubEnumValue: 1 },
|
|
18
|
+
payloadInfo: [{ payloadEnumValue: 99, payloadSubEnumValue: 2, payloadPositionIndex: 0 }],
|
|
19
|
+
},
|
|
20
|
+
Folder: {
|
|
21
|
+
templateType: "targetdetection",
|
|
22
|
+
templateId: 0,
|
|
23
|
+
waylineCoordinateSysParam: {
|
|
24
|
+
coordinateMode: "WGS84",
|
|
25
|
+
heightMode: "relativeToStartPoint",
|
|
26
|
+
globalShootHeight: meters(60),
|
|
27
|
+
surfaceFollowModeEnable: false,
|
|
28
|
+
},
|
|
29
|
+
autoFlightSpeed: metersPerSecond(10),
|
|
30
|
+
payloadParam: { payloadPositionIndex: 0, imageFormat: ["ir"] },
|
|
31
|
+
Placemark: {
|
|
32
|
+
direction: degrees(direction),
|
|
33
|
+
margin: 0,
|
|
34
|
+
gimbalPitchMode: "fixed",
|
|
35
|
+
overlap: { orthoCameraOverlapH: 1, orthoCameraOverlapW: 20 },
|
|
36
|
+
Polygon: {
|
|
37
|
+
outerBoundaryIs: {
|
|
38
|
+
LinearRing: {
|
|
39
|
+
coordinates: "0,0,0 0.001,0,0 0.001,0.002,0 0,0.002,0",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
ellipsoidHeight: meters(60),
|
|
44
|
+
height: meters(60),
|
|
45
|
+
mappingHeadingParam: {
|
|
46
|
+
mappingHeadingMode: "fixed",
|
|
47
|
+
mappingHeadingAngle: degrees(direction),
|
|
48
|
+
},
|
|
49
|
+
gimbalPitchAngle: degrees(pitch),
|
|
50
|
+
recordEnable: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("plan(targetdetection)", () => {
|
|
57
|
+
it("matches FlightHub recording and target-detection actions without stopRecord", () => {
|
|
58
|
+
const input = template();
|
|
59
|
+
Object.assign(input.Folder.Placemark, {
|
|
60
|
+
recordEnable: true,
|
|
61
|
+
targetDetectionActionEnable: true,
|
|
62
|
+
action: {
|
|
63
|
+
actionId: 0,
|
|
64
|
+
actionActuatorFunc: "targetDetection",
|
|
65
|
+
actionActuatorFuncParam: {
|
|
66
|
+
focalLength: 70,
|
|
67
|
+
payloadPositionIndex: 0,
|
|
68
|
+
useGlobalPayloadLensIndex: false,
|
|
69
|
+
payloadLensIndex: ["visable"],
|
|
70
|
+
actionUUID: "test-target-detection",
|
|
71
|
+
customTitle: "巡逻",
|
|
72
|
+
customText: "检测到异常目标",
|
|
73
|
+
modelIndex: 0,
|
|
74
|
+
modelScore: 55,
|
|
75
|
+
alarmInterval: 2,
|
|
76
|
+
useThirdModel: false,
|
|
77
|
+
algorithmType: "visable_ai",
|
|
78
|
+
targetParam: {
|
|
79
|
+
maxThreshold: 10,
|
|
80
|
+
minThreshold: 10,
|
|
81
|
+
useMaxThreshold: false,
|
|
82
|
+
useMinThreshold: true,
|
|
83
|
+
targetType: "person",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const folder = plan(input, { lineSpacing: meters(25) }).Folder[0]!;
|
|
90
|
+
const startFuncs = folder.startActionGroup!.action.map((a) => a.actionActuatorFunc);
|
|
91
|
+
const routeFuncs = folder.Placemark[0]!.actionGroup![0]!.action.map(
|
|
92
|
+
(a) => a.actionActuatorFunc,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(startFuncs).toEqual(["gimbalRotate", "startRecord"]);
|
|
96
|
+
expect(routeFuncs).toEqual(["gimbalRotate", "startRecord", "targetDetection"]);
|
|
97
|
+
expect(JSON.stringify(folder)).not.toContain("stopRecord");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("centers floor(width / spacing) scan lines", () => {
|
|
101
|
+
const waylines = plan(template(), { lineSpacing: meters(25) });
|
|
102
|
+
|
|
103
|
+
expect(waylines.Folder).toHaveLength(1);
|
|
104
|
+
expect(waylines.Folder[0]!.Placemark).toHaveLength(8);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("shifts the flight path backward by height / tan(abs(pitch))", () => {
|
|
108
|
+
const placemarks = plan(template({ pitch: -45 }), {
|
|
109
|
+
lineSpacing: meters(25),
|
|
110
|
+
}).Folder[0]!.Placemark;
|
|
111
|
+
const centerLat =
|
|
112
|
+
placemarks.reduce((sum, point) => sum + point.Point.coordinates.lat, 0) / placemarks.length;
|
|
113
|
+
|
|
114
|
+
expect(centerLat).toBeCloseTo(0.000461, 5);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("retains every clipped segment of a concave polygon", () => {
|
|
118
|
+
const concave = template({ direction: 90 });
|
|
119
|
+
concave.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates =
|
|
120
|
+
"0,0,0 0.003,0,0 0.003,0.003,0 0.002,0.003,0 0.002,0.001,0 0.001,0.001,0 0.001,0.003,0 0,0.003,0";
|
|
121
|
+
|
|
122
|
+
const placemarks = plan(concave, { lineSpacing: meters(100) }).Folder[0]!.Placemark;
|
|
123
|
+
|
|
124
|
+
expect(placemarks).toHaveLength(10);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("keeps fixed heading and pitch with one-third turn damping", () => {
|
|
128
|
+
const placemarks = plan(template({ pitch: -45, direction: 0 }), {
|
|
129
|
+
lineSpacing: meters(25),
|
|
130
|
+
}).Folder[0]!.Placemark;
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
placemarks.slice(0, -1).every((p) => p.waypointHeadingParam.waypointHeadingMode === "fixed"),
|
|
134
|
+
).toBe(true);
|
|
135
|
+
expect(placemarks[0]!.actionGroup?.[0]!.action[0]).toMatchObject({
|
|
136
|
+
actionActuatorFunc: "gimbalRotate",
|
|
137
|
+
actionActuatorFuncParam: { gimbalPitchRotateAngle: -45 },
|
|
138
|
+
});
|
|
139
|
+
expect(Number(placemarks[1]!.waypointTurnParam.waypointTurnDampingDist)).toBeCloseTo(25 / 3, 5);
|
|
140
|
+
expect(Number(placemarks[0]!.waypointTurnParam.waypointTurnDampingDist)).toBe(0);
|
|
141
|
+
expect(Number(placemarks.at(-1)!.waypointTurnParam.waypointTurnDampingDist)).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { degrees, meters, type LngLat, type Meters } from "../types/branded.js";
|
|
2
|
+
import type { Action } from "../types/action.js";
|
|
3
|
+
import { PlannerError } from "../types/errors.js";
|
|
4
|
+
import type { ExecuteHeightMode } from "../types/enums.js";
|
|
5
|
+
import type { WaylinesPlacemark } from "../types/placemark.js";
|
|
6
|
+
import type { TargetDetectionFolder, TargetDetectionTemplate } from "../types/template.js";
|
|
7
|
+
import type { Waylines } from "../types/waylines.js";
|
|
8
|
+
import { clipHorizontal, offsetPolygon, parseCoordinatesString } from "./geometry.js";
|
|
9
|
+
import type { PlanOptions } from "./index.js";
|
|
10
|
+
|
|
11
|
+
const EARTH_RADIUS_M = 6_378_137;
|
|
12
|
+
const DEG2RAD = Math.PI / 180;
|
|
13
|
+
const RAD2DEG = 180 / Math.PI;
|
|
14
|
+
|
|
15
|
+
type Point = readonly [number, number];
|
|
16
|
+
|
|
17
|
+
function project({ lng, lat }: LngLat): Point {
|
|
18
|
+
return [
|
|
19
|
+
EARTH_RADIUS_M * lng * DEG2RAD,
|
|
20
|
+
EARTH_RADIUS_M * Math.log(Math.tan(Math.PI / 4 + (lat * DEG2RAD) / 2)),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unproject([x, y]: Point): LngLat {
|
|
25
|
+
return {
|
|
26
|
+
lng: (x / EARTH_RADIUS_M) * RAD2DEG,
|
|
27
|
+
lat: (2 * Math.atan(Math.exp(y / EARTH_RADIUS_M)) - Math.PI / 2) * RAD2DEG,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function executeHeightMode(folder: TargetDetectionFolder): ExecuteHeightMode {
|
|
32
|
+
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
33
|
+
case "EGM96":
|
|
34
|
+
return "WGS84";
|
|
35
|
+
case "relativeToStartPoint":
|
|
36
|
+
return "relativeToStartPoint";
|
|
37
|
+
case "aboveGroundLevel":
|
|
38
|
+
case "realTimeFollowSurface":
|
|
39
|
+
return "realTimeFollowSurface";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function gimbalAction(pitch: number) {
|
|
44
|
+
return {
|
|
45
|
+
actionId: 0,
|
|
46
|
+
actionActuatorFunc: "gimbalRotate" as const,
|
|
47
|
+
actionActuatorFuncParam: {
|
|
48
|
+
payloadPositionIndex: 0,
|
|
49
|
+
gimbalHeadingYawBase: "aircraft" as const,
|
|
50
|
+
gimbalRotateMode: "absoluteAngle" as const,
|
|
51
|
+
gimbalPitchRotateEnable: true,
|
|
52
|
+
gimbalPitchRotateAngle: degrees(pitch),
|
|
53
|
+
gimbalRollRotateEnable: false,
|
|
54
|
+
gimbalRollRotateAngle: degrees(0),
|
|
55
|
+
gimbalYawRotateEnable: false,
|
|
56
|
+
gimbalYawRotateAngle: degrees(0),
|
|
57
|
+
gimbalRotateTimeEnable: false,
|
|
58
|
+
gimbalRotateTime: 10,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function startRecordAction(payloadPositionIndex: number, actionId: number) {
|
|
64
|
+
return {
|
|
65
|
+
actionId,
|
|
66
|
+
actionActuatorFunc: "startRecord" as const,
|
|
67
|
+
actionActuatorFuncParam: {
|
|
68
|
+
payloadPositionIndex,
|
|
69
|
+
useGlobalPayloadLensIndex: true,
|
|
70
|
+
payloadLensIndex: ["visable" as const],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pathPoints(
|
|
76
|
+
template: TargetDetectionTemplate,
|
|
77
|
+
lineSpacingM: number,
|
|
78
|
+
preferredStartPoint?: LngLat,
|
|
79
|
+
): LngLat[] {
|
|
80
|
+
const placemark = template.Folder.Placemark;
|
|
81
|
+
const ring = parseCoordinatesString(placemark.Polygon.outerBoundaryIs.LinearRing.coordinates);
|
|
82
|
+
if (ring.length < 3)
|
|
83
|
+
throw new PlannerError("targetdetection requires at least 3 polygon vertices");
|
|
84
|
+
|
|
85
|
+
const centerLat = ring.reduce((sum, point) => sum + point.lat, 0) / ring.length;
|
|
86
|
+
const scale = 1 / Math.cos(centerLat * DEG2RAD);
|
|
87
|
+
const theta = Number(placemark.direction) * DEG2RAD;
|
|
88
|
+
const u: Point = [Math.sin(theta), Math.cos(theta)];
|
|
89
|
+
const n: Point = [-Math.cos(theta), Math.sin(theta)];
|
|
90
|
+
const height = Number(placemark.height);
|
|
91
|
+
const pitch = Math.abs(Number(placemark.gimbalPitchAngle));
|
|
92
|
+
const lookAheadM = pitch >= 89.999999 ? 0 : height / Math.tan(pitch * DEG2RAD);
|
|
93
|
+
|
|
94
|
+
const translated = ring.map((point) => {
|
|
95
|
+
const [x, y] = project(point);
|
|
96
|
+
return [x - u[0] * lookAheadM * scale, y - u[1] * lookAheadM * scale] as Point;
|
|
97
|
+
});
|
|
98
|
+
const flightRing = offsetPolygon(translated, placemark.margin * scale);
|
|
99
|
+
const scanRing = flightRing.map(([x, y]) => [x * u[0] + y * u[1], x * n[0] + y * n[1]] as Point);
|
|
100
|
+
const cross = scanRing.map((point) => point[1]);
|
|
101
|
+
const min = Math.min(...cross);
|
|
102
|
+
const max = Math.max(...cross);
|
|
103
|
+
const spacing = lineSpacingM * scale;
|
|
104
|
+
const stripCount = Math.max(1, Math.floor((max - min) / spacing));
|
|
105
|
+
const center = (min + max) / 2;
|
|
106
|
+
|
|
107
|
+
const segments: [Point, Point][] = [];
|
|
108
|
+
for (let i = 0; i < stripCount; i++) {
|
|
109
|
+
const q = center + (i - (stripCount - 1) / 2) * spacing;
|
|
110
|
+
for (const [a, b] of clipHorizontal(q, scanRing)) {
|
|
111
|
+
segments.push([
|
|
112
|
+
[u[0] * a + n[0] * q, u[1] * a + n[1] * q],
|
|
113
|
+
[u[0] * b + n[0] * q, u[1] * b + n[1] * q],
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let points = segments.flatMap(([a, b], index) => (index % 2 === 0 ? [a, b] : [b, a]));
|
|
119
|
+
if (preferredStartPoint && points.length > 1) {
|
|
120
|
+
const preferred = project(preferredStartPoint);
|
|
121
|
+
const distance2 = ([x, y]: Point) => (x - preferred[0]) ** 2 + (y - preferred[1]) ** 2;
|
|
122
|
+
if (distance2(points.at(-1)!) < distance2(points[0]!)) points = points.reverse();
|
|
123
|
+
}
|
|
124
|
+
return points.map(unproject);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function turnDamping(points: readonly LngLat[], index: number): Meters {
|
|
128
|
+
if (index === 0 || index === points.length - 1) return meters(0);
|
|
129
|
+
const groundDistance = (a: LngLat, b: LngLat) => {
|
|
130
|
+
const [ax, ay] = project(a);
|
|
131
|
+
const [bx, by] = project(b);
|
|
132
|
+
return Math.hypot(bx - ax, by - ay) * Math.cos(((a.lat + b.lat) / 2) * DEG2RAD);
|
|
133
|
+
};
|
|
134
|
+
return meters(
|
|
135
|
+
Math.min(
|
|
136
|
+
10,
|
|
137
|
+
groundDistance(points[index - 1]!, points[index]!) / 3,
|
|
138
|
+
groundDistance(points[index]!, points[index + 1]!) / 3,
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function planTargetDetection(
|
|
144
|
+
template: TargetDetectionTemplate,
|
|
145
|
+
options?: PlanOptions,
|
|
146
|
+
): Waylines {
|
|
147
|
+
const spacing = Number(options?.lineSpacing);
|
|
148
|
+
if (!(spacing > 0 && Number.isFinite(spacing))) {
|
|
149
|
+
throw new PlannerError("targetdetection requires options.lineSpacing");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const folder = template.Folder;
|
|
153
|
+
const pitch = Number(folder.Placemark.gimbalPitchAngle);
|
|
154
|
+
const direction = Number(folder.Placemark.direction);
|
|
155
|
+
const points = pathPoints(template, spacing, options?.preferredStartPoint);
|
|
156
|
+
if (points.length < 2) throw new PlannerError("targetdetection polygon produced no flight path");
|
|
157
|
+
const lastIndex = points.length - 1;
|
|
158
|
+
const action = gimbalAction(pitch);
|
|
159
|
+
const payloadPositionIndex = folder.payloadParam?.payloadPositionIndex ?? 0;
|
|
160
|
+
const routeActions: Action[] = [action];
|
|
161
|
+
if (folder.Placemark.recordEnable) {
|
|
162
|
+
routeActions.push(startRecordAction(payloadPositionIndex, routeActions.length));
|
|
163
|
+
}
|
|
164
|
+
if (folder.Placemark.action) {
|
|
165
|
+
routeActions.push({ ...folder.Placemark.action, actionId: routeActions.length });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const placemarks: WaylinesPlacemark[] = points.map((point, index) => ({
|
|
169
|
+
Point: { coordinates: point },
|
|
170
|
+
index,
|
|
171
|
+
executeHeight:
|
|
172
|
+
folder.waylineCoordinateSysParam.heightMode === "aboveGroundLevel"
|
|
173
|
+
? (options?.resolveExecuteHeight?.({ ...point, index, waylineId: 0 }) ??
|
|
174
|
+
folder.Placemark.height)
|
|
175
|
+
: folder.Placemark.height,
|
|
176
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
177
|
+
waypointHeadingParam:
|
|
178
|
+
index === lastIndex
|
|
179
|
+
? { waypointHeadingMode: "followWayline", waypointHeadingPathMode: "followBadArc" }
|
|
180
|
+
: {
|
|
181
|
+
waypointHeadingMode: "fixed",
|
|
182
|
+
waypointHeadingAngle: degrees(direction),
|
|
183
|
+
waypointHeadingAngleEnable: true,
|
|
184
|
+
waypointHeadingPathMode: "followBadArc",
|
|
185
|
+
waypointHeadingPoiIndex: 0,
|
|
186
|
+
},
|
|
187
|
+
waypointTurnParam:
|
|
188
|
+
index === 0 || index === lastIndex
|
|
189
|
+
? {
|
|
190
|
+
waypointTurnMode: "toPointAndStopWithDiscontinuityCurvature",
|
|
191
|
+
waypointTurnDampingDist: meters(0),
|
|
192
|
+
}
|
|
193
|
+
: {
|
|
194
|
+
waypointTurnMode: "coordinateTurn",
|
|
195
|
+
waypointTurnDampingDist: turnDamping(points, index),
|
|
196
|
+
},
|
|
197
|
+
useStraightLine: true,
|
|
198
|
+
isRisky: false,
|
|
199
|
+
waypointWorkType: 0,
|
|
200
|
+
...(index === 0
|
|
201
|
+
? {
|
|
202
|
+
actionGroup: [
|
|
203
|
+
{
|
|
204
|
+
actionGroupId: 0,
|
|
205
|
+
actionGroupStartIndex: 0,
|
|
206
|
+
actionGroupEndIndex: lastIndex,
|
|
207
|
+
actionGroupMode: "sequence",
|
|
208
|
+
actionTrigger: { actionTriggerType: "betweenAdjacentPointsIncludeFirstPoint" },
|
|
209
|
+
action: routeActions,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
missionConfig: template.missionConfig,
|
|
218
|
+
Folder: [
|
|
219
|
+
{
|
|
220
|
+
templateId: folder.templateId,
|
|
221
|
+
waylineId: 0,
|
|
222
|
+
executeHeightMode: executeHeightMode(folder),
|
|
223
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
224
|
+
startActionGroup: {
|
|
225
|
+
action: folder.Placemark.recordEnable
|
|
226
|
+
? [gimbalAction(pitch), startRecordAction(payloadPositionIndex, 1)]
|
|
227
|
+
: [gimbalAction(pitch)],
|
|
228
|
+
},
|
|
229
|
+
Placemark: placemarks,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
}
|
package/src/types/action.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface ActionTrigger {
|
|
|
34
34
|
|
|
35
35
|
/** Discriminated by `actionActuatorFunc`. */
|
|
36
36
|
export type Action =
|
|
37
|
+
| TargetDetectionAction
|
|
37
38
|
| TakePhotoAction
|
|
38
39
|
| StartRecordAction
|
|
39
40
|
| StopRecordAction
|
|
@@ -64,6 +65,31 @@ interface ActionBase<F extends string, P> {
|
|
|
64
65
|
actionActuatorFuncParam: P;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
export interface TargetDetectionParam {
|
|
69
|
+
focalLength?: number;
|
|
70
|
+
payloadPositionIndex: number;
|
|
71
|
+
useGlobalPayloadLensIndex: boolean;
|
|
72
|
+
payloadLensIndex: LensType[];
|
|
73
|
+
actionUUID: string;
|
|
74
|
+
irGainMode?: string;
|
|
75
|
+
irColorPalette?: string;
|
|
76
|
+
customTitle: string;
|
|
77
|
+
customText: string;
|
|
78
|
+
modelIndex: number;
|
|
79
|
+
modelScore: number;
|
|
80
|
+
alarmInterval: number;
|
|
81
|
+
useThirdModel: boolean;
|
|
82
|
+
algorithmType: string;
|
|
83
|
+
targetParam: {
|
|
84
|
+
maxThreshold: number;
|
|
85
|
+
minThreshold: number;
|
|
86
|
+
useMaxThreshold: boolean;
|
|
87
|
+
useMinThreshold: boolean;
|
|
88
|
+
targetType: string;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export type TargetDetectionAction = ActionBase<"targetDetection", TargetDetectionParam>;
|
|
92
|
+
|
|
67
93
|
// ─── Action params (one type per actionActuatorFunc) ─────────────────────────
|
|
68
94
|
|
|
69
95
|
export interface TakePhotoParam {
|
|
@@ -15,6 +15,8 @@ export interface PayloadInfo {
|
|
|
15
15
|
*/
|
|
16
16
|
payloadSubEnumValue?: number;
|
|
17
17
|
payloadPositionIndex: number;
|
|
18
|
+
/** FlightHub 2 patrol-camera selection, in 35mm-equivalent millimeters. */
|
|
19
|
+
equivalentFocalLength?: number;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** Aircraft `{ droneEnumValue, droneSubEnumValue }` tuples. */
|
package/src/types/enums.ts
CHANGED
|
@@ -8,7 +8,12 @@ export type ExitOnRCLost = "goContinue" | "executeLostAction";
|
|
|
8
8
|
|
|
9
9
|
export type ExecuteRCLostAction = "goBack" | "landing" | "hover";
|
|
10
10
|
|
|
11
|
-
export type TemplateType =
|
|
11
|
+
export type TemplateType =
|
|
12
|
+
| "waypoint"
|
|
13
|
+
| "mapping2d"
|
|
14
|
+
| "mapping3d"
|
|
15
|
+
| "mappingStrip"
|
|
16
|
+
| "targetdetection";
|
|
12
17
|
|
|
13
18
|
export type CoordinateMode = "WGS84";
|
|
14
19
|
|
|
@@ -63,10 +68,12 @@ export type ActionGroupMode = "sequence";
|
|
|
63
68
|
export type ActionTriggerType =
|
|
64
69
|
| "reachPoint"
|
|
65
70
|
| "betweenAdjacentPoints"
|
|
71
|
+
| "betweenAdjacentPointsIncludeFirstPoint"
|
|
66
72
|
| "multipleTiming"
|
|
67
73
|
| "multipleDistance";
|
|
68
74
|
|
|
69
75
|
export type ActionActuatorFunc =
|
|
76
|
+
| "targetDetection"
|
|
70
77
|
| "takePhoto"
|
|
71
78
|
| "startRecord"
|
|
72
79
|
| "stopRecord"
|
|
@@ -31,6 +31,8 @@ export interface MissionConfig {
|
|
|
31
31
|
globalRTHHeight: Meters;
|
|
32
32
|
droneInfo: DroneInfo;
|
|
33
33
|
payloadInfo: PayloadInfo[];
|
|
34
|
+
/** FlightHub 2 custom restricted-area avoidance switch. */
|
|
35
|
+
waylineAvoidLimitAreaMode?: boolean;
|
|
34
36
|
/** Only M3D/M3TD, M4D/M4TD, M4E/M4T. */
|
|
35
37
|
autoRerouteInfo?: AutoRerouteInfo;
|
|
36
38
|
}
|
package/src/types/placemark.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Degrees, LngLat, Meters, MetersPerSecond } from "./branded.js";
|
|
2
|
-
import type { ActionGroup } from "./action.js";
|
|
2
|
+
import type { ActionGroup, TargetDetectionAction } from "./action.js";
|
|
3
3
|
import type { ExecuteHeightMode, ShootType } from "./enums.js";
|
|
4
4
|
import type {
|
|
5
5
|
MappingHeadingParam,
|
|
@@ -115,6 +115,23 @@ export interface Mapping3dPlacemark {
|
|
|
115
115
|
height: Meters;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** `Placemark` inside a FlightHub 2 patrol-area `targetdetection` Folder. */
|
|
119
|
+
export interface TargetDetectionPlacemark {
|
|
120
|
+
direction: Degrees;
|
|
121
|
+
margin: number;
|
|
122
|
+
gimbalPitchMode: "fixed";
|
|
123
|
+
overlap: Overlap;
|
|
124
|
+
Polygon: TemplatePolygon;
|
|
125
|
+
ellipsoidHeight: Meters;
|
|
126
|
+
height: Meters;
|
|
127
|
+
mappingHeadingParam: MappingHeadingParam;
|
|
128
|
+
gimbalPitchAngle: Degrees;
|
|
129
|
+
recordEnable: boolean;
|
|
130
|
+
/** FlightHub 2 always emits this action for patrol-area routes. */
|
|
131
|
+
targetDetectionActionEnable?: boolean;
|
|
132
|
+
action?: TargetDetectionAction;
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
/** `Placemark` inside a `mappingStrip` Folder — uses a LineString; params live here. */
|
|
119
136
|
export interface MappingStripPlacemark {
|
|
120
137
|
caliFlightEnable: boolean;
|
package/src/types/template.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
Mapping2dPlacemark,
|
|
7
7
|
Mapping3dPlacemark,
|
|
8
8
|
MappingStripPlacemark,
|
|
9
|
+
TargetDetectionPlacemark,
|
|
9
10
|
TemplateWaypointPlacemark,
|
|
10
11
|
} from "./placemark.js";
|
|
11
12
|
|
|
@@ -17,7 +18,8 @@ export type Template =
|
|
|
17
18
|
| WaypointTemplate
|
|
18
19
|
| Mapping2dTemplate
|
|
19
20
|
| Mapping3dTemplate
|
|
20
|
-
| MappingStripTemplate
|
|
21
|
+
| MappingStripTemplate
|
|
22
|
+
| TargetDetectionTemplate;
|
|
21
23
|
|
|
22
24
|
interface TemplateDocumentBase {
|
|
23
25
|
/** Unix milliseconds. */
|
|
@@ -93,6 +95,21 @@ export interface Mapping3dFolder {
|
|
|
93
95
|
Placemark: Mapping3dPlacemark;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// ─── targetdetection (patrol area) ──────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export interface TargetDetectionTemplate extends TemplateDocumentBase {
|
|
101
|
+
Folder: TargetDetectionFolder;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TargetDetectionFolder {
|
|
105
|
+
templateType: "targetdetection";
|
|
106
|
+
templateId: number;
|
|
107
|
+
waylineCoordinateSysParam: WaylineCoordinateSysParam;
|
|
108
|
+
autoFlightSpeed: MetersPerSecond;
|
|
109
|
+
payloadParam?: PayloadParam;
|
|
110
|
+
Placemark: TargetDetectionPlacemark;
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
// ─── mappingStrip ────────────────────────────────────────────────────────────
|
|
97
114
|
|
|
98
115
|
export interface MappingStripTemplate extends TemplateDocumentBase {
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
Mapping2dTemplate,
|
|
4
4
|
Mapping3dTemplate,
|
|
5
5
|
MappingStripTemplate,
|
|
6
|
+
TargetDetectionTemplate,
|
|
6
7
|
Template,
|
|
7
8
|
WaypointTemplate,
|
|
8
9
|
} from "../types/template.js";
|
|
@@ -41,6 +42,9 @@ export function validateTemplate(template: Template): Issue[] {
|
|
|
41
42
|
case "mappingStrip":
|
|
42
43
|
issues.push(...validateMappingStripFolder(template as MappingStripTemplate, path));
|
|
43
44
|
break;
|
|
45
|
+
case "targetdetection":
|
|
46
|
+
issues.push(...validateTargetDetectionFolder(template as TargetDetectionTemplate, path));
|
|
47
|
+
break;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
return issues;
|
|
@@ -72,6 +76,13 @@ function validateMapping3dFolder(t: Mapping3dTemplate, path: string): Issue[] {
|
|
|
72
76
|
);
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
function validateTargetDetectionFolder(t: TargetDetectionTemplate, path: string): Issue[] {
|
|
80
|
+
return validatePolygonRing(
|
|
81
|
+
t.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates,
|
|
82
|
+
`${path}.Placemark.Polygon`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
function validateMappingStripFolder(t: MappingStripTemplate, path: string): Issue[] {
|
|
76
87
|
const placemark = t.Folder.Placemark;
|
|
77
88
|
const tuples = placemark.LineString.coordinates.trim().split(/\s+/).filter(Boolean);
|