SMACC2
Loading...
Searching...
No Matches
pattern_generators.cpp
Go to the documentation of this file.
1// Copyright 2026 RobosoftAI Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/*****************************************************************************************************************
16 *
17 * Authors: Brett Aldrich
18 *
19 ******************************************************************************************************************/
20
22
23#include <algorithm>
24#include <limits>
25
26namespace cl_px4_mr
27{
28
32
33std::vector<NedPoint> generateFlightPatternAscend(
34 const FlightPatternAscendParams & p, const NedPoint & current)
35{
36 NedPoint target;
37 target.x = pick(p.targetX, current.x);
38 target.y = pick(p.targetY, current.y);
39 target.z = -p.altitudeAgl;
40
41 const bool atXy = std::hypot(target.x - current.x, target.y - current.y) <= p.tolerance;
42 if (atXy && std::fabs(current.z - target.z) <= p.tolerance)
43 {
44 return {target};
45 }
46 NedPoint start = current;
47 start.yaw = kNaN;
48 return {start, target};
49}
50
51std::vector<NedPoint> generateFlightPatternLoiter(
52 const FlightPatternLoiterParams & p, const NedPoint & current)
53{
54 const float cx = pick(p.centerX, current.x);
55 const float cy = pick(p.centerY, current.y);
56 const float z = altitudeToZ(p.altitudeAgl, current.z);
57 const float r = std::max(p.radius, 0.5f);
58 const int n = std::max(p.pointsPerCircle, 8);
59 const float sign = turnSign(p.direction);
60
61 if (p.count <= 0)
62 {
63 // nothing to fly: hold the entry point (the follower posts success at once)
64 NedPoint here = current;
65 here.z = z;
66 here.yaw = kNaN;
67 return {here};
68 }
69 const int count = p.count;
70
71 // enter at the point of the circle nearest the vehicle; if at the centre
72 // use the entry heading to pick a start
73 const float dx = current.x - cx;
74 const float dy = current.y - cy;
75 float theta0 =
76 std::hypot(dx, dy) > 0.1f ? std::atan2(dy, dx) : (std::isnan(current.yaw) ? 0.0f : current.yaw);
77
78 std::vector<NedPoint> path;
79 path.reserve(static_cast<size_t>(count * n + 1));
80 for (int k = 0; k <= count * n; ++k)
81 {
82 const float theta = theta0 + sign * 2.0f * static_cast<float>(M_PI) * k / n;
83 NedPoint v;
84 v.x = cx + r * std::cos(theta);
85 v.y = cy + r * std::sin(theta);
86 v.z = z;
87 v.yaw = p.faceCenter ? std::atan2(cy - v.y, cx - v.x) : kNaN;
88 path.push_back(v);
89 }
90 return path;
91}
92
93namespace
94{
95
96struct Leg
97{
98 float ux = 0.0f, uy = 0.0f; // unit direction
99 float length = 0.0f;
100};
101
102Leg legFrom(const NedPoint & start, float endX, float endY)
103{
104 Leg leg;
105 const float dx = endX - start.x;
106 const float dy = endY - start.y;
107 leg.length = std::hypot(dx, dy);
108 if (leg.length > 1e-3f)
109 {
110 leg.ux = dx / leg.length;
111 leg.uy = dy / leg.length;
112 }
113 return leg;
114}
115
116} // namespace
117
119 const FlightPatternFigureEightParams & p, const NedPoint & current)
120{
121 const float cx = pick(p.centerX, current.x);
122 const float cy = pick(p.centerY, current.y);
123 const float z = altitudeToZ(p.altitudeAgl, current.z);
124 const float a = std::max(p.size, 0.5f);
125 const int n = std::max(p.pointsPerLoop, 16);
126 const float sign = turnSign(p.direction);
127
128 if (p.count <= 0)
129 {
130 NedPoint here = current;
131 here.z = z;
132 here.yaw = kNaN;
133 return {here};
134 }
135
136 // lobe axis: given, else the entry heading, else north
137 const float heading =
138 std::isnan(p.heading) ? (std::isnan(current.yaw) ? 0.0f : current.yaw) : p.heading;
139 const float ch = std::cos(heading);
140 const float sh = std::sin(heading);
141
142 // one loop of the lemniscate x = a cos t / (1 + sin^2 t), y = a sin t cos t / (1 + sin^2 t)
143 std::vector<NedPoint> loop;
144 loop.reserve(static_cast<size_t>(n));
145 for (int k = 0; k < n; ++k)
146 {
147 const float t = sign * 2.0f * static_cast<float>(M_PI) * k / n;
148 const float st = std::sin(t);
149 const float ct = std::cos(t);
150 const float denom = 1.0f + st * st;
151 const float lx = a * ct / denom;
152 const float ly = a * st * ct / denom;
153 NedPoint v;
154 v.x = cx + lx * ch - ly * sh;
155 v.y = cy + lx * sh + ly * ch;
156 v.z = z;
157 v.yaw = kNaN;
158 loop.push_back(v);
159 }
160
161 // enter at the sample nearest the vehicle
162 int start = 0;
163 float best = std::numeric_limits<float>::infinity();
164 for (int k = 0; k < n; ++k)
165 {
166 const float d = std::hypot(loop[k].x - current.x, loop[k].y - current.y);
167 if (d < best)
168 {
169 best = d;
170 start = k;
171 }
172 }
173
174 std::vector<NedPoint> path;
175 path.reserve(static_cast<size_t>(p.count * n + 1));
176 for (int k = 0; k <= p.count * n; ++k)
177 {
178 path.push_back(loop[static_cast<size_t>((start + k) % n)]);
179 }
180 return path;
181}
182
184{
185 // perimeter of the lemniscate of Bernoulli = 5.2441 a
186 return 5.2441f * std::max(p.size, 0.5f) * std::max(p.count, 0);
187}
188
190 const FlightPatternSineWaveVerticalParams & p, const NedPoint & current)
191{
192 const float endX = pick(p.endX, current.x);
193 const float endY = pick(p.endY, current.y);
194 const float zBase = altitudeToZ(p.baseAltitudeAgl, current.z);
195 const float lambda = std::max(p.wavelength, 0.1f);
196 const float spacing = std::max(p.sampleSpacing, 0.1f);
197 const Leg leg = legFrom(current, endX, endY);
198
199 std::vector<NedPoint> path;
200 if (leg.length <= 1e-3f)
201 {
202 path.push_back(NedPoint{endX, endY, zBase, kNaN});
203 return path;
204 }
205
206 const int n = static_cast<int>(std::floor(leg.length / spacing));
207 path.reserve(static_cast<size_t>(n + 2));
208 for (int k = 0; k < n; ++k)
209 {
210 const float s = k * spacing;
211 NedPoint v;
212 v.x = current.x + s * leg.ux;
213 v.y = current.y + s * leg.uy;
214 v.z = zBase - p.amplitude * std::sin(2.0f * static_cast<float>(M_PI) * s / lambda);
215 path.push_back(v);
216 }
217 path.push_back(NedPoint{endX, endY, zBase, kNaN});
218 return path;
219}
220
222 const FlightPatternSineWaveHorizontalParams & p, const NedPoint & current)
223{
224 const float endX = pick(p.endX, current.x);
225 const float endY = pick(p.endY, current.y);
226 const float z = altitudeToZ(p.altitudeAgl, current.z);
227 const float lambda = std::max(p.wavelength, 0.1f);
228 const float spacing = std::max(p.sampleSpacing, 0.1f);
229 const Leg leg = legFrom(current, endX, endY);
230
231 std::vector<NedPoint> path;
232 if (leg.length <= 1e-3f)
233 {
234 path.push_back(NedPoint{endX, endY, z, kNaN});
235 return path;
236 }
237
238 // left-of-track normal in NED (x north, y east): rotate u by -90 deg
239 const float nx = leg.uy;
240 const float ny = -leg.ux;
241
242 const int n = static_cast<int>(std::floor(leg.length / spacing));
243 path.reserve(static_cast<size_t>(n + 2));
244 for (int k = 0; k < n; ++k)
245 {
246 const float s = k * spacing;
247 const float offset = p.amplitude * std::sin(2.0f * static_cast<float>(M_PI) * s / lambda);
248 NedPoint v;
249 v.x = current.x + s * leg.ux + offset * nx;
250 v.y = current.y + s * leg.uy + offset * ny;
251 v.z = z;
252 path.push_back(v);
253 }
254 path.push_back(NedPoint{endX, endY, z, kNaN});
255 return path;
256}
257
259 const FlightPatternSquareSpiralParams & p, const NedPoint & current)
260{
261 const float ox = pick(p.originX, current.x);
262 const float oy = pick(p.originY, current.y);
263 const float z = altitudeToZ(p.altitudeAgl, current.z);
264 const float spacing = std::max(p.spacing, 0.5f);
265 const float sign = turnSign(p.direction);
266 float heading = pick(p.initialHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
267
268 std::vector<NedPoint> path;
269 path.reserve(static_cast<size_t>(std::max(p.numLegs, 0) + 1));
270 NedPoint v;
271 v.x = ox;
272 v.y = oy;
273 v.z = z;
274 path.push_back(v);
275
276 for (int i = 0; i < p.numLegs; ++i)
277 {
278 const float len = spacing * static_cast<float>(i / 2 + 1);
279 if (p.maxLegLength > 0.0f && len > p.maxLegLength)
280 {
281 break;
282 }
283 v.x += len * std::cos(heading);
284 v.y += len * std::sin(heading);
285 path.push_back(v);
286 heading += sign * static_cast<float>(M_PI) / 2.0f;
287 }
288 return path;
289}
290
292{
293 const float spacing = std::max(p.spacing, 0.5f);
294 float total = 0.0f;
295 for (int i = 0; i < p.numLegs; ++i)
296 {
297 const float len = spacing * static_cast<float>(i / 2 + 1);
298 if (p.maxLegLength > 0.0f && len > p.maxLegLength)
299 {
300 break;
301 }
302 total += len;
303 }
304 return total;
305}
306
307std::vector<NedPoint> generateFlightPatternSpiral(
308 const FlightPatternSpiralParams & p, const NedPoint & current)
309{
310 const float cx = pick(p.centerX, current.x);
311 const float cy = pick(p.centerY, current.y);
312 const float z = altitudeToZ(p.altitudeAgl, current.z);
313 const float spacing = std::max(p.spacing, 0.5f);
314 const float a = spacing / (2.0f * static_cast<float>(M_PI)); // r = a * theta
315 const float rStart = std::max(p.startRadius, 0.0f);
316 const float rEnd = std::max(p.endRadius, rStart);
317 const float sign = turnSign(p.direction);
318 const float ds = std::max(p.sampleSpacing, 0.1f);
319
320 // angular origin: bearing of the vehicle from the centre, else entry heading
321 const float dx = current.x - cx;
322 const float dy = current.y - cy;
323 const float theta0 =
324 std::hypot(dx, dy) > 0.1f ? std::atan2(dy, dx) : (std::isnan(current.yaw) ? 0.0f : current.yaw);
325
326 const float thetaStart = rStart / a;
327 const float thetaEnd = rEnd / a;
328
329 std::vector<NedPoint> path;
330 float theta = thetaStart;
331 while (theta < thetaEnd)
332 {
333 const float r = a * theta;
334 NedPoint v;
335 v.x = cx + r * std::cos(theta0 + sign * theta);
336 v.y = cy + r * std::sin(theta0 + sign * theta);
337 v.z = z;
338 path.push_back(v);
339 theta += ds / std::max(r, a); // constant arc-length steps
340 }
341 NedPoint last;
342 last.x = cx + rEnd * std::cos(theta0 + sign * thetaEnd);
343 last.y = cy + rEnd * std::sin(theta0 + sign * thetaEnd);
344 last.z = z;
345 path.push_back(last);
346
347 if (p.inward)
348 {
349 std::reverse(path.begin(), path.end());
350 }
351 return path;
352}
353
355{
356 // area between the radii divided by the track spacing
357 const float rStart = std::max(p.startRadius, 0.0f);
358 const float rEnd = std::max(p.endRadius, rStart);
359 const float spacing = std::max(p.spacing, 0.5f);
360 return static_cast<float>(M_PI) * (rEnd * rEnd - rStart * rStart) / spacing;
361}
362
364{
365 const float spacing = std::max(p.laneSpacing, 0.5f);
366 return std::max(1, static_cast<int>(std::floor(std::max(p.width, 0.0f) / spacing + 1e-3f)) + 1);
367}
368
370{
371 const int lanes = flightPatternLawnmowerLaneCount(p);
372 return lanes * std::max(p.laneLength, 0.0f) + (lanes - 1) * std::max(p.laneSpacing, 0.5f);
373}
374
375std::vector<NedPoint> generateFlightPatternLawnmower(
376 const FlightPatternLawnmowerParams & p, const NedPoint & current)
377{
378 const float z = altitudeToZ(p.altitudeAgl, current.z);
379 const float heading = pick(p.laneHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
380 const float ux = std::cos(heading);
381 const float uy = std::sin(heading);
382 // side normal: RIGHT of the heading in NED (x north, y east) is (-uy, ux)
383 const float side = turnSign(p.firstTurn);
384 const float nx = -uy * side;
385 const float ny = ux * side;
386
387 const int lanes = flightPatternLawnmowerLaneCount(p);
388 const float spacing = std::max(p.laneSpacing, 0.5f);
389 const float length = std::max(p.laneLength, 0.0f);
390 const float coveredWidth = (lanes - 1) * spacing;
391
392 float ox = pick(p.originX, current.x);
393 float oy = pick(p.originY, current.y);
394 if (p.originIsCenter)
395 {
396 ox -= 0.5f * length * ux + 0.5f * coveredWidth * nx;
397 oy -= 0.5f * length * uy + 0.5f * coveredWidth * ny;
398 }
399
400 std::vector<NedPoint> path;
401 path.reserve(static_cast<size_t>(2 * lanes));
402 for (int i = 0; i < lanes; ++i)
403 {
404 NedPoint a;
405 a.x = ox + i * spacing * nx;
406 a.y = oy + i * spacing * ny;
407 a.z = z;
408 NedPoint b;
409 b.x = a.x + length * ux;
410 b.y = a.y + length * uy;
411 b.z = z;
412 if (i % 2 == 0)
413 {
414 path.push_back(a);
415 path.push_back(b);
416 }
417 else
418 {
419 path.push_back(b);
420 path.push_back(a);
421 }
422 }
423 return path;
424}
425
434
436 const FlightPatternGridPatternParams & p, const NedPoint & current)
437{
439 first.originIsCenter = true; // the grid is always centred on the origin
440 first.originX = pick(p.base.originX, current.x);
441 first.originY = pick(p.base.originY, current.y);
442 first.laneHeading = pick(p.base.laneHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
443
444 std::vector<NedPoint> path = generateFlightPatternLawnmower(first, current);
445 if (!p.secondPass || path.empty())
446 {
447 return path;
448 }
449
450 // second pass: rotated 90 degrees over the same rectangle; try the four
451 // start corners (heading +-90, step side left/right) and take the one that
452 // starts nearest the end of pass 1
453 const NedPoint & joint = path.back();
454 std::vector<NedPoint> best;
455 float bestDist = std::numeric_limits<float>::max();
456 for (int variant = 0; variant < 4; ++variant)
457 {
458 FlightPatternLawnmowerParams second = first;
459 second.laneLength = first.width;
460 second.width = first.laneLength;
461 second.laneHeading =
462 first.laneHeading + ((variant & 1) ? -1.0f : 1.0f) * static_cast<float>(M_PI) / 2.0f;
463 second.firstTurn = (variant & 2) ? Turn::LEFT : Turn::RIGHT;
464 std::vector<NedPoint> candidate = generateFlightPatternLawnmower(second, current);
465 if (candidate.empty())
466 {
467 continue;
468 }
469 const float d = nedDistance(joint, candidate.front());
470 if (d < bestDist)
471 {
472 bestDist = d;
473 best = std::move(candidate);
474 }
475 }
476 path.insert(path.end(), best.begin(), best.end());
477 return path;
478}
479
481{
482 return 9.0f * std::max(p.radius, 1.0f) * static_cast<float>(std::max(p.cycles, 1));
483}
484
485std::vector<NedPoint> generateFlightPatternVSSearch(
486 const FlightPatternVSSearchParams & p, const NedPoint & current)
487{
488 const float dx = pick(p.datumX, current.x);
489 const float dy = pick(p.datumY, current.y);
490 const float z = altitudeToZ(p.altitudeAgl, current.z);
491 const float r = std::max(p.radius, 1.0f);
492 const float sign = turnSign(p.direction);
493 const float h0 = pick(p.initialHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
494 const int cycles = std::max(p.cycles, 1);
495
496 // leg headings in units of 120 degrees relative to the cycle's base heading:
497 // legs 3->4 and 6->7 continue straight through the datum
498 static const int kSteps[9] = {0, 1, 2, 2, 0, 1, 1, 2, 0};
499 const float third = 2.0f * static_cast<float>(M_PI) / 3.0f;
500
501 std::vector<NedPoint> path;
502 path.reserve(static_cast<size_t>(9 * cycles + 1));
503 NedPoint v;
504 v.x = dx;
505 v.y = dy;
506 v.z = z;
507 path.push_back(v);
508
509 for (int c = 0; c < cycles; ++c)
510 {
511 const float base = h0 + sign * p.reorientation * static_cast<float>(c);
512 for (int leg = 0; leg < 9; ++leg)
513 {
514 const float heading = base + sign * third * static_cast<float>(kSteps[leg]);
515 v.x += r * std::cos(heading);
516 v.y += r * std::sin(heading);
517 path.push_back(v);
518 }
519 // snap back onto the datum to cancel accumulated float drift
520 path.back().x = dx;
521 path.back().y = dy;
522 }
523 return path;
524}
525
526} // namespace cl_px4_mr
Leg legFrom(const NedPoint &start, float endX, float endY)
float altitudeToZ(float altitudeAgl, float currentZ)
float pick(float param, float fallback)
std::vector< NedPoint > generateFlightPatternSpiral(const FlightPatternSpiralParams &p, const NedPoint &current)
std::vector< NedPoint > generateFlightPatternFigureEight(const FlightPatternFigureEightParams &p, const NedPoint &current)
std::vector< NedPoint > generateFlightPatternSineWaveVertical(const FlightPatternSineWaveVerticalParams &p, const NedPoint &current)
std::vector< NedPoint > generateFlightPatternVSSearch(const FlightPatternVSSearchParams &p, const NedPoint &current)
std::vector< NedPoint > generateFlightPatternSquareSpiral(const FlightPatternSquareSpiralParams &p, const NedPoint &current)
std::vector< NedPoint > generateFlightPatternLoiter(const FlightPatternLoiterParams &p, const NedPoint &current)
float flightPatternSquareSpiralLength(const FlightPatternSquareSpiralParams &p)
float nedDistance(const NedPoint &a, const NedPoint &b)
std::vector< NedPoint > generateFlightPatternSineWaveHorizontal(const FlightPatternSineWaveHorizontalParams &p, const NedPoint &current)
float flightPatternGridPatternLength(const FlightPatternGridPatternParams &p)
float flightPatternFigureEightLength(const FlightPatternFigureEightParams &p)
std::vector< NedPoint > generateFlightPatternLawnmower(const FlightPatternLawnmowerParams &p, const NedPoint &current)
int flightPatternLawnmowerLaneCount(const FlightPatternLawnmowerParams &p)
float flightPatternVSSearchLength(const FlightPatternVSSearchParams &p)
std::vector< NedPoint > generateFlightPatternGridPattern(const FlightPatternGridPatternParams &p, const NedPoint &current)
float turnSign(Turn t)
float flightPatternSpiralLength(const FlightPatternSpiralParams &p)
float flightPatternLawnmowerLength(const FlightPatternLawnmowerParams &p)
std::vector< NedPoint > generateFlightPatternAscend(const FlightPatternAscendParams &p, const NedPoint &current)