@lumikmz/kmz 0.7.1 → 0.8.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 +49 -4
- package/dist/index.mjs +748 -61
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/plan/index.ts +7 -1
- package/src/plan/plan-mapping-strip.test.ts +540 -0
- package/src/plan/plan-mapping-strip.ts +1056 -48
- package/src/plan/plan-mapping2d.ts +2 -2
- package/src/types/action.ts +3 -3
- package/src/types/placemark.ts +4 -0
- package/src/validate/validate-template.ts +57 -9
package/dist/index.mjs
CHANGED
|
@@ -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(enuWps, i) {
|
|
464
|
+
function turnDamping$1(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));
|
|
@@ -558,7 +558,7 @@ function shootSectionStartActions(pitchDeg, shootInterval, sectionStart, section
|
|
|
558
558
|
actionActuatorFuncParam: {
|
|
559
559
|
payloadPositionIndex: 0,
|
|
560
560
|
useGlobalPayloadLensIndex: false,
|
|
561
|
-
payloadLensIndex: "visable",
|
|
561
|
+
payloadLensIndex: ["visable"],
|
|
562
562
|
minShootInterval: shootInterval
|
|
563
563
|
}
|
|
564
564
|
}
|
|
@@ -606,7 +606,7 @@ function shootSectionStopActions(sectionEnd, groupId) {
|
|
|
606
606
|
actionActuatorFunc: "stopTimeLapse",
|
|
607
607
|
actionActuatorFuncParam: {
|
|
608
608
|
payloadPositionIndex: 0,
|
|
609
|
-
payloadLensIndex: "visable"
|
|
609
|
+
payloadLensIndex: ["visable"]
|
|
610
610
|
}
|
|
611
611
|
}, {
|
|
612
612
|
actionId: 1,
|
|
@@ -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(enuWps, index)
|
|
747
|
+
waypointTurnDampingDist: turnDamping$1(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(enuWps, index)
|
|
786
|
+
waypointTurnDampingDist: turnDamping$1(enuWps, index)
|
|
787
787
|
},
|
|
788
788
|
...COMMON_WP_EXTRAS
|
|
789
789
|
};
|
|
@@ -1054,64 +1054,450 @@ function heightModeToExecuteMode$2(mode) {
|
|
|
1054
1054
|
}
|
|
1055
1055
|
//#endregion
|
|
1056
1056
|
//#region src/plan/plan-mapping-strip.ts
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
function planMappingStrip(template) {
|
|
1065
|
-
const folder = template.Folder;
|
|
1066
|
-
const coords = parseTriples(folder.Placemark.LineString.coordinates);
|
|
1067
|
-
const origin = centroid(coords.map((c) => ({
|
|
1068
|
-
lng: c.lng,
|
|
1069
|
-
lat: c.lat
|
|
1070
|
-
})));
|
|
1071
|
-
const enuPath = coords.map((c) => lngLatToEnu({
|
|
1072
|
-
lng: c.lng,
|
|
1073
|
-
lat: c.lat
|
|
1074
|
-
}, origin));
|
|
1075
|
-
const placemarks = coords.map((c, index) => {
|
|
1076
|
-
const headingAngle = index === coords.length - 1 ? 0 : bearingDeg(enuPath[index], enuPath[index + 1]);
|
|
1077
|
-
const executeHeight = folder.Placemark.stripUseTemplateAltitude && c.alt !== void 0 ? c.alt : folder.Placemark.height;
|
|
1057
|
+
const POINT_EPS_M = .05;
|
|
1058
|
+
const MITER_LIMIT = 2.5;
|
|
1059
|
+
const MAX_ROUTE_COUNT = 1e3;
|
|
1060
|
+
const TURN_DAMPING_M = 10;
|
|
1061
|
+
function parseTriples(s) {
|
|
1062
|
+
return s.trim().split(/\s+/).filter(Boolean).map((tuple) => {
|
|
1063
|
+
const parts = tuple.split(",").map(Number);
|
|
1078
1064
|
return {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
} },
|
|
1083
|
-
index,
|
|
1084
|
-
executeHeight,
|
|
1085
|
-
waypointSpeed: folder.autoFlightSpeed,
|
|
1086
|
-
waypointHeadingParam: {
|
|
1087
|
-
waypointHeadingMode: "followWayline",
|
|
1088
|
-
waypointHeadingAngle: degrees(headingAngle),
|
|
1089
|
-
waypointHeadingPathMode: "followBadArc"
|
|
1090
|
-
},
|
|
1091
|
-
waypointTurnParam: { waypointTurnMode: "toPointAndPassWithContinuityCurvature" }
|
|
1065
|
+
lng: parts[0],
|
|
1066
|
+
lat: parts[1],
|
|
1067
|
+
...parts[2] !== void 0 ? { alt: parts[2] } : {}
|
|
1092
1068
|
};
|
|
1093
1069
|
});
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1070
|
+
}
|
|
1071
|
+
function cleanLine(points) {
|
|
1072
|
+
const clean = [];
|
|
1073
|
+
for (const point of points) {
|
|
1074
|
+
const previous = clean.at(-1);
|
|
1075
|
+
if (previous && Math.hypot(point[0] - previous[0], point[1] - previous[1]) < POINT_EPS_M) continue;
|
|
1076
|
+
clean.push([point[0], point[1]]);
|
|
1077
|
+
}
|
|
1078
|
+
return clean;
|
|
1079
|
+
}
|
|
1080
|
+
function lineLength(points) {
|
|
1081
|
+
let total = 0;
|
|
1082
|
+
for (let index = 1; index < points.length; index++) total += Math.hypot(points[index][0] - points[index - 1][0], points[index][1] - points[index - 1][1]);
|
|
1083
|
+
return total;
|
|
1084
|
+
}
|
|
1085
|
+
function localRouteSegments(lines) {
|
|
1086
|
+
const segments = [];
|
|
1087
|
+
lines.forEach((line, index) => {
|
|
1088
|
+
if (index > 0) segments.push({
|
|
1089
|
+
kind: "turn",
|
|
1090
|
+
points: [lines[index - 1].at(-1), line[0]],
|
|
1091
|
+
photoEnabled: false
|
|
1092
|
+
});
|
|
1093
|
+
segments.push({
|
|
1094
|
+
kind: "work",
|
|
1095
|
+
points: line,
|
|
1096
|
+
photoEnabled: true
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
return segments;
|
|
1100
|
+
}
|
|
1101
|
+
function serpentineRouteVariants(lines) {
|
|
1102
|
+
const variants = [];
|
|
1103
|
+
for (const reverseLineOrder of [false, true]) {
|
|
1104
|
+
const ordered = reverseLineOrder ? [...lines].reverse() : [...lines];
|
|
1105
|
+
for (const reverseFirstLine of [false, true]) variants.push(ordered.map((line, index) => index % 2 === Number(reverseFirstLine) ? [...line] : [...line].reverse()));
|
|
1106
|
+
}
|
|
1107
|
+
return variants;
|
|
1108
|
+
}
|
|
1109
|
+
function localRouteCandidate(lines, centerLine, elevationOptimizeEnable) {
|
|
1110
|
+
const segments = localRouteSegments(lines);
|
|
1111
|
+
if (elevationOptimizeEnable) {
|
|
1112
|
+
const normalEnd = joinSegmentPoints(segments).at(-1);
|
|
1113
|
+
let optimizationLine = [...centerLine];
|
|
1114
|
+
const first = optimizationLine[0];
|
|
1115
|
+
const last = optimizationLine.at(-1);
|
|
1116
|
+
if (Math.hypot(last[0] - normalEnd[0], last[1] - normalEnd[1]) < Math.hypot(first[0] - normalEnd[0], first[1] - normalEnd[1])) optimizationLine = optimizationLine.reverse();
|
|
1117
|
+
const optimizationPath = cleanLine([normalEnd, ...optimizationLine]);
|
|
1118
|
+
segments.push({
|
|
1119
|
+
kind: "elevationOptimize",
|
|
1120
|
+
points: [normalEnd, ...optimizationPath],
|
|
1121
|
+
photoEnabled: true
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
const executable = joinSegmentPoints(segments);
|
|
1101
1125
|
return {
|
|
1102
|
-
|
|
1103
|
-
|
|
1126
|
+
lines,
|
|
1127
|
+
segments,
|
|
1128
|
+
start: executable[0],
|
|
1129
|
+
end: executable.at(-1)
|
|
1104
1130
|
};
|
|
1105
1131
|
}
|
|
1106
|
-
function
|
|
1107
|
-
|
|
1108
|
-
|
|
1132
|
+
function shortestCandidateIndexes(candidateGroups, startPoint) {
|
|
1133
|
+
if (candidateGroups.length === 0) return [];
|
|
1134
|
+
let costs = candidateGroups[0].map((candidate) => startPoint ? Math.hypot(candidate.start[0] - startPoint[0], candidate.start[1] - startPoint[1]) : 0);
|
|
1135
|
+
const previousIndexes = [candidateGroups[0].map(() => -1)];
|
|
1136
|
+
for (let regionIndex = 1; regionIndex < candidateGroups.length; regionIndex++) {
|
|
1137
|
+
const previousCandidates = candidateGroups[regionIndex - 1];
|
|
1138
|
+
const candidates = candidateGroups[regionIndex];
|
|
1139
|
+
const nextCosts = [];
|
|
1140
|
+
const nextPreviousIndexes = [];
|
|
1141
|
+
for (const candidate of candidates) {
|
|
1142
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
1143
|
+
let bestPreviousIndex = 0;
|
|
1144
|
+
previousCandidates.forEach((previous, previousIndex) => {
|
|
1145
|
+
const transition = Math.hypot(candidate.start[0] - previous.end[0], candidate.start[1] - previous.end[1]);
|
|
1146
|
+
const cost = costs[previousIndex] + transition;
|
|
1147
|
+
if (cost < bestCost) {
|
|
1148
|
+
bestCost = cost;
|
|
1149
|
+
bestPreviousIndex = previousIndex;
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
nextCosts.push(bestCost);
|
|
1153
|
+
nextPreviousIndexes.push(bestPreviousIndex);
|
|
1154
|
+
}
|
|
1155
|
+
costs = nextCosts;
|
|
1156
|
+
previousIndexes.push(nextPreviousIndexes);
|
|
1157
|
+
}
|
|
1158
|
+
let selectedIndex = costs.reduce((best, cost, index) => cost < costs[best] ? index : best, 0);
|
|
1159
|
+
const selected = Array.from({ length: candidateGroups.length }, () => 0);
|
|
1160
|
+
for (let regionIndex = candidateGroups.length - 1; regionIndex >= 0; regionIndex--) {
|
|
1161
|
+
selected[regionIndex] = selectedIndex;
|
|
1162
|
+
selectedIndex = previousIndexes[regionIndex][selectedIndex];
|
|
1163
|
+
}
|
|
1164
|
+
return selected;
|
|
1165
|
+
}
|
|
1166
|
+
function joinSegmentPoints(segments) {
|
|
1167
|
+
const result = [];
|
|
1168
|
+
for (const segment of segments) if (result.length === 0) result.push(...segment.points);
|
|
1169
|
+
else result.push(...segment.points.slice(1));
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
function splitLineByDistance(points, distance) {
|
|
1173
|
+
const total = lineLength(points);
|
|
1174
|
+
const cut = Number.isFinite(distance) && distance > POINT_EPS_M ? distance : total;
|
|
1175
|
+
if (cut >= total - POINT_EPS_M) return [{
|
|
1176
|
+
id: "region-0",
|
|
1177
|
+
sourceRegionIndexes: [0],
|
|
1178
|
+
startDistance: 0,
|
|
1179
|
+
endDistance: total,
|
|
1180
|
+
centerLine: [...points],
|
|
1181
|
+
polygon: []
|
|
1182
|
+
}];
|
|
1183
|
+
const regions = [];
|
|
1184
|
+
let current = [points[0]];
|
|
1185
|
+
let regionStart = 0;
|
|
1186
|
+
let accumulated = 0;
|
|
1187
|
+
const finishRegion = (endDistance) => {
|
|
1188
|
+
if (current.length < 2) return;
|
|
1189
|
+
regions.push({
|
|
1190
|
+
id: `region-${regions.length}`,
|
|
1191
|
+
sourceRegionIndexes: [regions.length],
|
|
1192
|
+
startDistance: regionStart,
|
|
1193
|
+
endDistance,
|
|
1194
|
+
centerLine: current,
|
|
1195
|
+
polygon: []
|
|
1196
|
+
});
|
|
1197
|
+
current = [current.at(-1)];
|
|
1198
|
+
regionStart = endDistance;
|
|
1199
|
+
};
|
|
1200
|
+
for (let index = 1; index < points.length; index++) {
|
|
1201
|
+
const a = points[index - 1];
|
|
1202
|
+
const b = points[index];
|
|
1203
|
+
const segmentLength = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
1204
|
+
const currentLength = accumulated - regionStart;
|
|
1205
|
+
if (currentLength > POINT_EPS_M && currentLength + segmentLength > cut + POINT_EPS_M) finishRegion(accumulated);
|
|
1206
|
+
let segmentStart = a;
|
|
1207
|
+
let consumed = 0;
|
|
1208
|
+
while (segmentLength - consumed > cut + POINT_EPS_M) {
|
|
1209
|
+
const step = cut;
|
|
1210
|
+
const t = (consumed + step) / segmentLength;
|
|
1211
|
+
const point = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
1212
|
+
if (current.at(-1) !== segmentStart) current.push(segmentStart);
|
|
1213
|
+
current.push(point);
|
|
1214
|
+
finishRegion(accumulated + consumed + step);
|
|
1215
|
+
segmentStart = point;
|
|
1216
|
+
consumed += step;
|
|
1217
|
+
}
|
|
1218
|
+
if (current.at(-1) !== segmentStart) current.push(segmentStart);
|
|
1219
|
+
current.push(b);
|
|
1220
|
+
accumulated += segmentLength;
|
|
1221
|
+
}
|
|
1222
|
+
finishRegion(total);
|
|
1223
|
+
return regions;
|
|
1224
|
+
}
|
|
1225
|
+
function applyMergedRegionRanges(regions, ranges) {
|
|
1226
|
+
if (!ranges || ranges.length === 0) return [...regions];
|
|
1227
|
+
const normalized = ranges.map((range) => {
|
|
1228
|
+
if (range.length !== 2) throw new PlannerError("mappingStrip merged region range must contain [start,end]");
|
|
1229
|
+
const start = range[0] - 1;
|
|
1230
|
+
const end = range[1] - 1;
|
|
1231
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) throw new PlannerError("mappingStrip merged region range is invalid");
|
|
1232
|
+
if (end >= regions.length) throw new PlannerError("mappingStrip merged region range exceeds region count");
|
|
1109
1233
|
return {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
...parts[2] !== void 0 ? { alt: parts[2] } : {}
|
|
1234
|
+
start,
|
|
1235
|
+
end
|
|
1113
1236
|
};
|
|
1237
|
+
}).sort((a, b) => a.start - b.start);
|
|
1238
|
+
for (let index = 1; index < normalized.length; index++) if (normalized[index].start <= normalized[index - 1].end) throw new PlannerError("mappingStrip merged region ranges cannot overlap");
|
|
1239
|
+
const byStart = new Map(normalized.map((range) => [range.start, range]));
|
|
1240
|
+
const merged = [];
|
|
1241
|
+
for (let index = 0; index < regions.length;) {
|
|
1242
|
+
const range = byStart.get(index);
|
|
1243
|
+
if (!range) {
|
|
1244
|
+
merged.push(regions[index]);
|
|
1245
|
+
index += 1;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const members = regions.slice(range.start, range.end + 1);
|
|
1249
|
+
merged.push({
|
|
1250
|
+
id: `region-${range.start}-${range.end}`,
|
|
1251
|
+
sourceRegionIndexes: members.flatMap((region) => region.sourceRegionIndexes),
|
|
1252
|
+
startDistance: members[0].startDistance,
|
|
1253
|
+
endDistance: members.at(-1).endDistance,
|
|
1254
|
+
centerLine: members.flatMap((region, memberIndex) => memberIndex === 0 ? region.centerLine : region.centerLine.slice(1)),
|
|
1255
|
+
polygon: [],
|
|
1256
|
+
beforePoint: members[0].beforePoint,
|
|
1257
|
+
afterPoint: members.at(-1).afterPoint
|
|
1258
|
+
});
|
|
1259
|
+
index = range.end + 1;
|
|
1260
|
+
}
|
|
1261
|
+
return merged;
|
|
1262
|
+
}
|
|
1263
|
+
function cross(a, b) {
|
|
1264
|
+
return a[0] * b[1] - a[1] * b[0];
|
|
1265
|
+
}
|
|
1266
|
+
function segmentUnit(a, b) {
|
|
1267
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
1268
|
+
if (length < POINT_EPS_M) throw new PlannerError("mappingStrip center line has a zero segment");
|
|
1269
|
+
return [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
|
|
1270
|
+
}
|
|
1271
|
+
function leftNormal(direction) {
|
|
1272
|
+
return [-direction[1], direction[0]];
|
|
1273
|
+
}
|
|
1274
|
+
function offsetPolyline(points, distance) {
|
|
1275
|
+
if (distance === 0) return points.map((point) => [point[0], point[1]]);
|
|
1276
|
+
const directions = points.slice(1).map((point, index) => segmentUnit(points[index], point));
|
|
1277
|
+
const out = [];
|
|
1278
|
+
const firstNormal = leftNormal(directions[0]);
|
|
1279
|
+
out.push([points[0][0] + firstNormal[0] * distance, points[0][1] + firstNormal[1] * distance]);
|
|
1280
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
1281
|
+
const previousDirection = directions[index - 1];
|
|
1282
|
+
const nextDirection = directions[index];
|
|
1283
|
+
const previousNormal = leftNormal(previousDirection);
|
|
1284
|
+
const nextNormal = leftNormal(nextDirection);
|
|
1285
|
+
const previousOffset = [points[index][0] + previousNormal[0] * distance, points[index][1] + previousNormal[1] * distance];
|
|
1286
|
+
const nextOffset = [points[index][0] + nextNormal[0] * distance, points[index][1] + nextNormal[1] * distance];
|
|
1287
|
+
const denominator = cross(previousDirection, nextDirection);
|
|
1288
|
+
if (Math.abs(denominator) < 1e-9) {
|
|
1289
|
+
out.push(previousOffset);
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
const t = cross([nextOffset[0] - previousOffset[0], nextOffset[1] - previousOffset[1]], nextDirection) / denominator;
|
|
1293
|
+
const intersection = [previousOffset[0] + previousDirection[0] * t, previousOffset[1] + previousDirection[1] * t];
|
|
1294
|
+
if (Math.hypot(intersection[0] - points[index][0], intersection[1] - points[index][1]) <= Math.abs(distance) * MITER_LIMIT) out.push(intersection);
|
|
1295
|
+
else out.push(previousOffset, nextOffset);
|
|
1296
|
+
}
|
|
1297
|
+
const lastNormal = leftNormal(directions.at(-1));
|
|
1298
|
+
const last = points.at(-1);
|
|
1299
|
+
out.push([last[0] + lastNormal[0] * distance, last[1] + lastNormal[1] * distance]);
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
function corridorSide(points, distance, beforePoint, afterPoint) {
|
|
1303
|
+
const out = [];
|
|
1304
|
+
for (let index = 0; index < points.length; index++) {
|
|
1305
|
+
const point = points[index];
|
|
1306
|
+
const previous = index > 0 ? points[index - 1] : beforePoint;
|
|
1307
|
+
const next = index + 1 < points.length ? points[index + 1] : afterPoint;
|
|
1308
|
+
if (!previous && next) {
|
|
1309
|
+
const normal = leftNormal(segmentUnit(point, next));
|
|
1310
|
+
out.push([point[0] + normal[0] * distance, point[1] + normal[1] * distance]);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (previous && !next) {
|
|
1314
|
+
const normal = leftNormal(segmentUnit(previous, point));
|
|
1315
|
+
out.push([point[0] + normal[0] * distance, point[1] + normal[1] * distance]);
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (!previous || !next) continue;
|
|
1319
|
+
const previousDirection = segmentUnit(previous, point);
|
|
1320
|
+
const nextDirection = segmentUnit(point, next);
|
|
1321
|
+
const previousNormal = leftNormal(previousDirection);
|
|
1322
|
+
const nextNormal = leftNormal(nextDirection);
|
|
1323
|
+
const previousOffset = [point[0] + previousNormal[0] * distance, point[1] + previousNormal[1] * distance];
|
|
1324
|
+
const nextOffset = [point[0] + nextNormal[0] * distance, point[1] + nextNormal[1] * distance];
|
|
1325
|
+
const denominator = cross(previousDirection, nextDirection);
|
|
1326
|
+
if (Math.abs(denominator) < 1e-9) {
|
|
1327
|
+
out.push(previousOffset);
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (denominator * distance < 0) {
|
|
1331
|
+
out.push(previousOffset, nextOffset);
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
const t = cross([nextOffset[0] - previousOffset[0], nextOffset[1] - previousOffset[1]], nextDirection) / denominator;
|
|
1335
|
+
const intersection = [previousOffset[0] + previousDirection[0] * t, previousOffset[1] + previousDirection[1] * t];
|
|
1336
|
+
if (Math.hypot(intersection[0] - point[0], intersection[1] - point[1]) <= Math.abs(distance) * MITER_LIMIT) out.push(intersection);
|
|
1337
|
+
else out.push(previousOffset, nextOffset);
|
|
1338
|
+
}
|
|
1339
|
+
return out;
|
|
1340
|
+
}
|
|
1341
|
+
function corridorPolygon(points, left, right, beforePoint, afterPoint) {
|
|
1342
|
+
return [...corridorSide(points, left, beforePoint, afterPoint), ...corridorSide(points, -right, beforePoint, afterPoint).reverse()];
|
|
1343
|
+
}
|
|
1344
|
+
function perpendicularRouteLines(centerLine, leftExtend, rightExtend, margin, lineSpacing) {
|
|
1345
|
+
const left = offsetPolyline(centerLine, leftExtend);
|
|
1346
|
+
const right = offsetPolyline(centerLine, -rightExtend);
|
|
1347
|
+
if (left.length !== centerLine.length || right.length !== centerLine.length) throw new PlannerError("mappingStrip offset corridor cannot be segmented");
|
|
1348
|
+
const lines = [];
|
|
1349
|
+
for (let index = 0; index < centerLine.length - 1; index++) {
|
|
1350
|
+
const segment = [centerLine[index], centerLine[index + 1]];
|
|
1351
|
+
const cell = [
|
|
1352
|
+
left[index],
|
|
1353
|
+
left[index + 1],
|
|
1354
|
+
right[index + 1],
|
|
1355
|
+
right[index]
|
|
1356
|
+
];
|
|
1357
|
+
const direction = segmentUnit(segment[0], segment[1]);
|
|
1358
|
+
const projections = cell.map((point) => point[0] * direction[0] + point[1] * direction[1]);
|
|
1359
|
+
const longitudinalSpan = Math.max(...projections) - Math.min(...projections);
|
|
1360
|
+
const sourceSegmentLength = lineLength(segment);
|
|
1361
|
+
const previousDirection = index > 0 ? segmentUnit(centerLine[index - 1], segment[0]) : void 0;
|
|
1362
|
+
const hasSharpStart = previousDirection !== void 0 && Math.abs(cross(previousDirection, direction)) > Math.sin(5 * Math.PI / 180);
|
|
1363
|
+
const lineCount = Math.max(1, Math.ceil(sourceSegmentLength / lineSpacing), hasSharpStart ? Math.ceil(longitudinalSpan / lineSpacing) : Math.round(longitudinalSpan / lineSpacing));
|
|
1364
|
+
const phaseOffset = Math.max(0, (longitudinalSpan - (lineCount - 1) * lineSpacing) / 2);
|
|
1365
|
+
const grid = surveyGrid(cell, {
|
|
1366
|
+
directionDeg: (bearingDeg(segment[0], segment[1]) + 90) % 360,
|
|
1367
|
+
marginM: margin,
|
|
1368
|
+
lineSpacing,
|
|
1369
|
+
phaseOffsetM: phaseOffset
|
|
1370
|
+
});
|
|
1371
|
+
const flightDirection = [direction[1], -direction[0]];
|
|
1372
|
+
for (let gridIndex = 0; gridIndex + 1 < grid.length; gridIndex += 2) {
|
|
1373
|
+
const a = grid[gridIndex];
|
|
1374
|
+
const b = grid[gridIndex + 1];
|
|
1375
|
+
const followsDirection = (b[0] - a[0]) * flightDirection[0] + (b[1] - a[1]) * flightDirection[1] >= 0;
|
|
1376
|
+
lines.push(followsDirection ? [a, b] : [b, a]);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return lines;
|
|
1380
|
+
}
|
|
1381
|
+
function polygonArea(points) {
|
|
1382
|
+
let area2 = 0;
|
|
1383
|
+
for (let index = 0; index < points.length; index++) {
|
|
1384
|
+
const a = points[index];
|
|
1385
|
+
const b = points[(index + 1) % points.length];
|
|
1386
|
+
area2 += a[0] * b[1] - b[0] * a[1];
|
|
1387
|
+
}
|
|
1388
|
+
return Math.abs(area2) / 2;
|
|
1389
|
+
}
|
|
1390
|
+
function routeOffsets(folder, inputs) {
|
|
1391
|
+
if (folder.Placemark.singleLineEnable) return [0];
|
|
1392
|
+
const left = folder.Placemark.leftExtend;
|
|
1393
|
+
const minimum = -folder.Placemark.rightExtend;
|
|
1394
|
+
const maximum = left;
|
|
1395
|
+
const spacing = inputs.lineSpacing;
|
|
1396
|
+
if (!(spacing > POINT_EPS_M && Number.isFinite(spacing))) throw new PlannerError("mappingStrip lineSpacing must be finite and greater than zero");
|
|
1397
|
+
const span = maximum - minimum;
|
|
1398
|
+
const hasInfrared = folder.payloadParam?.imageFormat?.includes("ir") ?? false;
|
|
1399
|
+
const minimumCount = Math.max(hasInfrared && span > spacing + POINT_EPS_M ? 2 : 1, Math.ceil(Math.max(0, span - inputs.crossTrackFootprint) / spacing) + 1);
|
|
1400
|
+
if (minimumCount === 1) return [0];
|
|
1401
|
+
const count = minimumCount % 2 === 0 ? minimumCount : folder.Placemark.includeCenterEnable ? minimumCount : minimumCount + 1;
|
|
1402
|
+
if (count > MAX_ROUTE_COUNT) throw new PlannerError("mappingStrip route count exceeds limit");
|
|
1403
|
+
const offsets = Array.from({ length: count }, (_, index) => (minimum + maximum - (count - 1) * spacing) / 2 + index * spacing);
|
|
1404
|
+
if (folder.Placemark.includeCenterEnable) {
|
|
1405
|
+
const oddCount = minimumCount % 2 === 1 ? minimumCount : minimumCount + 1;
|
|
1406
|
+
const centered = Array.from({ length: oddCount }, (_, index) => (index - (oddCount - 1) / 2) * spacing);
|
|
1407
|
+
if (centered[0] >= minimum && centered.at(-1) <= maximum) return centered;
|
|
1408
|
+
if (!offsets.some((offset) => Math.abs(offset) < POINT_EPS_M)) offsets.push(0);
|
|
1409
|
+
}
|
|
1410
|
+
return offsets.sort((a, b) => a - b);
|
|
1411
|
+
}
|
|
1412
|
+
function normalizeInputs(folder, options) {
|
|
1413
|
+
const corridorWidth = folder.Placemark.leftExtend + folder.Placemark.rightExtend;
|
|
1414
|
+
return {
|
|
1415
|
+
crossTrackFootprint: Number(options?.crossTrackFootprint ?? corridorWidth),
|
|
1416
|
+
lineSpacing: Number(options?.lineSpacing ?? Math.max(corridorWidth, 1)),
|
|
1417
|
+
photoSpacing: Number(options?.photoSpacing ?? 1),
|
|
1418
|
+
shootInterval: Number(options?.shootInterval ?? 1)
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function deriveMappingStrip(template, options) {
|
|
1422
|
+
const folder = template.Folder;
|
|
1423
|
+
if (folder.Placemark.leftExtend < 0 || folder.Placemark.rightExtend < 0) throw new PlannerError("mappingStrip extend distances cannot be negative");
|
|
1424
|
+
const triples = parseTriples(folder.Placemark.LineString.coordinates);
|
|
1425
|
+
if (triples.length < 2) throw new PlannerError("mappingStrip requires at least two center points");
|
|
1426
|
+
if (triples.some((point) => !Number.isFinite(point.lng) || !Number.isFinite(point.lat))) throw new PlannerError("mappingStrip center line contains non-finite coordinates");
|
|
1427
|
+
const origin = centroid(triples);
|
|
1428
|
+
const centerLine = cleanLine(triples.map((point) => lngLatToEnu(point, origin)));
|
|
1429
|
+
if (centerLine.length < 2) throw new PlannerError("mappingStrip requires two distinct center points");
|
|
1430
|
+
const inputs = normalizeInputs(folder, options);
|
|
1431
|
+
if (!(inputs.crossTrackFootprint > 0 && Number.isFinite(inputs.crossTrackFootprint))) throw new PlannerError("mappingStrip crossTrackFootprint must be finite and greater than zero");
|
|
1432
|
+
if (!(inputs.photoSpacing > 0 && Number.isFinite(inputs.photoSpacing))) throw new PlannerError("mappingStrip photoSpacing must be finite and greater than zero");
|
|
1433
|
+
const splitRegions = splitLineByDistance(centerLine, folder.Placemark.cuttingDistance);
|
|
1434
|
+
const localRegions = applyMergedRegionRanges(splitRegions.map((region, index) => ({
|
|
1435
|
+
...region,
|
|
1436
|
+
beforePoint: index > 0 ? splitRegions[index - 1].centerLine.at(-2) : void 0,
|
|
1437
|
+
afterPoint: index + 1 < splitRegions.length ? splitRegions[index + 1].centerLine[1] : void 0
|
|
1438
|
+
})), folder.Placemark.stripMergedRegions).map((region) => ({
|
|
1439
|
+
...region,
|
|
1440
|
+
polygon: corridorPolygon(region.centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend, region.beforePoint, region.afterPoint)
|
|
1441
|
+
}));
|
|
1442
|
+
const offsets = routeOffsets(folder, inputs);
|
|
1443
|
+
const executionRegions = options?.regionOrderReversed ? [...localRegions].reverse() : localRegions;
|
|
1444
|
+
const perpendicular = options?.stripDirection === "perpendicular" && !folder.Placemark.singleLineEnable;
|
|
1445
|
+
const candidateGroups = executionRegions.map((region) => {
|
|
1446
|
+
const rawLines = perpendicular ? perpendicularRouteLines(region.centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend, folder.Placemark.margin, inputs.lineSpacing) : offsets.map((offset) => offsetPolyline(region.centerLine, offset));
|
|
1447
|
+
if (rawLines.length === 0) throw new PlannerError("mappingStrip region has no available flight routes");
|
|
1448
|
+
return serpentineRouteVariants(rawLines).map((lines) => localRouteCandidate(lines, region.centerLine, folder.Placemark.elevationOptimizeEnable));
|
|
1449
|
+
});
|
|
1450
|
+
const takeOffRefPoint = template.missionConfig.takeOffRefPoint;
|
|
1451
|
+
const selectedCandidateIndexes = shortestCandidateIndexes(candidateGroups, takeOffRefPoint ? lngLatToEnu(takeOffRefPoint, origin) : void 0);
|
|
1452
|
+
const flightRoutes = [];
|
|
1453
|
+
let previousRegionEnd;
|
|
1454
|
+
executionRegions.forEach((region, regionIndex) => {
|
|
1455
|
+
const candidate = candidateGroups[regionIndex][selectedCandidateIndexes[regionIndex]];
|
|
1456
|
+
const localSegments = previousRegionEnd ? [{
|
|
1457
|
+
kind: "transit",
|
|
1458
|
+
points: [previousRegionEnd, candidate.start],
|
|
1459
|
+
photoEnabled: false
|
|
1460
|
+
}, ...candidate.segments] : candidate.segments;
|
|
1461
|
+
const displayPath = joinSegmentPoints(localSegments);
|
|
1462
|
+
const executable = joinSegmentPoints(candidate.segments);
|
|
1463
|
+
flightRoutes.push({
|
|
1464
|
+
regionId: region.id,
|
|
1465
|
+
routeOffsets: perpendicular ? [] : offsets.map(meters),
|
|
1466
|
+
routeLines: candidate.lines.map((line) => line.map((point) => enuToLngLat(point, origin))),
|
|
1467
|
+
segments: localSegments.map((segment) => ({
|
|
1468
|
+
...segment,
|
|
1469
|
+
points: segment.points.map((point) => enuToLngLat(point, origin))
|
|
1470
|
+
})),
|
|
1471
|
+
executablePath: executable.map((point) => enuToLngLat(point, origin)),
|
|
1472
|
+
length: meters(lineLength(displayPath))
|
|
1473
|
+
});
|
|
1474
|
+
previousRegionEnd = candidate.end;
|
|
1114
1475
|
});
|
|
1476
|
+
const surveyAreaLocal = corridorPolygon(centerLine, folder.Placemark.leftExtend, folder.Placemark.rightExtend);
|
|
1477
|
+
const totalFlightLength = flightRoutes.reduce((sum, route) => sum + Number(route.length), 0);
|
|
1478
|
+
const estimatedPhotoCount = flightRoutes.reduce((sum, route) => sum + route.segments.filter(({ photoEnabled }) => photoEnabled).reduce((lineSum, segment) => {
|
|
1479
|
+
let length = 0;
|
|
1480
|
+
for (let index = 1; index < segment.points.length; index++) length += haversineMeters(segment.points[index - 1], segment.points[index]);
|
|
1481
|
+
return lineSum + Math.max(1, Math.floor(length / inputs.photoSpacing) + 1);
|
|
1482
|
+
}, 0), 0);
|
|
1483
|
+
return {
|
|
1484
|
+
centerLineLength: meters(lineLength(centerLine)),
|
|
1485
|
+
surveyArea: surveyAreaLocal.map((point) => enuToLngLat(point, origin)),
|
|
1486
|
+
surveyAreaSize: localRegions.reduce((sum, region) => sum + polygonArea(region.polygon), 0),
|
|
1487
|
+
regions: localRegions.map((region) => ({
|
|
1488
|
+
id: region.id,
|
|
1489
|
+
sourceRegionIndexes: region.sourceRegionIndexes,
|
|
1490
|
+
startDistance: meters(region.startDistance),
|
|
1491
|
+
endDistance: meters(region.endDistance),
|
|
1492
|
+
centerLine: region.centerLine.map((point) => enuToLngLat(point, origin)),
|
|
1493
|
+
polygon: region.polygon.map((point) => enuToLngLat(point, origin))
|
|
1494
|
+
})),
|
|
1495
|
+
flightRoutes,
|
|
1496
|
+
totalFlightLength: meters(totalFlightLength),
|
|
1497
|
+
photoSpacing: meters(inputs.photoSpacing),
|
|
1498
|
+
estimatedPhotoCount,
|
|
1499
|
+
estimatedDuration: totalFlightLength / Number(folder.autoFlightSpeed)
|
|
1500
|
+
};
|
|
1115
1501
|
}
|
|
1116
1502
|
function heightModeToExecuteMode$1(folder) {
|
|
1117
1503
|
switch (folder.waylineCoordinateSysParam.heightMode) {
|
|
@@ -1121,6 +1507,274 @@ function heightModeToExecuteMode$1(folder) {
|
|
|
1121
1507
|
case "realTimeFollowSurface": return "realTimeFollowSurface";
|
|
1122
1508
|
}
|
|
1123
1509
|
}
|
|
1510
|
+
function turnDamping(points, index) {
|
|
1511
|
+
const previous = haversineMeters(points[index - 1], points[index]);
|
|
1512
|
+
const next = haversineMeters(points[index], points[index + 1]);
|
|
1513
|
+
return meters(Math.min(TURN_DAMPING_M, previous * .45, next * .45));
|
|
1514
|
+
}
|
|
1515
|
+
function shootingGroups(folder, startIndex, endIndex, firstGroupId, inputs) {
|
|
1516
|
+
const lenses = folder.payloadParam?.imageFormat ?? ["visable"];
|
|
1517
|
+
if (folder.Placemark.shootType === "time") return [{
|
|
1518
|
+
actionGroupId: firstGroupId,
|
|
1519
|
+
actionGroupStartIndex: startIndex,
|
|
1520
|
+
actionGroupEndIndex: endIndex,
|
|
1521
|
+
actionGroupMode: "sequence",
|
|
1522
|
+
actionTrigger: { actionTriggerType: "betweenAdjacentPoints" },
|
|
1523
|
+
action: [{
|
|
1524
|
+
actionId: 0,
|
|
1525
|
+
actionActuatorFunc: "startTimeLapse",
|
|
1526
|
+
actionActuatorFuncParam: {
|
|
1527
|
+
payloadPositionIndex: folder.payloadParam?.payloadPositionIndex ?? 0,
|
|
1528
|
+
useGlobalPayloadLensIndex: false,
|
|
1529
|
+
payloadLensIndex: lenses,
|
|
1530
|
+
minShootInterval: inputs.shootInterval
|
|
1531
|
+
}
|
|
1532
|
+
}]
|
|
1533
|
+
}, {
|
|
1534
|
+
actionGroupId: firstGroupId + 1,
|
|
1535
|
+
actionGroupStartIndex: endIndex,
|
|
1536
|
+
actionGroupEndIndex: endIndex,
|
|
1537
|
+
actionGroupMode: "sequence",
|
|
1538
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
1539
|
+
action: [{
|
|
1540
|
+
actionId: 0,
|
|
1541
|
+
actionActuatorFunc: "stopTimeLapse",
|
|
1542
|
+
actionActuatorFuncParam: {
|
|
1543
|
+
payloadPositionIndex: folder.payloadParam?.payloadPositionIndex ?? 0,
|
|
1544
|
+
payloadLensIndex: lenses
|
|
1545
|
+
}
|
|
1546
|
+
}]
|
|
1547
|
+
}];
|
|
1548
|
+
return [{
|
|
1549
|
+
actionGroupId: firstGroupId,
|
|
1550
|
+
actionGroupStartIndex: startIndex,
|
|
1551
|
+
actionGroupEndIndex: endIndex,
|
|
1552
|
+
actionGroupMode: "sequence",
|
|
1553
|
+
actionTrigger: {
|
|
1554
|
+
actionTriggerType: "multipleDistance",
|
|
1555
|
+
actionTriggerParam: inputs.photoSpacing
|
|
1556
|
+
},
|
|
1557
|
+
action: [{
|
|
1558
|
+
actionId: 0,
|
|
1559
|
+
actionActuatorFunc: "takePhoto",
|
|
1560
|
+
actionActuatorFuncParam: {
|
|
1561
|
+
payloadPositionIndex: folder.payloadParam?.payloadPositionIndex ?? 0,
|
|
1562
|
+
useGlobalPayloadLensIndex: false,
|
|
1563
|
+
payloadLensIndex: lenses
|
|
1564
|
+
}
|
|
1565
|
+
}]
|
|
1566
|
+
}];
|
|
1567
|
+
}
|
|
1568
|
+
function nadirGimbalRotateAction(folder, actionId) {
|
|
1569
|
+
return {
|
|
1570
|
+
actionId,
|
|
1571
|
+
actionActuatorFunc: "gimbalRotate",
|
|
1572
|
+
actionActuatorFuncParam: {
|
|
1573
|
+
payloadPositionIndex: folder.payloadParam?.payloadPositionIndex ?? 0,
|
|
1574
|
+
gimbalHeadingYawBase: "aircraft",
|
|
1575
|
+
gimbalRotateMode: "absoluteAngle",
|
|
1576
|
+
gimbalPitchRotateEnable: true,
|
|
1577
|
+
gimbalPitchRotateAngle: degrees(-90),
|
|
1578
|
+
gimbalRollRotateEnable: false,
|
|
1579
|
+
gimbalRollRotateAngle: degrees(0),
|
|
1580
|
+
gimbalYawRotateEnable: false,
|
|
1581
|
+
gimbalYawRotateAngle: degrees(0),
|
|
1582
|
+
gimbalRotateTimeEnable: false,
|
|
1583
|
+
gimbalRotateTime: 10
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function continuousMappingGroups(folder, endIndex, inputs) {
|
|
1588
|
+
const payloadPositionIndex = folder.payloadParam?.payloadPositionIndex ?? 0;
|
|
1589
|
+
const lenses = folder.payloadParam?.imageFormat ?? ["visable"];
|
|
1590
|
+
const startAction = folder.Placemark.shootType === "time" ? {
|
|
1591
|
+
actionId: 2,
|
|
1592
|
+
actionActuatorFunc: "startTimeLapse",
|
|
1593
|
+
actionActuatorFuncParam: {
|
|
1594
|
+
payloadPositionIndex,
|
|
1595
|
+
useGlobalPayloadLensIndex: false,
|
|
1596
|
+
payloadLensIndex: lenses,
|
|
1597
|
+
minShootInterval: inputs.shootInterval
|
|
1598
|
+
}
|
|
1599
|
+
} : {
|
|
1600
|
+
actionId: 2,
|
|
1601
|
+
actionActuatorFunc: "takePhoto",
|
|
1602
|
+
actionActuatorFuncParam: {
|
|
1603
|
+
payloadPositionIndex,
|
|
1604
|
+
useGlobalPayloadLensIndex: false,
|
|
1605
|
+
payloadLensIndex: lenses
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
return [
|
|
1609
|
+
{
|
|
1610
|
+
actionGroupId: 0,
|
|
1611
|
+
actionGroupStartIndex: 0,
|
|
1612
|
+
actionGroupEndIndex: endIndex,
|
|
1613
|
+
actionGroupMode: "sequence",
|
|
1614
|
+
actionTrigger: { actionTriggerType: "betweenAdjacentPoints" },
|
|
1615
|
+
action: [
|
|
1616
|
+
{
|
|
1617
|
+
actionId: 0,
|
|
1618
|
+
actionActuatorFunc: "gimbalAngleLock",
|
|
1619
|
+
actionActuatorFuncParam: { payloadPositionIndex }
|
|
1620
|
+
},
|
|
1621
|
+
nadirGimbalRotateAction(folder, 1),
|
|
1622
|
+
startAction
|
|
1623
|
+
]
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
actionGroupId: 1,
|
|
1627
|
+
actionGroupStartIndex: 0,
|
|
1628
|
+
actionGroupEndIndex: endIndex,
|
|
1629
|
+
actionGroupMode: "sequence",
|
|
1630
|
+
actionTrigger: {
|
|
1631
|
+
actionTriggerType: "multipleTiming",
|
|
1632
|
+
actionTriggerParam: 2
|
|
1633
|
+
},
|
|
1634
|
+
action: [nadirGimbalRotateAction(folder, 0)]
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
actionGroupId: 2,
|
|
1638
|
+
actionGroupStartIndex: endIndex,
|
|
1639
|
+
actionGroupEndIndex: endIndex,
|
|
1640
|
+
actionGroupMode: "sequence",
|
|
1641
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
1642
|
+
action: [...folder.Placemark.shootType === "time" ? [{
|
|
1643
|
+
actionId: 0,
|
|
1644
|
+
actionActuatorFunc: "stopTimeLapse",
|
|
1645
|
+
actionActuatorFuncParam: {
|
|
1646
|
+
payloadPositionIndex,
|
|
1647
|
+
payloadLensIndex: lenses
|
|
1648
|
+
}
|
|
1649
|
+
}] : [], {
|
|
1650
|
+
actionId: 1,
|
|
1651
|
+
actionActuatorFunc: "gimbalAngleUnlock"
|
|
1652
|
+
}]
|
|
1653
|
+
}
|
|
1654
|
+
];
|
|
1655
|
+
}
|
|
1656
|
+
function elevationOptimizeGroups(folder, startIndex, endIndex, firstGroupId, inputs) {
|
|
1657
|
+
const gimbalGroup = {
|
|
1658
|
+
actionGroupId: firstGroupId,
|
|
1659
|
+
actionGroupStartIndex: startIndex,
|
|
1660
|
+
actionGroupEndIndex: startIndex,
|
|
1661
|
+
actionGroupMode: "sequence",
|
|
1662
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
1663
|
+
action: [{
|
|
1664
|
+
actionId: 0,
|
|
1665
|
+
actionActuatorFunc: "gimbalRotate",
|
|
1666
|
+
actionActuatorFuncParam: {
|
|
1667
|
+
payloadPositionIndex: folder.payloadParam?.payloadPositionIndex ?? 0,
|
|
1668
|
+
gimbalHeadingYawBase: "aircraft",
|
|
1669
|
+
gimbalRotateMode: "absoluteAngle",
|
|
1670
|
+
gimbalPitchRotateEnable: true,
|
|
1671
|
+
gimbalPitchRotateAngle: degrees(-45),
|
|
1672
|
+
gimbalRollRotateEnable: false,
|
|
1673
|
+
gimbalRollRotateAngle: degrees(0),
|
|
1674
|
+
gimbalYawRotateEnable: true,
|
|
1675
|
+
gimbalYawRotateAngle: degrees(0),
|
|
1676
|
+
gimbalRotateTimeEnable: false,
|
|
1677
|
+
gimbalRotateTime: 10
|
|
1678
|
+
}
|
|
1679
|
+
}, {
|
|
1680
|
+
actionId: 1,
|
|
1681
|
+
actionActuatorFunc: "hover",
|
|
1682
|
+
actionActuatorFuncParam: { hoverTime: .5 }
|
|
1683
|
+
}]
|
|
1684
|
+
};
|
|
1685
|
+
const shooting = shootingGroups(folder, startIndex, endIndex, firstGroupId + 1, inputs);
|
|
1686
|
+
const unlockGroup = {
|
|
1687
|
+
actionGroupId: firstGroupId + shooting.length + 1,
|
|
1688
|
+
actionGroupStartIndex: endIndex,
|
|
1689
|
+
actionGroupEndIndex: endIndex,
|
|
1690
|
+
actionGroupMode: "sequence",
|
|
1691
|
+
actionTrigger: { actionTriggerType: "reachPoint" },
|
|
1692
|
+
action: [{
|
|
1693
|
+
actionId: 0,
|
|
1694
|
+
actionActuatorFunc: "gimbalAngleUnlock"
|
|
1695
|
+
}]
|
|
1696
|
+
};
|
|
1697
|
+
return [
|
|
1698
|
+
gimbalGroup,
|
|
1699
|
+
...shooting,
|
|
1700
|
+
unlockGroup
|
|
1701
|
+
];
|
|
1702
|
+
}
|
|
1703
|
+
/** Compile a mappingStrip template into one executable Folder per cut region. */
|
|
1704
|
+
function planMappingStrip(template, options) {
|
|
1705
|
+
const folder = template.Folder;
|
|
1706
|
+
const derived = deriveMappingStrip(template, options);
|
|
1707
|
+
const inputs = normalizeInputs(folder, options);
|
|
1708
|
+
const waylineFolders = derived.flightRoutes.map((route, waylineId) => {
|
|
1709
|
+
const points = route.executablePath;
|
|
1710
|
+
const groupsByStart = /* @__PURE__ */ new Map();
|
|
1711
|
+
const forcedStopIndexes = /* @__PURE__ */ new Set();
|
|
1712
|
+
if (!folder.Placemark.elevationOptimizeEnable) for (const group of continuousMappingGroups(folder, points.length - 1, inputs)) {
|
|
1713
|
+
const index = group.actionGroupStartIndex;
|
|
1714
|
+
groupsByStart.set(index, [...groupsByStart.get(index) ?? [], group]);
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
let cursor = 0;
|
|
1718
|
+
let nextActionGroupId = 0;
|
|
1719
|
+
for (const segment of route.segments.filter(({ kind }) => kind !== "transit")) {
|
|
1720
|
+
const startIndex = cursor;
|
|
1721
|
+
const endIndex = startIndex + segment.points.length - 1;
|
|
1722
|
+
if (segment.photoEnabled && endIndex > startIndex) {
|
|
1723
|
+
const shootingStartIndex = segment.kind === "elevationOptimize" ? startIndex + 1 : startIndex;
|
|
1724
|
+
const groups = segment.kind === "elevationOptimize" ? elevationOptimizeGroups(folder, shootingStartIndex, endIndex, nextActionGroupId, inputs) : shootingGroups(folder, shootingStartIndex, endIndex, nextActionGroupId, inputs);
|
|
1725
|
+
nextActionGroupId += groups.length;
|
|
1726
|
+
for (const group of groups) {
|
|
1727
|
+
const index = group.actionGroupStartIndex;
|
|
1728
|
+
groupsByStart.set(index, [...groupsByStart.get(index) ?? [], group]);
|
|
1729
|
+
}
|
|
1730
|
+
if (segment.kind === "elevationOptimize") {
|
|
1731
|
+
forcedStopIndexes.add(startIndex);
|
|
1732
|
+
forcedStopIndexes.add(shootingStartIndex);
|
|
1733
|
+
forcedStopIndexes.add(endIndex);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
cursor = endIndex;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
const placemarks = points.map((point, index) => {
|
|
1740
|
+
const isFirst = index === 0;
|
|
1741
|
+
const isLast = index === points.length - 1;
|
|
1742
|
+
const headingAngle = isLast ? 0 : bearingDeg(lngLatToEnu(point, point), lngLatToEnu(points[index + 1], point));
|
|
1743
|
+
return {
|
|
1744
|
+
Point: { coordinates: point },
|
|
1745
|
+
index,
|
|
1746
|
+
executeHeight: folder.Placemark.height,
|
|
1747
|
+
waypointSpeed: folder.autoFlightSpeed,
|
|
1748
|
+
waypointHeadingParam: {
|
|
1749
|
+
waypointHeadingMode: "followWayline",
|
|
1750
|
+
waypointHeadingAngle: degrees(headingAngle),
|
|
1751
|
+
waypointHeadingPathMode: "followBadArc"
|
|
1752
|
+
},
|
|
1753
|
+
waypointTurnParam: isFirst || isLast || forcedStopIndexes.has(index) ? {
|
|
1754
|
+
waypointTurnMode: "toPointAndStopWithDiscontinuityCurvature",
|
|
1755
|
+
waypointTurnDampingDist: meters(0)
|
|
1756
|
+
} : {
|
|
1757
|
+
waypointTurnMode: "coordinateTurn",
|
|
1758
|
+
waypointTurnDampingDist: turnDamping(points, index)
|
|
1759
|
+
},
|
|
1760
|
+
useStraightLine: true,
|
|
1761
|
+
isRisky: false,
|
|
1762
|
+
...groupsByStart.has(index) ? { actionGroup: groupsByStart.get(index) } : {}
|
|
1763
|
+
};
|
|
1764
|
+
});
|
|
1765
|
+
return {
|
|
1766
|
+
templateId: folder.templateId + waylineId,
|
|
1767
|
+
waylineId,
|
|
1768
|
+
executeHeightMode: heightModeToExecuteMode$1(folder),
|
|
1769
|
+
autoFlightSpeed: folder.autoFlightSpeed,
|
|
1770
|
+
Placemark: placemarks
|
|
1771
|
+
};
|
|
1772
|
+
});
|
|
1773
|
+
return {
|
|
1774
|
+
missionConfig: template.missionConfig,
|
|
1775
|
+
Folder: waylineFolders
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1124
1778
|
//#endregion
|
|
1125
1779
|
//#region src/plan/plan-waypoint.ts
|
|
1126
1780
|
/**
|
|
@@ -1187,7 +1841,7 @@ function plan(template, options) {
|
|
|
1187
1841
|
if (isWaypoint(template)) return planWaypoint(template);
|
|
1188
1842
|
if (isMapping2d(template)) return planMapping2d(template, options);
|
|
1189
1843
|
if (isMapping3d(template)) return planMapping3d(template, options);
|
|
1190
|
-
if (isMappingStrip(template)) return planMappingStrip(template);
|
|
1844
|
+
if (isMappingStrip(template)) return planMappingStrip(template, options);
|
|
1191
1845
|
throw new PlannerError(`Unknown templateType: ${template.Folder.templateType}`);
|
|
1192
1846
|
}
|
|
1193
1847
|
//#endregion
|
|
@@ -1269,13 +1923,46 @@ function validateMapping3dFolder(t, path) {
|
|
|
1269
1923
|
return validatePolygonRing(t.Folder.Placemark.Polygon.outerBoundaryIs.LinearRing.coordinates, `${path}.Placemark.Polygon`);
|
|
1270
1924
|
}
|
|
1271
1925
|
function validateMappingStripFolder(t, path) {
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1926
|
+
const placemark = t.Folder.Placemark;
|
|
1927
|
+
const tuples = placemark.LineString.coordinates.trim().split(/\s+/).filter(Boolean);
|
|
1928
|
+
const distinct = new Set(tuples.map((tuple) => {
|
|
1929
|
+
const [lng, lat] = tuple.split(",");
|
|
1930
|
+
return `${lng},${lat}`;
|
|
1931
|
+
}));
|
|
1932
|
+
const issues = [];
|
|
1933
|
+
if (tuples.length < 2 || distinct.size < 2) issues.push({
|
|
1274
1934
|
path: `${path}.Placemark.LineString`,
|
|
1275
|
-
message:
|
|
1935
|
+
message: "LineString must contain at least 2 distinct vertices",
|
|
1276
1936
|
severity: "error"
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1937
|
+
});
|
|
1938
|
+
if (!Number.isFinite(placemark.leftExtend) || placemark.leftExtend < 0) issues.push({
|
|
1939
|
+
path: `${path}.Placemark.leftExtend`,
|
|
1940
|
+
message: "leftExtend must be finite and non-negative",
|
|
1941
|
+
severity: "error"
|
|
1942
|
+
});
|
|
1943
|
+
if (!Number.isFinite(placemark.rightExtend) || placemark.rightExtend < 0) issues.push({
|
|
1944
|
+
path: `${path}.Placemark.rightExtend`,
|
|
1945
|
+
message: "rightExtend must be finite and non-negative",
|
|
1946
|
+
severity: "error"
|
|
1947
|
+
});
|
|
1948
|
+
if (!Number.isFinite(placemark.cuttingDistance) || placemark.cuttingDistance <= 0) issues.push({
|
|
1949
|
+
path: `${path}.Placemark.cuttingDistance`,
|
|
1950
|
+
message: "cuttingDistance must be finite and greater than zero",
|
|
1951
|
+
severity: "error"
|
|
1952
|
+
});
|
|
1953
|
+
if (placemark.stripMergedRegions) {
|
|
1954
|
+
const ranges = placemark.stripMergedRegions.map((range) => ({
|
|
1955
|
+
start: range[0],
|
|
1956
|
+
end: range[1],
|
|
1957
|
+
length: range.length
|
|
1958
|
+
})).sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
1959
|
+
if (ranges.some(({ start, end, length }, index) => length !== 2 || !Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start || index > 0 && start <= ranges[index - 1].end)) issues.push({
|
|
1960
|
+
path: `${path}.Placemark.stripMergedRegions`,
|
|
1961
|
+
message: "stripMergedRegions must contain non-overlapping one-based [start,end] ranges",
|
|
1962
|
+
severity: "error"
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return issues;
|
|
1279
1966
|
}
|
|
1280
1967
|
function validatePolygonRing(coords, path) {
|
|
1281
1968
|
const tuples = coords.trim().split(/\s+/);
|
|
@@ -1342,4 +2029,4 @@ function validate(doc) {
|
|
|
1342
2029
|
return issues;
|
|
1343
2030
|
}
|
|
1344
2031
|
//#endregion
|
|
1345
|
-
export { DroneEnum, KmzError, PayloadEnum, PlannerError, degrees, haversineMeters, meters, metersPerSecond, plan, validate };
|
|
2032
|
+
export { DroneEnum, KmzError, PayloadEnum, PlannerError, degrees, deriveMappingStrip, haversineMeters, meters, metersPerSecond, plan, validate };
|