@lumikmz/kmz 0.7.0 → 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 +786 -67
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/plan/geometry.test.ts +18 -0
- package/src/plan/geometry.ts +56 -5
- 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/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export * from "./types/index.js";
|
|
2
2
|
export { plan, type PlanOptions } from "./plan/index.js";
|
|
3
|
+
export {
|
|
4
|
+
deriveMappingStrip,
|
|
5
|
+
type MappingStripDerived,
|
|
6
|
+
type MappingStripRegion,
|
|
7
|
+
type MappingStripRegionRoute,
|
|
8
|
+
} from "./plan/plan-mapping-strip.js";
|
|
3
9
|
export { validate } from "./validate/index.js";
|
|
4
10
|
export { haversineMeters } from "./plan/geometry.js";
|
|
@@ -93,4 +93,22 @@ describe("surveyGrid", () => {
|
|
|
93
93
|
const span = (pts: EnuPoint[]) => Math.abs(pts[1]![0] - pts[0]![0]);
|
|
94
94
|
expect(span(widened)).toBeGreaterThan(span(base));
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
it("keeps same-side turns inside the polygon instead of following acute slanted edges", () => {
|
|
98
|
+
const trapezoid: EnuPoint[] = [
|
|
99
|
+
[0, 0],
|
|
100
|
+
[120, 0],
|
|
101
|
+
[80, 100],
|
|
102
|
+
[0, 100],
|
|
103
|
+
];
|
|
104
|
+
const pts = surveyGrid(trapezoid, { directionDeg: 90, marginM: 0, lineSpacing: 25 });
|
|
105
|
+
|
|
106
|
+
// First turn is on the right side. The lower endpoint is pulled back from
|
|
107
|
+
// x=120 to the next line's right boundary x=110, making a vertical in-area
|
|
108
|
+
// connector instead of an acute diagonal along the slanted edge.
|
|
109
|
+
expect(pts[1]![0]).toBeCloseTo(pts[2]![0], 3);
|
|
110
|
+
expect(pts[1]![0]).toBeCloseTo(110, 3);
|
|
111
|
+
expect(pts[1]![1]).toBeCloseTo(0, 3);
|
|
112
|
+
expect(pts[2]![1]).toBeCloseTo(25, 3);
|
|
113
|
+
});
|
|
96
114
|
});
|
package/src/plan/geometry.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { LngLat } from "../types/branded.js";
|
|
|
3
3
|
const EARTH_RADIUS = 6_371_000;
|
|
4
4
|
const DEG2RAD = Math.PI / 180;
|
|
5
5
|
const RAD2DEG = 180 / Math.PI;
|
|
6
|
+
const ANGLE_EPS_RAD = 1e-12;
|
|
6
7
|
|
|
7
8
|
export type EnuPoint = readonly [number, number];
|
|
8
9
|
|
|
@@ -133,6 +134,7 @@ function dedupeRing(ring: readonly EnuPoint[], epsM = 0.5): EnuPoint[] {
|
|
|
133
134
|
|
|
134
135
|
/** Beyond this multiple of `distM`, a corner miter is clamped to avoid spikes. */
|
|
135
136
|
const OFFSET_MITER_LIMIT = 2.5;
|
|
137
|
+
const MIN_SCAN_SEGMENT_LEN_M = 1e-6;
|
|
136
138
|
|
|
137
139
|
/**
|
|
138
140
|
* Outward-offset (dilate) a simple polygon ring by `distM` meters in ENU.
|
|
@@ -208,6 +210,51 @@ export interface SurveyGridOptions {
|
|
|
208
210
|
lengthwiseShiftM?: number;
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
interface ScanSegment {
|
|
214
|
+
a: number;
|
|
215
|
+
b: number;
|
|
216
|
+
y: number;
|
|
217
|
+
leftToRight: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Pull same-side boustrophedon turn points inward until adjacent flight lines
|
|
222
|
+
* share one x coordinate. This keeps margin=0 routes inside the polygon while
|
|
223
|
+
* avoiding very acute boundary-following connectors on slanted edges.
|
|
224
|
+
*/
|
|
225
|
+
function stabilizeTurnCorridors(segments: ScanSegment[]): EnuPoint[] {
|
|
226
|
+
const segmentPts = segments.map(({ a, b, y, leftToRight }) =>
|
|
227
|
+
leftToRight
|
|
228
|
+
? ([
|
|
229
|
+
[a, y],
|
|
230
|
+
[b, y],
|
|
231
|
+
] as [EnuPoint, EnuPoint])
|
|
232
|
+
: ([
|
|
233
|
+
[b, y],
|
|
234
|
+
[a, y],
|
|
235
|
+
] as [EnuPoint, EnuPoint]),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i + 1 < segments.length; i++) {
|
|
239
|
+
const cur = segments[i]!;
|
|
240
|
+
const next = segments[i + 1]!;
|
|
241
|
+
if (cur.leftToRight === next.leftToRight) continue;
|
|
242
|
+
|
|
243
|
+
const overlapMin = Math.max(cur.a, next.a);
|
|
244
|
+
const overlapMax = Math.min(cur.b, next.b);
|
|
245
|
+
if (overlapMin > overlapMax) continue;
|
|
246
|
+
|
|
247
|
+
// LTR -> RTL turns on the right edge; RTL -> LTR turns on the left edge.
|
|
248
|
+
const turnX = cur.leftToRight ? overlapMax : overlapMin;
|
|
249
|
+
segmentPts[i]![1] = [turnX, cur.y];
|
|
250
|
+
segmentPts[i + 1]![0] = [turnX, next.y];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const pts: EnuPoint[] = [];
|
|
254
|
+
for (const [start, end] of segmentPts) pts.push(start, end);
|
|
255
|
+
return pts;
|
|
256
|
+
}
|
|
257
|
+
|
|
211
258
|
/**
|
|
212
259
|
* Boustrophedon survey grid over an arbitrary simple polygon, aligned to `directionDeg`.
|
|
213
260
|
* Flight lines run parallel to the heading; consecutive lines step by `lineSpacing`.
|
|
@@ -219,7 +266,8 @@ export function surveyGrid(ring: readonly EnuPoint[], opts: SurveyGridOptions):
|
|
|
219
266
|
const { directionDeg, marginM, lineSpacing, phaseOffsetM = 0, lengthwiseShiftM = 0 } = opts;
|
|
220
267
|
// Map the heading clockwise-from-north → CCW angle from +x axis
|
|
221
268
|
const t = directionDeg * DEG2RAD;
|
|
222
|
-
const
|
|
269
|
+
const rawAlpha = Math.atan2(Math.cos(t), Math.sin(t));
|
|
270
|
+
const alpha = Math.abs(rawAlpha) < ANGLE_EPS_RAD ? 0 : rawAlpha;
|
|
223
271
|
const scanRing = ring.map((p) => rotateEnu(p, -alpha));
|
|
224
272
|
|
|
225
273
|
let minY = Infinity;
|
|
@@ -229,17 +277,20 @@ export function surveyGrid(ring: readonly EnuPoint[], opts: SurveyGridOptions):
|
|
|
229
277
|
if (y > maxY) maxY = y;
|
|
230
278
|
}
|
|
231
279
|
|
|
232
|
-
const
|
|
280
|
+
const scanSegments: ScanSegment[] = [];
|
|
233
281
|
let leftToRight = true;
|
|
234
282
|
for (let y = minY + phaseOffsetM; y <= maxY + lineSpacing * 0.5; y += lineSpacing) {
|
|
283
|
+
let emitted = false;
|
|
235
284
|
for (const [x1, x2] of clipHorizontal(y, scanRing)) {
|
|
285
|
+
if (Math.abs(x2 - x1) < MIN_SCAN_SEGMENT_LEN_M) continue;
|
|
236
286
|
const a = x1 - marginM + lengthwiseShiftM;
|
|
237
287
|
const b = x2 + marginM + lengthwiseShiftM;
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
scanSegments.push({ a, b, y, leftToRight });
|
|
289
|
+
emitted = true;
|
|
240
290
|
}
|
|
241
|
-
leftToRight = !leftToRight;
|
|
291
|
+
if (emitted) leftToRight = !leftToRight;
|
|
242
292
|
}
|
|
243
293
|
|
|
294
|
+
const scanPts = stabilizeTurnCorridors(scanSegments);
|
|
244
295
|
return scanPts.map((p) => rotateEnu(p, alpha));
|
|
245
296
|
}
|
package/src/plan/index.ts
CHANGED
|
@@ -27,7 +27,13 @@ import { planWaypoint } from "./plan-waypoint.js";
|
|
|
27
27
|
*/
|
|
28
28
|
export interface PlanOptions {
|
|
29
29
|
lineSpacing?: Meters;
|
|
30
|
+
crossTrackFootprint?: Meters;
|
|
31
|
+
photoSpacing?: Meters;
|
|
30
32
|
shootInterval?: number;
|
|
33
|
+
/** mappingStrip UI layout mode; kept outside WPML until DJI direction encoding is verified. */
|
|
34
|
+
stripDirection?: "parallel" | "perpendicular";
|
|
35
|
+
/** Reverse mappingStrip region execution without changing region identities. */
|
|
36
|
+
regionOrderReversed?: boolean;
|
|
31
37
|
resolveExecuteHeight?: (point: {
|
|
32
38
|
lng: number;
|
|
33
39
|
lat: number;
|
|
@@ -52,6 +58,6 @@ export function plan(template: Template, options?: PlanOptions): Waylines {
|
|
|
52
58
|
if (isWaypoint(template)) return planWaypoint(template);
|
|
53
59
|
if (isMapping2d(template)) return planMapping2d(template, options);
|
|
54
60
|
if (isMapping3d(template)) return planMapping3d(template, options);
|
|
55
|
-
if (isMappingStrip(template)) return planMappingStrip(template);
|
|
61
|
+
if (isMappingStrip(template)) return planMappingStrip(template, options);
|
|
56
62
|
throw new PlannerError(`Unknown templateType: ${(template as Template).Folder.templateType}`);
|
|
57
63
|
}
|