@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.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
- * Compile a `mappingStrip` template into a single wayline that follows the
1059
- * supplied LineString. Each LineString vertex becomes one waypoint.
1060
- *
1061
- * When `stripUseTemplateAltitude` is true the altitude in each LineString
1062
- * triple is used as the executeHeight; otherwise the folder's `height` is used.
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
- Point: { coordinates: {
1080
- lng: c.lng,
1081
- lat: c.lat
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
- const waylineFolder = {
1095
- templateId: folder.templateId,
1096
- waylineId: 0,
1097
- executeHeightMode: heightModeToExecuteMode$1(folder),
1098
- autoFlightSpeed: folder.autoFlightSpeed,
1099
- Placemark: placemarks
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
- missionConfig: template.missionConfig,
1103
- Folder: [waylineFolder]
1126
+ lines,
1127
+ segments,
1128
+ start: executable[0],
1129
+ end: executable.at(-1)
1104
1130
  };
1105
1131
  }
1106
- function parseTriples(s) {
1107
- return s.trim().split(/\s+/).map((tuple) => {
1108
- const parts = tuple.split(",").map(Number);
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
- lng: parts[0],
1111
- lat: parts[1],
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 tuples = t.Folder.Placemark.LineString.coordinates.trim().split(/\s+/);
1273
- if (tuples.length < 2) return [{
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: `LineString must have at least 2 vertices, got ${tuples.length}`,
1935
+ message: "LineString must contain at least 2 distinct vertices",
1276
1936
  severity: "error"
1277
- }];
1278
- return [];
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 };