@lumikmz/kmz 0.2.1 → 0.3.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/README.md +69 -66
- package/dist/index.d.mts +215 -76
- package/dist/index.mjs +634 -157
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/plan/geometry.test.ts +96 -0
- package/src/plan/geometry.ts +161 -25
- package/src/plan/index.ts +23 -3
- package/src/plan/plan-mapping-strip.ts +3 -3
- package/src/plan/plan-mapping2d.ts +512 -89
- package/src/plan/plan-mapping3d.ts +157 -98
- package/src/plan/plan.test.ts +263 -9
- package/src/types/action.ts +65 -1
- package/src/types/drone-payload.ts +18 -12
- package/src/types/enums.ts +17 -3
- package/src/types/mission-config.ts +5 -0
- package/src/types/placemark.ts +92 -15
- package/src/types/template.ts +12 -76
- package/src/types/waylines.ts +12 -3
- package/src/types/waypoint-params.ts +18 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { offsetPolygon, rotateEnu, surveyGrid, type EnuPoint } from "./geometry.js";
|
|
3
|
+
|
|
4
|
+
const SQUARE: EnuPoint[] = [
|
|
5
|
+
[0, 0],
|
|
6
|
+
[100, 0],
|
|
7
|
+
[100, 100],
|
|
8
|
+
[0, 100],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe("offsetPolygon", () => {
|
|
12
|
+
it("dilates a square outward by ~distM on each side", () => {
|
|
13
|
+
const out = offsetPolygon(SQUARE, 10);
|
|
14
|
+
expect(out).toHaveLength(4);
|
|
15
|
+
const xs = out.map((p) => p[0]);
|
|
16
|
+
const ys = out.map((p) => p[1]);
|
|
17
|
+
// 90° corners → miter √2·10 ≈ 14.1 along the diagonal, i.e. -10 / 110 per axis.
|
|
18
|
+
expect(Math.min(...xs)).toBeCloseTo(-10, 1);
|
|
19
|
+
expect(Math.max(...xs)).toBeCloseTo(110, 1);
|
|
20
|
+
expect(Math.min(...ys)).toBeCloseTo(-10, 1);
|
|
21
|
+
expect(Math.max(...ys)).toBeCloseTo(110, 1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("does not spike on a near-collinear vertex (miter-stable)", () => {
|
|
25
|
+
// A quad whose right side is split by an almost-straight extra vertex — the
|
|
26
|
+
// edge-intersection method would shoot this corner far to the right.
|
|
27
|
+
const ring: EnuPoint[] = [
|
|
28
|
+
[0, 0],
|
|
29
|
+
[100, 0],
|
|
30
|
+
[100.5, 50], // near-collinear with its neighbours on the right edge
|
|
31
|
+
[100, 100],
|
|
32
|
+
[0, 100],
|
|
33
|
+
];
|
|
34
|
+
const out = offsetPolygon(ring, 20);
|
|
35
|
+
// No vertex may land beyond distM·MITER_LIMIT (2.5) of the original area.
|
|
36
|
+
expect(Math.max(...out.map((p) => p[0]))).toBeLessThan(100.5 + 20 * 2.5 + 1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("tolerates a duplicate vertex without exploding", () => {
|
|
40
|
+
const ring: EnuPoint[] = [
|
|
41
|
+
[0, 0],
|
|
42
|
+
[100, 0],
|
|
43
|
+
[100, 0], // duplicate
|
|
44
|
+
[100, 100],
|
|
45
|
+
[0, 100],
|
|
46
|
+
];
|
|
47
|
+
const out = offsetPolygon(ring, 10);
|
|
48
|
+
expect(out).toHaveLength(4);
|
|
49
|
+
expect(out.every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("rotateEnu", () => {
|
|
54
|
+
it("rotates +x by 90° CCW to +y", () => {
|
|
55
|
+
const [x, y] = rotateEnu([1, 0], Math.PI / 2);
|
|
56
|
+
expect(x).toBeCloseTo(0, 6);
|
|
57
|
+
expect(y).toBeCloseTo(1, 6);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rotates +y by 90° CCW to -x", () => {
|
|
61
|
+
const [x, y] = rotateEnu([0, 1], Math.PI / 2);
|
|
62
|
+
expect(x).toBeCloseTo(-1, 6);
|
|
63
|
+
expect(y).toBeCloseTo(0, 6);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("identity at 0 radians", () => {
|
|
67
|
+
const [x, y] = rotateEnu([3, 4], 0);
|
|
68
|
+
expect(x).toBeCloseTo(3, 6);
|
|
69
|
+
expect(y).toBeCloseTo(4, 6);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("surveyGrid", () => {
|
|
74
|
+
it("scans a square and returns paired flight-line endpoints", () => {
|
|
75
|
+
const pts = surveyGrid(SQUARE, { directionDeg: 90, marginM: 0, lineSpacing: 25 });
|
|
76
|
+
expect(pts.length).toBeGreaterThanOrEqual(8); // ≥4 lines × 2 endpoints
|
|
77
|
+
expect(pts.length % 2).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("direction=90 → flight lines run east-west (endpoints share y)", () => {
|
|
81
|
+
const pts = surveyGrid(SQUARE, { directionDeg: 90, marginM: 0, lineSpacing: 25 });
|
|
82
|
+
expect(pts[0]![1]).toBeCloseTo(pts[1]![1], 3); // first line: same y
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("direction=0 → flight lines run north-south (endpoints share x)", () => {
|
|
86
|
+
const pts = surveyGrid(SQUARE, { directionDeg: 0, marginM: 0, lineSpacing: 25 });
|
|
87
|
+
expect(pts[0]![0]).toBeCloseTo(pts[1]![0], 3); // first line: same x
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("margin extends each flight line lengthwise", () => {
|
|
91
|
+
const base = surveyGrid(SQUARE, { directionDeg: 90, marginM: 0, lineSpacing: 25 });
|
|
92
|
+
const widened = surveyGrid(SQUARE, { directionDeg: 90, marginM: 10, lineSpacing: 25 });
|
|
93
|
+
const span = (pts: EnuPoint[]) => Math.abs(pts[1]![0] - pts[0]![0]);
|
|
94
|
+
expect(span(widened)).toBeGreaterThan(span(base));
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/plan/geometry.ts
CHANGED
|
@@ -41,6 +41,33 @@ export function bearingDeg(from: EnuPoint, to: EnuPoint): number {
|
|
|
41
41
|
return Math.atan2(to[0] - from[0], to[1] - from[1]) * RAD2DEG;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Orient an ordered survey path so its first point is the end nearest `home`.
|
|
46
|
+
* A boustrophedon grid has two natural entry ends; the aircraft should start at
|
|
47
|
+
* whichever is closer to the take-off point. Returns the input unchanged when
|
|
48
|
+
* `home` is absent or the start is already the nearer end.
|
|
49
|
+
*/
|
|
50
|
+
export function orientPathToHome(
|
|
51
|
+
enuWps: readonly EnuPoint[],
|
|
52
|
+
origin: LngLat,
|
|
53
|
+
home: LngLat | undefined,
|
|
54
|
+
): readonly EnuPoint[] {
|
|
55
|
+
if (!home || enuWps.length < 2) return enuWps;
|
|
56
|
+
const h = lngLatToEnu(home, origin);
|
|
57
|
+
const sqDist = (p: EnuPoint) => (p[0] - h[0]) ** 2 + (p[1] - h[1]) ** 2;
|
|
58
|
+
return sqDist(enuWps[enuWps.length - 1]!) < sqDist(enuWps[0]!) ? [...enuWps].reverse() : enuWps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Great-circle distance between two WGS-84 coordinates, in meters. */
|
|
62
|
+
export function haversineMeters(a: LngLat, b: LngLat): number {
|
|
63
|
+
const dLat = (b.lat - a.lat) * DEG2RAD;
|
|
64
|
+
const dLng = (b.lng - a.lng) * DEG2RAD;
|
|
65
|
+
const lat1 = a.lat * DEG2RAD;
|
|
66
|
+
const lat2 = b.lat * DEG2RAD;
|
|
67
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
|
|
68
|
+
return EARTH_RADIUS * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
/** Clip a horizontal scan line `y = const` against a polygon; returns `[x_enter, x_exit]` pairs. */
|
|
45
72
|
function clipHorizontal(y: number, polygon: readonly EnuPoint[]): [number, number][] {
|
|
46
73
|
const intersections: number[] = [];
|
|
@@ -61,31 +88,6 @@ function clipHorizontal(y: number, polygon: readonly EnuPoint[]): [number, numbe
|
|
|
61
88
|
return out;
|
|
62
89
|
}
|
|
63
90
|
|
|
64
|
-
/** Serpentine boustrophedon scan over a polygon; returns ENU waypoints. */
|
|
65
|
-
export function scanWaypoints(polygon: readonly EnuPoint[], lineSpacing: number): EnuPoint[] {
|
|
66
|
-
let minY = Infinity;
|
|
67
|
-
let maxY = -Infinity;
|
|
68
|
-
for (const [, y] of polygon) {
|
|
69
|
-
if (y < minY) minY = y;
|
|
70
|
-
if (y > maxY) maxY = y;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const waypoints: EnuPoint[] = [];
|
|
74
|
-
let leftToRight = true;
|
|
75
|
-
for (let y = minY; y <= maxY + lineSpacing * 0.5; y += lineSpacing) {
|
|
76
|
-
const segments = clipHorizontal(y, polygon);
|
|
77
|
-
for (const [x1, x2] of segments) {
|
|
78
|
-
if (leftToRight) {
|
|
79
|
-
waypoints.push([x1, y], [x2, y]);
|
|
80
|
-
} else {
|
|
81
|
-
waypoints.push([x2, y], [x1, y]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
leftToRight = !leftToRight;
|
|
85
|
-
}
|
|
86
|
-
return waypoints;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
91
|
/**
|
|
90
92
|
* Parse a KML coordinates string like `"lon,lat lon,lat lon,lat"` or `"lon,lat,alt ..."`
|
|
91
93
|
* into LngLat array (altitude dropped).
|
|
@@ -107,3 +109,137 @@ export function encodePolygonCoordinates(
|
|
|
107
109
|
): string {
|
|
108
110
|
return ring.map((p) => `${p.lng},${p.lat},${altitudeMeters}`).join(" ");
|
|
109
111
|
}
|
|
112
|
+
|
|
113
|
+
/** Rotate an ENU point counter-clockwise by `rad` radians about the origin. */
|
|
114
|
+
export function rotateEnu([x, y]: EnuPoint, rad: number): EnuPoint {
|
|
115
|
+
const c = Math.cos(rad);
|
|
116
|
+
const s = Math.sin(rad);
|
|
117
|
+
return [x * c - y * s, x * s + y * c];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Drop a closing duplicate and any consecutive vertices closer than `epsM`. */
|
|
121
|
+
function dedupeRing(ring: readonly EnuPoint[], epsM = 0.5): EnuPoint[] {
|
|
122
|
+
const pts: EnuPoint[] = [];
|
|
123
|
+
for (const p of ring) {
|
|
124
|
+
const last = pts[pts.length - 1];
|
|
125
|
+
if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < epsM) continue;
|
|
126
|
+
pts.push([p[0], p[1]]);
|
|
127
|
+
}
|
|
128
|
+
const f = pts[0];
|
|
129
|
+
const l = pts[pts.length - 1];
|
|
130
|
+
if (pts.length >= 2 && f && l && Math.hypot(f[0] - l[0], f[1] - l[1]) < epsM) pts.pop();
|
|
131
|
+
return pts;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Beyond this multiple of `distM`, a corner miter is clamped to avoid spikes. */
|
|
135
|
+
const OFFSET_MITER_LIMIT = 2.5;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Outward-offset (dilate) a simple polygon ring by `distM` meters in ENU.
|
|
139
|
+
* Each vertex moves along the bisector of its two adjacent outward edge normals
|
|
140
|
+
* by `distM / cos(halfAngle)`, clamped by a miter limit. The bisector form is
|
|
141
|
+
* stable when adjacent edges are near-collinear (where edge-intersection would
|
|
142
|
+
* shoot a spike far outward); the clamp bounds sharp corners; degenerate/duplicate
|
|
143
|
+
* vertices are removed first. Intended for convex-ish survey areas.
|
|
144
|
+
*/
|
|
145
|
+
export function offsetPolygon(ring: readonly EnuPoint[], distM: number): EnuPoint[] {
|
|
146
|
+
const pts = dedupeRing(ring);
|
|
147
|
+
const n = pts.length;
|
|
148
|
+
if (distM === 0 || n < 3) return pts;
|
|
149
|
+
|
|
150
|
+
// Signed area → winding. Outward normal of edge dir (dx,dy) is the right normal
|
|
151
|
+
// (dy,−dx) for a CCW ring, the left normal otherwise.
|
|
152
|
+
let area2 = 0;
|
|
153
|
+
for (let i = 0; i < n; i++) {
|
|
154
|
+
const a = pts[i]!;
|
|
155
|
+
const b = pts[(i + 1) % n]!;
|
|
156
|
+
area2 += a[0] * b[1] - b[0] * a[1];
|
|
157
|
+
}
|
|
158
|
+
const ccw = area2 > 0;
|
|
159
|
+
const outwardNormal = (dx: number, dy: number): EnuPoint => (ccw ? [dy, -dx] : [-dy, dx]);
|
|
160
|
+
|
|
161
|
+
const out: EnuPoint[] = [];
|
|
162
|
+
for (let i = 0; i < n; i++) {
|
|
163
|
+
const prev = pts[(i - 1 + n) % n]!;
|
|
164
|
+
const cur = pts[i]!;
|
|
165
|
+
const next = pts[(i + 1) % n]!;
|
|
166
|
+
|
|
167
|
+
const inLen = Math.hypot(cur[0] - prev[0], cur[1] - prev[1]) || 1;
|
|
168
|
+
const outLen = Math.hypot(next[0] - cur[0], next[1] - cur[1]) || 1;
|
|
169
|
+
const nIn = outwardNormal((cur[0] - prev[0]) / inLen, (cur[1] - prev[1]) / inLen);
|
|
170
|
+
const nOut = outwardNormal((next[0] - cur[0]) / outLen, (next[1] - cur[1]) / outLen);
|
|
171
|
+
|
|
172
|
+
let bx = nIn[0] + nOut[0];
|
|
173
|
+
let by = nIn[1] + nOut[1];
|
|
174
|
+
const bl = Math.hypot(bx, by);
|
|
175
|
+
if (bl < 1e-9) {
|
|
176
|
+
// ~180° reversal — offset straight out along the outgoing edge normal.
|
|
177
|
+
out.push([cur[0] + nOut[0] * distM, cur[1] + nOut[1] * distM]);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
bx /= bl;
|
|
181
|
+
by /= bl;
|
|
182
|
+
const cosHalf = nOut[0] * bx + nOut[1] * by; // = cos(half exterior angle)
|
|
183
|
+
const scale = distM / Math.max(cosHalf, 1 / OFFSET_MITER_LIMIT);
|
|
184
|
+
out.push([cur[0] + bx * scale, cur[1] + by * scale]);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface SurveyGridOptions {
|
|
190
|
+
/** Main flight-line heading, degrees clockwise from North. */
|
|
191
|
+
directionDeg: number;
|
|
192
|
+
/** Outward survey-area margin (meters); extends each flight line lengthwise. */
|
|
193
|
+
marginM: number;
|
|
194
|
+
/** Perpendicular distance between adjacent flight lines (meters). */
|
|
195
|
+
lineSpacing: number;
|
|
196
|
+
/**
|
|
197
|
+
* Across-track phase shift (meters) applied to every flight line.
|
|
198
|
+
* Used to interleave two grids on the same axis by half a line spacing
|
|
199
|
+
* (e.g. mapping3d forward vs backward oblique passes). Default 0.
|
|
200
|
+
*/
|
|
201
|
+
phaseOffsetM?: number;
|
|
202
|
+
/**
|
|
203
|
+
* Along-track translation (meters) of the whole grid, in the `directionDeg`
|
|
204
|
+
* direction. Used for the mapping3d oblique stand-off: a tilted camera images
|
|
205
|
+
* ahead of the aircraft, so the flight grid is shifted opposite the look so it
|
|
206
|
+
* overhangs the boundary on one side only. Default 0.
|
|
207
|
+
*/
|
|
208
|
+
lengthwiseShiftM?: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Boustrophedon survey grid over an arbitrary simple polygon, aligned to `directionDeg`.
|
|
213
|
+
* Flight lines run parallel to the heading; consecutive lines step by `lineSpacing`.
|
|
214
|
+
* `marginM` extends each line lengthwise past the polygon edge.
|
|
215
|
+
* Returns ENU waypoints in flight order — no transit/entry point
|
|
216
|
+
* (entry climb is a missionConfig behaviour, not a grid concern).
|
|
217
|
+
*/
|
|
218
|
+
export function surveyGrid(ring: readonly EnuPoint[], opts: SurveyGridOptions): EnuPoint[] {
|
|
219
|
+
const { directionDeg, marginM, lineSpacing, phaseOffsetM = 0, lengthwiseShiftM = 0 } = opts;
|
|
220
|
+
// Map the heading clockwise-from-north → CCW angle from +x axis
|
|
221
|
+
const t = directionDeg * DEG2RAD;
|
|
222
|
+
const alpha = Math.atan2(Math.cos(t), Math.sin(t));
|
|
223
|
+
const scanRing = ring.map((p) => rotateEnu(p, -alpha));
|
|
224
|
+
|
|
225
|
+
let minY = Infinity;
|
|
226
|
+
let maxY = -Infinity;
|
|
227
|
+
for (const [, y] of scanRing) {
|
|
228
|
+
if (y < minY) minY = y;
|
|
229
|
+
if (y > maxY) maxY = y;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const scanPts: EnuPoint[] = [];
|
|
233
|
+
let leftToRight = true;
|
|
234
|
+
for (let y = minY + phaseOffsetM; y <= maxY + lineSpacing * 0.5; y += lineSpacing) {
|
|
235
|
+
for (const [x1, x2] of clipHorizontal(y, scanRing)) {
|
|
236
|
+
const a = x1 - marginM + lengthwiseShiftM;
|
|
237
|
+
const b = x2 + marginM + lengthwiseShiftM;
|
|
238
|
+
if (leftToRight) scanPts.push([a, y], [b, y]);
|
|
239
|
+
else scanPts.push([b, y], [a, y]);
|
|
240
|
+
}
|
|
241
|
+
leftToRight = !leftToRight;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return scanPts.map((p) => rotateEnu(p, alpha));
|
|
245
|
+
}
|
package/src/plan/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PlannerError } from "../types/errors.js";
|
|
2
|
+
import type { Meters } from "../types/branded.js";
|
|
2
3
|
import type {
|
|
3
4
|
Mapping2dTemplate,
|
|
4
5
|
Mapping3dTemplate,
|
|
@@ -12,6 +13,23 @@ import { planMapping3d } from "./plan-mapping3d.js";
|
|
|
12
13
|
import { planMappingStrip } from "./plan-mapping-strip.js";
|
|
13
14
|
import { planWaypoint } from "./plan-waypoint.js";
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Options for `plan()`.
|
|
18
|
+
*
|
|
19
|
+
* Mapping templates (`mapping2d`, `mapping3d`) require `lineSpacing` — the perpendicular
|
|
20
|
+
* distance between adjacent flight lines in meters. The formula is:
|
|
21
|
+
* `2 · height · tan(HFOV / 2) · (1 − sideOverlap)`.
|
|
22
|
+
*
|
|
23
|
+
* `shootInterval` — time-lapse interval in seconds for `startTimeLapse` actions.
|
|
24
|
+
* Formula: `forwardGroundSpacing / speed`,
|
|
25
|
+
* where `forwardGroundSpacing = 2 · height · tan(VFOV / 2) · (1 − forwardOverlap)`.
|
|
26
|
+
* Camera FOV and sensor data live in the demo layer, not in this package.
|
|
27
|
+
*/
|
|
28
|
+
export interface PlanOptions {
|
|
29
|
+
lineSpacing?: Meters;
|
|
30
|
+
shootInterval?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
const isWaypoint = (t: Template): t is WaypointTemplate => t.Folder.templateType === "waypoint";
|
|
16
34
|
const isMapping2d = (t: Template): t is Mapping2dTemplate => t.Folder.templateType === "mapping2d";
|
|
17
35
|
const isMapping3d = (t: Template): t is Mapping3dTemplate => t.Folder.templateType === "mapping3d";
|
|
@@ -21,11 +39,13 @@ const isMappingStrip = (t: Template): t is MappingStripTemplate =>
|
|
|
21
39
|
/**
|
|
22
40
|
* Compile a high-level `Template` into executable `Waylines`.
|
|
23
41
|
* Dispatch is by `template.Folder.templateType` per the DJI WPML spec.
|
|
42
|
+
*
|
|
43
|
+
* Mapping templates require `options.lineSpacing` (meters between flight lines).
|
|
24
44
|
*/
|
|
25
|
-
export function plan(template: Template): Waylines {
|
|
45
|
+
export function plan(template: Template, options?: PlanOptions): Waylines {
|
|
26
46
|
if (isWaypoint(template)) return planWaypoint(template);
|
|
27
|
-
if (isMapping2d(template)) return planMapping2d(template);
|
|
28
|
-
if (isMapping3d(template)) return planMapping3d(template);
|
|
47
|
+
if (isMapping2d(template)) return planMapping2d(template, options);
|
|
48
|
+
if (isMapping3d(template)) return planMapping3d(template, options);
|
|
29
49
|
if (isMappingStrip(template)) return planMappingStrip(template);
|
|
30
50
|
throw new PlannerError(`Unknown templateType: ${(template as Template).Folder.templateType}`);
|
|
31
51
|
}
|
|
@@ -22,9 +22,9 @@ export function planMappingStrip(template: MappingStripTemplate): Waylines {
|
|
|
22
22
|
const isLast = index === coords.length - 1;
|
|
23
23
|
const headingAngle = isLast ? 0 : bearingDeg(enuPath[index]!, enuPath[index + 1]!);
|
|
24
24
|
const executeHeight =
|
|
25
|
-
folder.stripUseTemplateAltitude && c.alt !== undefined
|
|
26
|
-
? (c.alt as ReturnType<typeof folder.height extends infer T ? () => T : never>)
|
|
27
|
-
: folder.height;
|
|
25
|
+
folder.Placemark.stripUseTemplateAltitude && c.alt !== undefined
|
|
26
|
+
? (c.alt as ReturnType<typeof folder.Placemark.height extends infer T ? () => T : never>)
|
|
27
|
+
: folder.Placemark.height;
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
30
|
Point: { coordinates: { lng: c.lng, lat: c.lat } },
|