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
25namespace cl_px4_mr
26{
27
31
32std::vector<NedPoint> generateFlightPatternAscend(
33 const FlightPatternAscendParams & p, const NedPoint & current)
34{
35 NedPoint target;
36 target.x = pick(p.targetX, current.x);
37 target.y = pick(p.targetY, current.y);
38 target.z = -p.altitudeAgl;
39
40 const bool atXy = std::hypot(target.x - current.x, target.y - current.y) <= p.tolerance;
41 if (atXy && std::fabs(current.z - target.z) <= p.tolerance)
42 {
43 return {target};
44 }
45 NedPoint start = current;
46 start.yaw = kNaN;
47 return {start, target};
48}
49
50std::vector<NedPoint> generateFlightPatternLoiter(
51 const FlightPatternLoiterParams & p, const NedPoint & current)
52{
53 const float cx = pick(p.centerX, current.x);
54 const float cy = pick(p.centerY, current.y);
55 const float z = altitudeToZ(p.altitudeAgl, current.z);
56 const float r = std::max(p.radius, 0.5f);
57 const int n = std::max(p.pointsPerCircle, 8);
58 const float sign = turnSign(p.direction);
59
60 if (p.count <= 0)
61 {
62 // nothing to fly: hold the entry point (the follower posts success at once)
63 NedPoint here = current;
64 here.z = z;
65 here.yaw = kNaN;
66 return {here};
67 }
68 const int count = p.count;
69
70 // enter at the point of the circle nearest the vehicle; if at the centre
71 // use the entry heading to pick a start
72 const float dx = current.x - cx;
73 const float dy = current.y - cy;
74 float theta0 =
75 std::hypot(dx, dy) > 0.1f ? std::atan2(dy, dx) : (std::isnan(current.yaw) ? 0.0f : current.yaw);
76
77 std::vector<NedPoint> path;
78 path.reserve(static_cast<size_t>(count * n + 1));
79 for (int k = 0; k <= count * n; ++k)
80 {
81 const float theta = theta0 + sign * 2.0f * static_cast<float>(M_PI) * k / n;
82 NedPoint v;
83 v.x = cx + r * std::cos(theta);
84 v.y = cy + r * std::sin(theta);
85 v.z = z;
86 v.yaw = p.faceCenter ? std::atan2(cy - v.y, cx - v.x) : kNaN;
87 path.push_back(v);
88 }
89 return path;
90}
91
92namespace
93{
94
95struct Leg
96{
97 float ux = 0.0f, uy = 0.0f; // unit direction
98 float length = 0.0f;
99};
100
101Leg legFrom(const NedPoint & start, float endX, float endY)
102{
103 Leg leg;
104 const float dx = endX - start.x;
105 const float dy = endY - start.y;
106 leg.length = std::hypot(dx, dy);
107 if (leg.length > 1e-3f)
108 {
109 leg.ux = dx / leg.length;
110 leg.uy = dy / leg.length;
111 }
112 return leg;
113}
114
115} // namespace
116
118 const FlightPatternSineWaveVerticalParams & p, const NedPoint & current)
119{
120 const float endX = pick(p.endX, current.x);
121 const float endY = pick(p.endY, current.y);
122 const float zBase = altitudeToZ(p.baseAltitudeAgl, current.z);
123 const float lambda = std::max(p.wavelength, 0.1f);
124 const float spacing = std::max(p.sampleSpacing, 0.1f);
125 const Leg leg = legFrom(current, endX, endY);
126
127 std::vector<NedPoint> path;
128 if (leg.length <= 1e-3f)
129 {
130 path.push_back(NedPoint{endX, endY, zBase, kNaN});
131 return path;
132 }
133
134 const int n = static_cast<int>(std::floor(leg.length / spacing));
135 path.reserve(static_cast<size_t>(n + 2));
136 for (int k = 0; k < n; ++k)
137 {
138 const float s = k * spacing;
139 NedPoint v;
140 v.x = current.x + s * leg.ux;
141 v.y = current.y + s * leg.uy;
142 v.z = zBase - p.amplitude * std::sin(2.0f * static_cast<float>(M_PI) * s / lambda);
143 path.push_back(v);
144 }
145 path.push_back(NedPoint{endX, endY, zBase, kNaN});
146 return path;
147}
148
150 const FlightPatternSineWaveHorizontalParams & p, const NedPoint & current)
151{
152 const float endX = pick(p.endX, current.x);
153 const float endY = pick(p.endY, current.y);
154 const float z = altitudeToZ(p.altitudeAgl, current.z);
155 const float lambda = std::max(p.wavelength, 0.1f);
156 const float spacing = std::max(p.sampleSpacing, 0.1f);
157 const Leg leg = legFrom(current, endX, endY);
158
159 std::vector<NedPoint> path;
160 if (leg.length <= 1e-3f)
161 {
162 path.push_back(NedPoint{endX, endY, z, kNaN});
163 return path;
164 }
165
166 // left-of-track normal in NED (x north, y east): rotate u by -90 deg
167 const float nx = leg.uy;
168 const float ny = -leg.ux;
169
170 const int n = static_cast<int>(std::floor(leg.length / spacing));
171 path.reserve(static_cast<size_t>(n + 2));
172 for (int k = 0; k < n; ++k)
173 {
174 const float s = k * spacing;
175 const float offset = p.amplitude * std::sin(2.0f * static_cast<float>(M_PI) * s / lambda);
176 NedPoint v;
177 v.x = current.x + s * leg.ux + offset * nx;
178 v.y = current.y + s * leg.uy + offset * ny;
179 v.z = z;
180 path.push_back(v);
181 }
182 path.push_back(NedPoint{endX, endY, z, kNaN});
183 return path;
184}
185
187 const FlightPatternSquareSpiralParams & p, const NedPoint & current)
188{
189 const float ox = pick(p.originX, current.x);
190 const float oy = pick(p.originY, current.y);
191 const float z = altitudeToZ(p.altitudeAgl, current.z);
192 const float spacing = std::max(p.spacing, 0.5f);
193 const float sign = turnSign(p.direction);
194 float heading = pick(p.initialHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
195
196 std::vector<NedPoint> path;
197 path.reserve(static_cast<size_t>(std::max(p.numLegs, 0) + 1));
198 NedPoint v;
199 v.x = ox;
200 v.y = oy;
201 v.z = z;
202 path.push_back(v);
203
204 for (int i = 0; i < p.numLegs; ++i)
205 {
206 const float len = spacing * static_cast<float>(i / 2 + 1);
207 if (p.maxLegLength > 0.0f && len > p.maxLegLength)
208 {
209 break;
210 }
211 v.x += len * std::cos(heading);
212 v.y += len * std::sin(heading);
213 path.push_back(v);
214 heading += sign * static_cast<float>(M_PI) / 2.0f;
215 }
216 return path;
217}
218
220{
221 const float spacing = std::max(p.spacing, 0.5f);
222 float total = 0.0f;
223 for (int i = 0; i < p.numLegs; ++i)
224 {
225 const float len = spacing * static_cast<float>(i / 2 + 1);
226 if (p.maxLegLength > 0.0f && len > p.maxLegLength)
227 {
228 break;
229 }
230 total += len;
231 }
232 return total;
233}
234
235std::vector<NedPoint> generateFlightPatternSpiral(
236 const FlightPatternSpiralParams & p, const NedPoint & current)
237{
238 const float cx = pick(p.centerX, current.x);
239 const float cy = pick(p.centerY, current.y);
240 const float z = altitudeToZ(p.altitudeAgl, current.z);
241 const float spacing = std::max(p.spacing, 0.5f);
242 const float a = spacing / (2.0f * static_cast<float>(M_PI)); // r = a * theta
243 const float rStart = std::max(p.startRadius, 0.0f);
244 const float rEnd = std::max(p.endRadius, rStart);
245 const float sign = turnSign(p.direction);
246 const float ds = std::max(p.sampleSpacing, 0.1f);
247
248 // angular origin: bearing of the vehicle from the centre, else entry heading
249 const float dx = current.x - cx;
250 const float dy = current.y - cy;
251 const float theta0 =
252 std::hypot(dx, dy) > 0.1f ? std::atan2(dy, dx) : (std::isnan(current.yaw) ? 0.0f : current.yaw);
253
254 const float thetaStart = rStart / a;
255 const float thetaEnd = rEnd / a;
256
257 std::vector<NedPoint> path;
258 float theta = thetaStart;
259 while (theta < thetaEnd)
260 {
261 const float r = a * theta;
262 NedPoint v;
263 v.x = cx + r * std::cos(theta0 + sign * theta);
264 v.y = cy + r * std::sin(theta0 + sign * theta);
265 v.z = z;
266 path.push_back(v);
267 theta += ds / std::max(r, a); // constant arc-length steps
268 }
269 NedPoint last;
270 last.x = cx + rEnd * std::cos(theta0 + sign * thetaEnd);
271 last.y = cy + rEnd * std::sin(theta0 + sign * thetaEnd);
272 last.z = z;
273 path.push_back(last);
274
275 if (p.inward)
276 {
277 std::reverse(path.begin(), path.end());
278 }
279 return path;
280}
281
283{
284 // area between the radii divided by the track spacing
285 const float rStart = std::max(p.startRadius, 0.0f);
286 const float rEnd = std::max(p.endRadius, rStart);
287 const float spacing = std::max(p.spacing, 0.5f);
288 return static_cast<float>(M_PI) * (rEnd * rEnd - rStart * rStart) / spacing;
289}
290
292{
293 const float spacing = std::max(p.laneSpacing, 0.5f);
294 return std::max(1, static_cast<int>(std::floor(std::max(p.width, 0.0f) / spacing + 1e-3f)) + 1);
295}
296
298{
299 const int lanes = flightPatternLawnmowerLaneCount(p);
300 return lanes * std::max(p.laneLength, 0.0f) + (lanes - 1) * std::max(p.laneSpacing, 0.5f);
301}
302
303std::vector<NedPoint> generateFlightPatternLawnmower(
304 const FlightPatternLawnmowerParams & p, const NedPoint & current)
305{
306 const float z = altitudeToZ(p.altitudeAgl, current.z);
307 const float heading = pick(p.laneHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
308 const float ux = std::cos(heading);
309 const float uy = std::sin(heading);
310 // side normal: RIGHT of the heading in NED (x north, y east) is (-uy, ux)
311 const float side = turnSign(p.firstTurn);
312 const float nx = -uy * side;
313 const float ny = ux * side;
314
315 const int lanes = flightPatternLawnmowerLaneCount(p);
316 const float spacing = std::max(p.laneSpacing, 0.5f);
317 const float length = std::max(p.laneLength, 0.0f);
318 const float coveredWidth = (lanes - 1) * spacing;
319
320 float ox = pick(p.originX, current.x);
321 float oy = pick(p.originY, current.y);
322 if (p.originIsCenter)
323 {
324 ox -= 0.5f * length * ux + 0.5f * coveredWidth * nx;
325 oy -= 0.5f * length * uy + 0.5f * coveredWidth * ny;
326 }
327
328 std::vector<NedPoint> path;
329 path.reserve(static_cast<size_t>(2 * lanes));
330 for (int i = 0; i < lanes; ++i)
331 {
332 NedPoint a;
333 a.x = ox + i * spacing * nx;
334 a.y = oy + i * spacing * ny;
335 a.z = z;
336 NedPoint b;
337 b.x = a.x + length * ux;
338 b.y = a.y + length * uy;
339 b.z = z;
340 if (i % 2 == 0)
341 {
342 path.push_back(a);
343 path.push_back(b);
344 }
345 else
346 {
347 path.push_back(b);
348 path.push_back(a);
349 }
350 }
351 return path;
352}
353
362
364 const FlightPatternGridPatternParams & p, const NedPoint & current)
365{
367 first.originIsCenter = true; // the grid is always centred on the origin
368 first.originX = pick(p.base.originX, current.x);
369 first.originY = pick(p.base.originY, current.y);
370 first.laneHeading = pick(p.base.laneHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
371
372 std::vector<NedPoint> path = generateFlightPatternLawnmower(first, current);
373 if (!p.secondPass || path.empty())
374 {
375 return path;
376 }
377
378 // second pass: rotated 90 degrees over the same rectangle; try the four
379 // start corners (heading +-90, step side left/right) and take the one that
380 // starts nearest the end of pass 1
381 const NedPoint & joint = path.back();
382 std::vector<NedPoint> best;
383 float bestDist = std::numeric_limits<float>::max();
384 for (int variant = 0; variant < 4; ++variant)
385 {
386 FlightPatternLawnmowerParams second = first;
387 second.laneLength = first.width;
388 second.width = first.laneLength;
389 second.laneHeading =
390 first.laneHeading + ((variant & 1) ? -1.0f : 1.0f) * static_cast<float>(M_PI) / 2.0f;
391 second.firstTurn = (variant & 2) ? Turn::LEFT : Turn::RIGHT;
392 std::vector<NedPoint> candidate = generateFlightPatternLawnmower(second, current);
393 if (candidate.empty())
394 {
395 continue;
396 }
397 const float d = nedDistance(joint, candidate.front());
398 if (d < bestDist)
399 {
400 bestDist = d;
401 best = std::move(candidate);
402 }
403 }
404 path.insert(path.end(), best.begin(), best.end());
405 return path;
406}
407
409{
410 return 9.0f * std::max(p.radius, 1.0f) * static_cast<float>(std::max(p.cycles, 1));
411}
412
413std::vector<NedPoint> generateFlightPatternVSSearch(
414 const FlightPatternVSSearchParams & p, const NedPoint & current)
415{
416 const float dx = pick(p.datumX, current.x);
417 const float dy = pick(p.datumY, current.y);
418 const float z = altitudeToZ(p.altitudeAgl, current.z);
419 const float r = std::max(p.radius, 1.0f);
420 const float sign = turnSign(p.direction);
421 const float h0 = pick(p.initialHeading, std::isnan(current.yaw) ? 0.0f : current.yaw);
422 const int cycles = std::max(p.cycles, 1);
423
424 // leg headings in units of 120 degrees relative to the cycle's base heading:
425 // legs 3->4 and 6->7 continue straight through the datum
426 static const int kSteps[9] = {0, 1, 2, 2, 0, 1, 1, 2, 0};
427 const float third = 2.0f * static_cast<float>(M_PI) / 3.0f;
428
429 std::vector<NedPoint> path;
430 path.reserve(static_cast<size_t>(9 * cycles + 1));
431 NedPoint v;
432 v.x = dx;
433 v.y = dy;
434 v.z = z;
435 path.push_back(v);
436
437 for (int c = 0; c < cycles; ++c)
438 {
439 const float base = h0 + sign * p.reorientation * static_cast<float>(c);
440 for (int leg = 0; leg < 9; ++leg)
441 {
442 const float heading = base + sign * third * static_cast<float>(kSteps[leg]);
443 v.x += r * std::cos(heading);
444 v.y += r * std::sin(heading);
445 path.push_back(v);
446 }
447 // snap back onto the datum to cancel accumulated float drift
448 path.back().x = dx;
449 path.back().y = dy;
450 }
451 return path;
452}
453
454} // 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 > 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)
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)