1 // rnav_waypt_controller.cxx - Waypoint-specific behaviours for RNAV systems
2 // Written by James Turner, started 2009.
3 //
4 // Copyright (C) 2009 Curtis L. Olson
5 //
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License as
8 // published by the Free Software Foundation; either version 2 of the
9 // License, or (at your option) any later version.
10 //
11 // This program is distributed in the hope that it will be useful, but
12 // WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 // General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software
18 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20 #include "rnav_waypt_controller.hxx"
21
22 #include <cassert>
23 #include <stdexcept>
24
25 #include <simgear/sg_inlines.h>
26 #include <simgear/structure/exception.hxx>
27
28 #include <Airports/runways.hxx>
29 #include "Main/util.hxx" // for fgLowPass
30
31 extern double pointsKnownDistanceFromGC(const SGGeoc& a, const SGGeoc&b, const SGGeoc& d, double dist);
32
33 namespace flightgear
34 {
35
36 const auto turnComputeDist = SG_NM_TO_METER * 4.0;
37
38 // declared in routePath.cxx
39 bool geocRadialIntersection(const SGGeoc& a, double r1, const SGGeoc& b, double r2, SGGeoc& result);
40
41 /**
42 * Helper function Cross track error:
43 * http://williams.best.vwh.net/avform.htm#XTE
44 *
45 * param distanceOriginToPosition in Nautical Miles
46 * param courseDev difference between courseOriginToAircraft(AD) and courseOriginToTarget(AB)
47 * return XTE in Nautical Miles
48 *
49 * A(origin)
50 * B(target)
51 * D(aircraft) perhaps off course
52 *
53 * A-->--B
54 * \ /
55 * \ /
56 * D
57 */
58
greatCircleCrossTrackError(double distanceOriginToPosition,double courseDev)59 double greatCircleCrossTrackError(double distanceOriginToPosition,double courseDev){
60 double crossTrackError = asin(
61 sin(distanceOriginToPosition * SG_NM_TO_RAD) *
62 sin(courseDev * SG_DEGREES_TO_RADIANS)
63 );
64 return crossTrackError * SG_RAD_TO_NM;
65 }
66
computeTurnCenter(double turnRadiusM,const SGGeod & basePos,double inboundTrack,double turnAngle)67 SGGeod computeTurnCenter(double turnRadiusM, const SGGeod& basePos, double inboundTrack, double turnAngle)
68 {
69
70 // this is the heading half way through the turn. Perpendicular to
71 // this is our turn center
72 const auto halfTurnHeading = inboundTrack + (turnAngle * 0.5);
73 double p = copysign(90.0, turnAngle);
74 double h = halfTurnHeading + p;
75 SG_NORMALIZE_RANGE(h, 0.0, 360.0);
76
77 const double tcOffset = turnRadiusM / cos(turnAngle * 0.5 * SG_DEGREES_TO_RADIANS);
78 return SGGeodesy::direct(basePos, h, tcOffset);
79 }
80
81 ////////////////////////////////////////////////////////////////////////////s
82
~WayptController()83 WayptController::~WayptController()
84 {
85 }
86
init()87 bool WayptController::init()
88 {
89 return true;
90 }
91
isDone() const92 bool WayptController::isDone() const
93 {
94 if (_subController) {
95 return _subController->isDone();
96 }
97
98 return _isDone;
99 }
100
setDone()101 void WayptController::setDone()
102 {
103 if (_isDone) {
104 SG_LOG(SG_AUTOPILOT, SG_DEV_WARN, "already done @ WayptController::setDone");
105 }
106
107 _isDone = true;
108 }
109
timeToWaypt() const110 double WayptController::timeToWaypt() const
111 {
112 double gs = _rnav->groundSpeedKts();
113 if (gs < 1.0) {
114 return -1.0; // stationary
115 }
116
117 gs*= SG_KT_TO_MPS;
118 return (distanceToWayptM() / gs);
119 }
120
status() const121 std::string WayptController::status() const
122 {
123 return {};
124 }
125
setSubController(WayptController * sub)126 void WayptController::setSubController(WayptController* sub)
127 {
128 _subController.reset(sub);
129 if (_subController) {
130 // seems like a good idea to check this
131 assert(_subController->_rnav == _rnav);
132 _subController->init();
133 }
134 }
135
trueBearingDeg() const136 double WayptController::trueBearingDeg() const
137 {
138 if (_subController)
139 return _subController->trueBearingDeg();
140
141 return _targetTrack;
142 }
143
targetTrackDeg() const144 double WayptController::targetTrackDeg() const
145 {
146 if (_subController)
147 return _subController->targetTrackDeg();
148
149 return _targetTrack;
150 }
151
xtrackErrorNm() const152 double WayptController::xtrackErrorNm() const
153 {
154 if (_subController)
155 return _subController->xtrackErrorNm();
156
157 return 0.0;
158 }
159
courseDeviationDeg() const160 double WayptController::courseDeviationDeg() const
161 {
162 if (_subController)
163 return _subController->courseDeviationDeg();
164
165 return 0.0;
166 }
167
168 //////////////
169
170 class BasicWayptCtl : public WayptController
171 {
172 public:
BasicWayptCtl(RNAV * aRNAV,const WayptRef & aWpt)173 BasicWayptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
174 WayptController(aRNAV, aWpt),
175 _distanceAircraftTargetMeter(0.0),
176 _courseDev(0.0)
177 {
178 if (aWpt->flag(WPT_DYNAMIC)) {
179 throw sg_exception("BasicWayptCtrl doesn't work with dynamic waypoints");
180 }
181 }
182
init()183 bool init() override
184 {
185 _targetTrack = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
186 return true;
187 }
188
update(double)189 virtual void update(double)
190 {
191 double bearingAircraftToTarget;
192
193 bearingAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
194 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(), _waypt->position());
195
196 _courseDev = bearingAircraftToTarget - _targetTrack;
197 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
198
199 if ((fabs(_courseDev) > _rnav->overflightArmAngleDeg()) && (_distanceAircraftTargetMeter < _rnav->overflightArmDistanceM())) {
200 setDone();
201 }
202 }
203
distanceToWayptM() const204 virtual double distanceToWayptM() const
205 {
206 return _distanceAircraftTargetMeter;
207 }
208
xtrackErrorNm() const209 virtual double xtrackErrorNm() const
210 {
211 double x = sin(courseDeviationDeg() * SG_DEGREES_TO_RADIANS) * _distanceAircraftTargetMeter;
212 return x * SG_METER_TO_NM;
213 }
214
toFlag() const215 virtual bool toFlag() const
216 {
217 return (fabs(_courseDev) < _rnav->overflightArmAngleDeg());
218 }
219
courseDeviationDeg() const220 virtual double courseDeviationDeg() const
221 {
222 return _courseDev;
223 }
224
trueBearingDeg() const225 virtual double trueBearingDeg() const
226 {
227 return SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
228 }
229
position() const230 virtual SGGeod position() const
231 {
232 return _waypt->position();
233 }
234
235 private:
236 double _distanceAircraftTargetMeter;
237 double _courseDev;
238 };
239
240 /**
241 * Controller for leg course interception.
242 * In leg mode, we want to intercept the leg between 2 waypoints(A->B).
243 * If we can't reach the the selected waypoint leg,we going direct to B.
244 */
245
246 class LegWayptCtl : public WayptController
247 {
248 public:
LegWayptCtl(RNAV * aRNAV,const WayptRef & aWpt)249 LegWayptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
250 WayptController(aRNAV, aWpt),
251 _waypointOrigin(),
252 _distanceOriginAircraftMetre(0.0),
253 _distanceAircraftTargetMetre(0.0),
254 _courseOriginToAircraft(0.0),
255 _courseAircraftToTarget(0.0),
256 _courseDev(0.0),
257 _toFlag(true)
258 {
259 if (aWpt->flag(WPT_DYNAMIC)) {
260 throw sg_exception("LegWayptCtl doesn't work with dynamic waypoints");
261 }
262 }
263
init()264 bool init() override
265 {
266 auto previousLeg = _rnav->previousLegData();
267 if (previousLeg) {
268 _waypointOrigin = previousLeg.value().position;
269 _initialLegCourse = SGGeodesy::courseDeg(_waypointOrigin,_waypt->position());
270 } else {
271 // capture current position
272 _waypointOrigin = _rnav->position();
273 }
274
275 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
276 _distanceAircraftTargetMetre = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
277
278
279 // check reach the leg in 45Deg or going direct
280 bool canReachLeg = (fabs(_initialLegCourse -_courseAircraftToTarget) < 45.0);
281
282 if (previousLeg && canReachLeg) {
283 _targetTrack = _initialLegCourse;
284 } else {
285 // can't reach the leg with out a crazy turn, just go direct to the
286 // destination waypt
287 _targetTrack = _courseAircraftToTarget;
288 _initialLegCourse = _courseAircraftToTarget;
289 _waypointOrigin = _rnav->position();
290 }
291
292 // turn angle depends on final leg course, not initial
293 // do this here so we have a chance of doing a fly-by at the end of the leg
294 _finalLegCourse = SGGeodesy::courseDeg(_waypt->position(), _waypointOrigin) + 180;
295 SG_NORMALIZE_RANGE(_finalLegCourse, 0.0, 360.0);
296
297 // turn-in logic
298 if (previousLeg.has_value() && previousLeg.value().didFlyBy) {
299 _flyByTurnCenter = previousLeg.value().flyByTurnCenter;
300 _flyByTurnAngle = previousLeg.value().turnAngle;
301 _flyByTurnRadius = previousLeg.value().flyByRadius;
302 _entryFlyByActive = true;
303 SG_NORMALIZE_RANGE(_flyByTurnAngle, -180.0, 180.0);
304 }
305
306 return true;
307 }
308
computeTurnAnticipation()309 void computeTurnAnticipation()
310 {
311 _didComputeTurn = true;
312
313 if (_waypt->flag(WPT_OVERFLIGHT)) {
314 return; // can't fly-by
315 }
316
317 if (!_rnav->canFlyBy())
318 return;
319
320 auto nextLegTrack = _rnav->nextLegTrack();
321
322 if (!nextLegTrack.has_value()) {
323 return;
324 }
325
326 _flyByTurnAngle = nextLegTrack.value() - _finalLegCourse;
327 SG_NORMALIZE_RANGE(_flyByTurnAngle, -180.0, 180.0);
328
329 if (fabs(_flyByTurnAngle) > 120.0) {
330 // too sharp, don't anticipate
331 return;
332 }
333
334 _flyByTurnRadius = _rnav->turnRadiusNm() * SG_NM_TO_METER;
335 _flyByTurnCenter = computeTurnCenter(_flyByTurnRadius, _waypt->position(), _finalLegCourse, _flyByTurnAngle);
336 _doFlyBy = true;
337 }
338
updateInTurn()339 bool updateInTurn()
340 {
341 // find bearing to turn center
342 // when it hits 90 off our track
343
344 auto bearingToTurnCenter = SGGeodesy::courseDeg(_rnav->position(), _flyByTurnCenter);
345 auto distToTurnCenter = SGGeodesy::distanceM(_rnav->position(), _flyByTurnCenter);
346
347 if (!_flyByStarted) {
348 // check for the turn center being 90 degrees off one wing (start the turn)
349 auto a = bearingToTurnCenter - _finalLegCourse;
350 SG_NORMALIZE_RANGE(a, -180.0, 180.0);
351 if (fabs(a) < 90.0) {
352 return false; // keep flying normal leg
353 }
354
355 _flyByStarted = true;
356 }
357
358 // check for us passing the half-way point; that's when we should
359 // sequence to the next WP
360 const auto halfPointAngle = (_finalLegCourse + (_flyByTurnAngle * 0.5));
361 auto b = bearingToTurnCenter - halfPointAngle;
362 SG_NORMALIZE_RANGE(b, -180.0, 180.0);
363
364 if (fabs(b) >= 90.0) {
365 _toFlag = false;
366 setDone();
367 }
368
369 // in the actual turn, our desired track is always pependicular to the
370 // bearing to the turn center we computed
371 _targetTrack = bearingToTurnCenter - copysign(90, _flyByTurnAngle);
372
373 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
374
375 _crossTrackError = (distToTurnCenter - _flyByTurnRadius) * SG_METER_TO_NM;
376 _courseDev = _crossTrackError * 10.0; // arbitrary guess for now
377
378 return true;
379 }
380
updateInEntryTurn()381 void updateInEntryTurn()
382 {
383 auto bearingToTurnCenter = SGGeodesy::courseDeg(_rnav->position(), _flyByTurnCenter);
384 auto distToTurnCenter = SGGeodesy::distanceM(_rnav->position(), _flyByTurnCenter);
385
386 auto b = bearingToTurnCenter - _initialLegCourse;
387
388 SG_NORMALIZE_RANGE(b, -180.0, 180.0);
389 if (fabs(b) >= 90.0) {
390 _entryFlyByActive = false;
391 return; // we're done with the entry turn
392 }
393
394 _targetTrack = bearingToTurnCenter - copysign(90, _flyByTurnAngle);
395
396 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
397 _crossTrackError = (distToTurnCenter - _flyByTurnRadius) * SG_METER_TO_NM;
398 _courseDev = _crossTrackError * 10.0; // arbitrary guess for now
399 }
400
update(double)401 void update(double) override
402 {
403 _courseOriginToAircraft = SGGeodesy::courseDeg(_waypointOrigin,_rnav->position());
404 _distanceOriginAircraftMetre = SGGeodesy::distanceM(_waypointOrigin,_rnav->position());
405
406 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
407 _distanceAircraftTargetMetre = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
408
409 if (_entryFlyByActive) {
410 updateInEntryTurn();
411 return;
412 }
413
414 if (!_didComputeTurn && (_distanceAircraftTargetMetre < turnComputeDist)) {
415 computeTurnAnticipation();
416 }
417
418 if (_didComputeTurn && _doFlyBy) {
419 bool ok = updateInTurn();
420 if (ok) {
421 return;
422 }
423
424 // otherwise we fall through
425 }
426
427 // from the Aviation Formulary
428 #if 0
429 Suppose you are proceeding on a great circle route from A to B (course =crs_AB) and end up at D,
430 perhaps off course. (We presume that A is ot a pole!) You can calculate the course from A to D
431 (crs_AD) and the distance from A to D (dist_AD) using the formulae above. In terms of these the
432 cross track error, XTD, (distance off course) is given by
433
434 XTD =asin(sin(dist_AD)*sin(crs_AD-crs_AB))
435 (positive XTD means right of course, negative means left)
436 (If the point A is the N. or S. Pole replace crs_AD-crs_AB with
437 lon_D-lon_B or lon_B-lon_D, respectively.)
438 #endif
439 // however, just for fun, our convention for polarity of the cross-track
440 // sign is opposite, so we add a -ve to the computation below.
441
442 double distOriginAircraftRad = _distanceOriginAircraftMetre * SG_METER_TO_NM * SG_NM_TO_RAD;
443 double xtkRad = asin(sin(distOriginAircraftRad) * sin((_courseOriginToAircraft - _initialLegCourse) * SG_DEGREES_TO_RADIANS));
444
445 // convert to NM and flip sign for consistency with existing code.
446 // since we derive the abeam point and course-deviation from this, and
447 // thus the GPS cdi-deflection, if we don't fix this here, the sign of
448 // all of those comes out backwards.
449 _crossTrackError = -(xtkRad * SG_RAD_TO_NM);
450
451 /*
452 The "along track distance", ATD, the distance from A along the course towards B
453 to the point abeam D is given by:
454 ATD=acos(cos(dist_AD)/cos(XTD))
455 */
456 double atd = acos(cos(distOriginAircraftRad) / cos(xtkRad));
457
458 // fix sign of ATD, if we are 'behind' the waypoint origin, rather than
459 // in front of it. We need to set a -ve sign on atd, otherwise we compute
460 // an abeamPoint ahead of the waypoint origin, potentially so far ahead
461 // that the computed track is backwards.
462 // see test-case GPSTests::testCourseLegIntermediateWaypoint
463 // for this happening
464 {
465 double x = _courseOriginToAircraft - _initialLegCourse;
466 SG_NORMALIZE_RANGE(x, -180.0, 180.0);
467 if (fabs(x) > 90.0) {
468 atd = -atd;
469 }
470 }
471
472 const double atdM = atd * SG_RAD_TO_NM * SG_NM_TO_METER;
473 SGGeod abeamPoint = SGGeodesy::direct(_waypointOrigin, _initialLegCourse, atdM);
474
475 // if we're close to the target point, we enter something similar to the VOR zone
476 // of confusion. Force target track to the final course from the origin.
477 if (_distanceAircraftTargetMetre < (SG_NM_TO_METER * 2.0)) {
478 _targetTrack = SGGeodesy::courseDeg(_waypt->position(), _waypointOrigin) + 180.0;
479 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
480 } else {
481 // use the abeam point to compute the desired course
482 double desiredCourse = SGGeodesy::courseDeg(abeamPoint, _waypt->position());
483 _targetTrack = desiredCourse;
484 }
485
486 _courseDev = _courseAircraftToTarget - _targetTrack;
487 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
488
489
490 bool isMinimumOverFlightDistanceReached = _distanceAircraftTargetMetre < _rnav->overflightDistanceM();
491 bool isOverFlightConeArmed = _distanceAircraftTargetMetre < ( _rnav->overflightArmDistanceM() + _rnav->overflightDistanceM() );
492 bool leavingOverFlightCone = (fabs(_courseDev) > _rnav->overflightArmAngleDeg());
493
494 if ( isMinimumOverFlightDistanceReached ){
495 _toFlag = false;
496 setDone();
497 } else {
498 if( isOverFlightConeArmed ){
499 _toFlag = false;
500 if ( leavingOverFlightCone ) {
501 setDone();
502 }
503 }else{
504 _toFlag = true;
505 }
506 }
507 }
508
legData() const509 simgear::optional<RNAV::LegData> legData() const override
510 {
511 RNAV::LegData r;
512 r.position = _waypt->position();
513 if (_doFlyBy) {
514 // copy all the fly by paramters, so the next controller can
515 // smoothly link up
516 r.didFlyBy = true;
517 r.flyByRadius = _flyByTurnRadius;
518 r.flyByTurnCenter = _flyByTurnCenter;
519 r.turnAngle = _flyByTurnAngle;
520 }
521
522 return r;
523 }
524
distanceToWayptM() const525 virtual double distanceToWayptM() const
526 {
527 return _distanceAircraftTargetMetre;
528 }
529
xtrackErrorNm() const530 virtual double xtrackErrorNm() const
531 {
532 return _crossTrackError;
533 }
534
toFlag() const535 virtual bool toFlag() const
536 {
537 return _toFlag;
538 }
539
courseDeviationDeg() const540 virtual double courseDeviationDeg() const
541 {
542 return _courseDev;
543 }
544
trueBearingDeg() const545 virtual double trueBearingDeg() const
546 {
547 return _courseAircraftToTarget;
548 }
549
position() const550 virtual SGGeod position() const
551 {
552 return _waypt->position();
553 }
554
555 private:
556
557 /*
558 * great circle route
559 * A(from), B(to), D(position) perhaps off course
560 */
561 SGGeod _waypointOrigin;
562 double _initialLegCourse;
563 double _finalLegCourse;
564 double _distanceOriginAircraftMetre;
565 double _distanceAircraftTargetMetre;
566 double _courseOriginToAircraft;
567 double _courseAircraftToTarget;
568 double _courseDev;
569 bool _toFlag;
570 double _crossTrackError;
571
572 bool _didComputeTurn = false;
573 bool _doFlyBy = false;
574 SGGeod _flyByTurnCenter;
575 double _flyByTurnAngle;
576 double _flyByTurnRadius;
577 bool _entryFlyByActive = false;
578 bool _flyByStarted = false;
579 };
580
581 /**
582 * Special controller for runways. For runways, we want very narrow deviation
583 * constraints, and to understand that any point along the paved area is
584 * equivalent to being 'at' the runway.
585 */
586 class RunwayCtl : public WayptController
587 {
588 public:
RunwayCtl(RNAV * aRNAV,const WayptRef & aWpt)589 RunwayCtl(RNAV* aRNAV, const WayptRef& aWpt) :
590 WayptController(aRNAV, aWpt),
591 _runway(NULL),
592 _distanceAircraftRunwayEnd(0.0),
593 _courseDev(0.0)
594 {
595 }
596
init()597 bool init() override
598 {
599 _runway = static_cast<RunwayWaypt*>(_waypt.get())->runway();
600 if (!_runway) {
601 return false;
602 }
603 _targetTrack = _runway->headingDeg();
604 return true;
605 }
606
update(double)607 virtual void update(double)
608 {
609 double bearingAircraftRunwayEnd;
610 // use the far end of the runway for course deviation calculations.
611 // this should do the correct thing both for takeoffs (including entering
612 // the runway at a taxiway after the threshold) and also landings.
613 // seperately compute the distance to the threshold for timeToWaypt calc
614
615 bearingAircraftRunwayEnd = SGGeodesy::courseDeg(_rnav->position(), _runway->end());
616 _distanceAircraftRunwayEnd = SGGeodesy::distanceM(_rnav->position(), _runway->end());
617
618 _courseDev = bearingAircraftRunwayEnd - _targetTrack;
619 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
620
621 if ((fabs(_courseDev) > _rnav->overflightArmAngleDeg()) && (_distanceAircraftRunwayEnd < _rnav->overflightArmDistanceM())) {
622 setDone();
623 }
624 }
625
distanceToWayptM() const626 virtual double distanceToWayptM() const
627 {
628 return SGGeodesy::distanceM(_rnav->position(), _runway->threshold());
629 }
630
xtrackErrorNm() const631 virtual double xtrackErrorNm() const
632 {
633 double x = sin(_courseDev * SG_DEGREES_TO_RADIANS) * _distanceAircraftRunwayEnd;
634 return x * SG_METER_TO_NM;
635 }
636
courseDeviationDeg() const637 virtual double courseDeviationDeg() const
638 {
639 return _courseDev;
640 }
641
trueBearingDeg() const642 virtual double trueBearingDeg() const
643 {
644 // as in update(), use runway->end here, so the value remains
645 // sensible whether taking off or landing.
646 return SGGeodesy::courseDeg(_rnav->position(), _runway->end());
647 }
648
position() const649 virtual SGGeod position() const
650 {
651 return _runway->threshold();
652 }
653 private:
654 FGRunway* _runway;
655 double _distanceAircraftRunwayEnd;
656 double _courseDev;
657 };
658
659 class ConstHdgToAltCtl : public WayptController
660 {
661 public:
ConstHdgToAltCtl(RNAV * aRNAV,const WayptRef & aWpt)662 ConstHdgToAltCtl(RNAV* aRNAV, const WayptRef& aWpt) :
663 WayptController(aRNAV, aWpt)
664
665 {
666 if (_waypt->type() != "hdgToAlt") {
667 throw sg_exception("invalid waypoint type", "ConstHdgToAltCtl ctor");
668 }
669
670 if (_waypt->altitudeRestriction() == RESTRICT_NONE) {
671 throw sg_exception("invalid waypoint alt restriction", "ConstHdgToAltCtl ctor");
672 }
673 }
674
init()675 bool init() override
676 {
677 HeadingToAltitude* w = (HeadingToAltitude*) _waypt.get();
678 _targetTrack = w->headingDegMagnetic() + _rnav->magvarDeg();
679 _filteredFPM = _lastFPM = _rnav->vspeedFPM();
680 return true;
681 }
682
update(double dt)683 virtual void update(double dt)
684 {
685 double curAlt = _rnav->position().getElevationFt();
686 // adjust to get a stable FPM value; bigger values mean slower
687 // response but more stable.
688 const double RESPONSIVENESS = 1.0;
689
690 _filteredFPM = fgGetLowPass(_lastFPM, _rnav->vspeedFPM(), dt * RESPONSIVENESS);
691 _lastFPM = _rnav->vspeedFPM();
692
693 switch (_waypt->altitudeRestriction()) {
694 case RESTRICT_AT:
695 case RESTRICT_COMPUTED:
696 {
697 double d = curAlt - _waypt->altitudeFt();
698 if (fabs(d) < 50.0) {
699 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, reached target altitude " << _waypt->altitudeFt());
700 setDone();
701 }
702 } break;
703
704 case RESTRICT_ABOVE:
705 if (curAlt >= _waypt->altitudeFt()) {
706 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, above target altitude " << _waypt->altitudeFt());
707 setDone();
708 }
709 break;
710
711 case RESTRICT_BELOW:
712 if (curAlt <= _waypt->altitudeFt()) {
713 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, below target altitude " << _waypt->altitudeFt());
714 setDone();
715 }
716 break;
717
718 default:
719 break;
720 }
721 }
722
timeToWaypt() const723 virtual double timeToWaypt() const
724 {
725 double d = fabs(_rnav->position().getElevationFt() - _waypt->altitudeFt());
726 return (d / _filteredFPM) * 60.0;
727 }
728
distanceToWayptM() const729 virtual double distanceToWayptM() const
730 {
731 // we could filter ground speed here, but it's likely stable enough,
732 // and timeToWaypt already filters the FPM value
733 double gsMsec = _rnav->groundSpeedKts() * SG_KT_TO_MPS;
734 return timeToWaypt() * gsMsec;
735 }
736
position() const737 virtual SGGeod position() const
738 {
739 SGGeod p;
740 double az2;
741 SGGeodesy::direct(_rnav->position(), _targetTrack, distanceToWayptM(), p, az2);
742 return p;
743 }
744
745 private:
746 double _lastFPM, _filteredFPM;
747 };
748
749 class InterceptCtl : public WayptController
750 {
751 public:
InterceptCtl(RNAV * aRNAV,const WayptRef & aWpt)752 InterceptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
753 WayptController(aRNAV, aWpt),
754 _trueRadial(0.0)
755 {
756 if (_waypt->type() != "radialIntercept") {
757 throw sg_exception("invalid waypoint type", "InterceptCtl ctor");
758 }
759 }
760
init()761 bool init() override
762 {
763 RadialIntercept* w = (RadialIntercept*) _waypt.get();
764 _trueRadial = w->radialDegMagnetic() + _rnav->magvarDeg();
765 _targetTrack = w->courseDegMagnetic() + _rnav->magvarDeg();
766
767 _canFlyBy = !_waypt->flag(WPT_OVERFLIGHT) && _rnav->canFlyBy();
768
769 return true;
770 }
771
update(double)772 void update(double) override
773 {
774 SGGeoc c,
775 geocPos = SGGeoc::fromGeod(_rnav->position()),
776 geocWayptPos = SGGeoc::fromGeod(_waypt->position());
777
778 bool ok = geocRadialIntersection(geocPos, _targetTrack,
779 geocWayptPos, _trueRadial, c);
780 if (!ok) {
781 // try with a backwards offset from the waypt pos, in case the
782 // procedure waypt location is too close. (eg, KSFO OCEAN SID)
783
784 SGGeoc navidAdjusted = SGGeodesy::advanceDegM(geocWayptPos, _trueRadial, -10 * SG_NM_TO_METER);
785 ok = geocRadialIntersection(geocPos, _targetTrack,
786 navidAdjusted, _trueRadial, c);
787 if (!ok) {
788 SG_LOG(SG_INSTR, SG_WARN, "InterceptCtl, bad intersection, skipping waypt");
789 setDone();
790 return;
791 }
792 }
793
794 _projectedPosition = SGGeod::fromGeoc(c);
795 _distanceToProjectedInterceptM = SGGeodesy::distanceM(_rnav->position(), _projectedPosition);
796 if (!_didComputeTurn && (_distanceToProjectedInterceptM < turnComputeDist)) {
797 computeTurn();
798 }
799
800 if (_doFlyBy) {
801 updateFlyByTurn();
802 }
803
804
805 // note we want the outbound radial from the waypt, hence the ordering
806 // of arguments to courseDeg
807 double r = SGGeodesy::courseDeg(_waypt->position(), _rnav->position());
808 double bearingDiff = r - _trueRadial;
809 SG_NORMALIZE_RANGE(bearingDiff, -180.0, 180.0);
810 if (fabs(bearingDiff) < 0.5) {
811 setDone();
812 }
813 }
814
updateFlyByTurn()815 bool updateFlyByTurn()
816 {
817 // find bearing to turn center
818 // when it hits 90 off our track
819
820 auto bearingToTurnCenter = SGGeodesy::courseDeg(_rnav->position(), _flyByTurnCenter);
821 auto distToTurnCenter = SGGeodesy::distanceM(_rnav->position(), _flyByTurnCenter);
822
823 if (!_flyByStarted) {
824 // check for the turn center being 90 degrees off one wing (start the turn)
825 auto a = bearingToTurnCenter - _targetTrack;
826 SG_NORMALIZE_RANGE(a, -180.0, 180.0);
827 if (fabs(a) < 90.0) {
828 return false;
829 }
830
831 _flyByStarted = true;
832 }
833
834
835 // in the actual turn, our desired track is always pependicular to the
836 // bearing to the turn center we computed
837 _targetTrack = bearingToTurnCenter - copysign(90, _flyByTurnAngle);
838
839 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
840 _crossTrackError = (distToTurnCenter - _flyByTurnRadius) * SG_METER_TO_NM;
841 return true;
842 }
843
computeTurn()844 void computeTurn()
845 {
846 _didComputeTurn = true;
847 if (!_canFlyBy)
848 return;
849
850 double inverseRadial = _trueRadial + 180.0;
851 SG_NORMALIZE_RANGE(inverseRadial, 0.0, 360.0);
852 _flyByTurnAngle = inverseRadial - _targetTrack;
853 SG_NORMALIZE_RANGE(_flyByTurnAngle, -180.0, 180.0);
854
855 if (fabs(_flyByTurnAngle) > 120.0) {
856 // too sharp, no fly-by
857 return;
858 }
859
860 _flyByTurnRadius = _rnav->turnRadiusNm() * SG_NM_TO_METER;
861 _flyByTurnCenter = computeTurnCenter(_flyByTurnRadius, _projectedPosition, _targetTrack, _flyByTurnAngle);
862 _doFlyBy = true;
863 }
864
distanceToWayptM() const865 double distanceToWayptM() const override
866 {
867 return _distanceToProjectedInterceptM;
868 }
869
position() const870 SGGeod position() const override
871 {
872 return _projectedPosition;
873 }
874
xtrackErrorNm() const875 double xtrackErrorNm() const override
876 {
877 if (!_flyByStarted)
878 return 0.0; // unless we're doing the fly-by, we're always on course
879 return _crossTrackError;
880 }
881
courseDeviationDeg() const882 double courseDeviationDeg() const override
883 {
884 // guessed scaling factor
885 return xtrackErrorNm() * 10.0;
886 }
887 private:
888 double _trueRadial;
889 SGGeod _projectedPosition;
890 bool _canFlyBy = false;
891 bool _didComputeTurn = false;
892 bool _doFlyBy = false;
893 double _distanceToProjectedInterceptM = 0.0;
894 SGGeod _flyByTurnCenter;
895 double _flyByTurnAngle;
896 double _flyByTurnRadius;
897 bool _flyByStarted = false;
898 double _crossTrackError = 0.0;
899 };
900
901 class DMEInterceptCtl : public WayptController
902 {
903 public:
DMEInterceptCtl(RNAV * aRNAV,const WayptRef & aWpt)904 DMEInterceptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
905 WayptController(aRNAV, aWpt),
906 _dme(NULL),
907 _distanceNm(0.0)
908 {
909 if (_waypt->type() != "dmeIntercept") {
910 throw sg_exception("invalid waypoint type", "DMEInterceptCtl ctor");
911 }
912 }
913
init()914 bool init() override
915 {
916 _dme = (DMEIntercept*) _waypt.get();
917 _targetTrack = _dme->courseDegMagnetic() + _rnav->magvarDeg();
918 return true;
919 }
920
update(double)921 virtual void update(double)
922 {
923 _distanceNm = SGGeodesy::distanceNm(_rnav->position(), _dme->position());
924 double d = fabs(_distanceNm - _dme->dmeDistanceNm());
925 if (d < 0.1) {
926 setDone();
927 }
928 }
929
distanceToWayptM() const930 virtual double distanceToWayptM() const
931 {
932 return fabs(_distanceNm - _dme->dmeDistanceNm()) * SG_NM_TO_METER;
933 }
934
position() const935 virtual SGGeod position() const
936 {
937 SGGeoc geocPos = SGGeoc::fromGeod(_rnav->position());
938 SGGeoc navid = SGGeoc::fromGeod(_dme->position());
939 double distRad = _dme->dmeDistanceNm() * SG_NM_TO_RAD;
940
941 // compute radial GC course
942 SGGeoc bPt = SGGeodesy::advanceDegM(geocPos, _targetTrack, 1e5);
943 double dNm = pointsKnownDistanceFromGC(geocPos, bPt, navid, distRad);
944 if (dNm < 0.0) {
945 SG_LOG(SG_AUTOPILOT, SG_WARN, "DMEInterceptCtl::position failed");
946 return _dme->position(); // horrible fallback
947 }
948
949 return SGGeodesy::direct(_rnav->position(), _targetTrack, dNm * SG_NM_TO_METER);
950 }
951
952 private:
953 DMEIntercept* _dme;
954 double _distanceNm;
955 };
956
957
HoldCtl(RNAV * aRNAV,const WayptRef & aWpt)958 HoldCtl::HoldCtl(RNAV* aRNAV, const WayptRef& aWpt) :
959 WayptController(aRNAV, aWpt)
960
961 {
962 // find published hold for aWpt
963 // do we have this?!
964 if (aWpt->type() == "hold") {
965 const auto hold = static_cast<Hold*>(aWpt.ptr());
966 _holdCourse = hold->inboundRadial();
967 if (hold->isDistance())
968 _holdLegDistance = hold->timeOrDistance();
969 else
970 _holdLegTime = hold->timeOrDistance();
971 _leftHandTurns = hold->isLeftHanded();
972 }
973 }
974
init()975 bool HoldCtl::init()
976 {
977 _segmentEnd = _waypt->position();
978 _state = LEG_TO_HOLD;
979
980 // use leg controller to fly us to the hold point - this also gives
981 // the normal legl behaviour if the hold is not enabled
982 setSubController(new LegWayptCtl(_rnav, _waypt));
983
984 return true;
985 }
986
computeEntry()987 void HoldCtl::computeEntry()
988 {
989 const double entryCourse = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
990 const double diff = SGMiscd::normalizePeriodic( -180.0, 180.0, _holdCourse - entryCourse);
991
992 if (_leftHandTurns) {
993 if ((diff > -70) && (diff < 120.0)) {
994 _state = ENTRY_DIRECT;
995 } else if (diff < 0.0) {
996 _state = ENTRY_PARALLEL;
997 } else {
998 _state = ENTRY_TEARDROP;
999 }
1000 } else {
1001 // right handed entry angles
1002 if ((diff > -120) && (diff < 70.0)) {
1003 _state = ENTRY_DIRECT;
1004 } else if (diff > 0.0) {
1005 _state = ENTRY_PARALLEL;
1006 } else {
1007 _state = ENTRY_TEARDROP;
1008 }
1009 }
1010 }
1011
update(double dt)1012 void HoldCtl::update(double dt)
1013 {
1014 const auto rnavPos = _rnav->position();
1015 const double dEnd = SGGeodesy::distanceNm(rnavPos, _segmentEnd);
1016
1017 // fly inbound / outbound sides, or execute the turn
1018 switch (_state) {
1019 case LEG_TO_HOLD:
1020 // update the leg controller
1021 _subController->update(dt);
1022 break;
1023
1024 case HOLD_EXITING:
1025 // in the common case of a hold in a procedure, we often just fly
1026 // the hold waypoint as leg. Keep running the Leg sub-controller until
1027 // it's done (WayptController::isDone calls the subcController
1028 // automaticlaly)
1029 if (_subController) {
1030 _subController->update(dt);
1031 }
1032 break;
1033
1034 default:
1035 break;
1036 }
1037
1038 if (_inTurn) {
1039 const double turnOffset = inLeftTurn() ? 90 : -90;
1040 const double bearingTurnCenter = SGGeodesy::courseDeg(rnavPos, _turnCenter);
1041 _targetTrack = bearingTurnCenter + turnOffset;
1042 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
1043
1044 const double angleToTurn = SGMiscd::normalizePeriodic( -180.0, 180.0, _targetTrack - _turnEndAngle);
1045 if (fabs(angleToTurn) < 0.5) {
1046 exitTurn();
1047 }
1048 } else if (_state == LEG_TO_HOLD) {
1049 checkInitialEntry(dEnd);
1050 } else if ((_state == HOLD_INBOUND) || (_state == ENTRY_PARALLEL_INBOUND)) {
1051 if (checkOverHold()) {
1052 // we are done
1053 } else {
1054 // straight line XTK/deviation
1055 // computed on-demand in xtrackErrorNm so nothing to do here
1056 }
1057 } else if ((_state == HOLD_OUTBOUND) || (_state == ENTRY_TEARDROP)) {
1058 if (dEnd < 0.2) {
1059 startInboundTurn();
1060 } else {
1061 // straight line XTK
1062 }
1063 } else if (_state == ENTRY_PARALLEL_OUTBOUND) {
1064 if (dEnd < 0.2) {
1065 startParallelEntryTurn(); // opposite direction to normal
1066 } else {
1067 // straight line XTK
1068 }
1069 }
1070 }
1071
setHoldCount(int count)1072 void HoldCtl::setHoldCount(int count)
1073 {
1074 _holdCount = count;
1075 }
1076
exitHold()1077 void HoldCtl::exitHold()
1078 {
1079 _holdCount = 0;
1080 }
1081
checkOverHold()1082 bool HoldCtl::checkOverHold()
1083 {
1084 // check distance to entry fix
1085 const double d = SGGeodesy::distanceNm(_rnav->position(), _waypt->position());
1086 if (d > 0.2) {
1087 return false;
1088 }
1089
1090 if (_holdCount == 0) {
1091 setDone();
1092 return true;
1093 }
1094
1095 startOutboundTurn();
1096 return true;
1097 }
1098
checkInitialEntry(double dNm)1099 void HoldCtl::checkInitialEntry(double dNm)
1100 {
1101 _turnRadius = _rnav->turnRadiusNm();
1102 if (dNm > _turnRadius) {
1103 // keep going;
1104 return;
1105 }
1106
1107 if (_holdCount == 0) {
1108 // we're done, but we want to keep the leg controller going until
1109 // we're right on top
1110 setDone();
1111
1112 // ensure we keep running the Leg cub-controller until it's done,
1113 // which happens a bit later
1114 _state = HOLD_EXITING;
1115 return;
1116 }
1117
1118 // clear the leg controller we were using to fly us to the hold
1119 setSubController(nullptr);
1120 computeEntry();
1121
1122 if (_state == ENTRY_DIRECT) {
1123 startOutboundTurn();
1124 } else if (_state == ENTRY_TEARDROP) {
1125 _targetTrack = _holdCourse + (_leftHandTurns ? -150 : 150);
1126 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
1127 _segmentEnd = outboundEndPoint();
1128 } else {
1129 // parallel entry
1130 assert(_state == ENTRY_PARALLEL);
1131 _state = ENTRY_PARALLEL_OUTBOUND;
1132 _targetTrack = _holdCourse + 180;
1133 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
1134 const double legLengthM = holdLegLengthNm() * SG_NM_TO_METER;
1135 _segmentEnd = SGGeodesy::direct(_waypt->position(), _holdCourse, -legLengthM);
1136 }
1137 }
1138
startInboundTurn()1139 void HoldCtl::startInboundTurn()
1140 {
1141 _state = HOLD_INBOUND;
1142 _inTurn = true;
1143 _turnCenter = inboundTurnCenter();
1144 _turnRadius = _rnav->turnRadiusNm();
1145 _turnEndAngle = _holdCourse;
1146 SG_NORMALIZE_RANGE(_turnEndAngle, 0.0, 360.0);
1147 }
1148
startOutboundTurn()1149 void HoldCtl::startOutboundTurn()
1150 {
1151 _state = HOLD_OUTBOUND;
1152 _inTurn = true;
1153 _turnCenter = outboundTurnCenter();
1154 _turnRadius = _rnav->turnRadiusNm();
1155 _turnEndAngle = _holdCourse + 180.0;
1156 SG_NORMALIZE_RANGE(_turnEndAngle, 0.0, 360.0);
1157 }
1158
startParallelEntryTurn()1159 void HoldCtl::startParallelEntryTurn()
1160 {
1161 _state = ENTRY_PARALLEL_INBOUND;
1162 _inTurn = true;
1163 _turnRadius = _rnav->turnRadiusNm();
1164 _turnCenter = inboundTurnCenter();
1165 _turnEndAngle = _holdCourse + (_leftHandTurns ? 45.0 : -45.0);
1166 SG_NORMALIZE_RANGE(_turnEndAngle, 0.0, 360.0);
1167 }
1168
exitTurn()1169 void HoldCtl::exitTurn()
1170 {
1171 _inTurn = false;
1172 switch (_state) {
1173 case HOLD_INBOUND:
1174 _targetTrack = _holdCourse;
1175 _segmentEnd = _waypt->position();
1176 break;
1177
1178 case ENTRY_PARALLEL_INBOUND:
1179 // possible improvement : fly the current track until the bearing tp
1180 // the hold point matches the hold radial. This would cause us to fly
1181 // a neater parallel entry, rather than flying to the hold point
1182 // direct from where we are now.
1183 _targetTrack = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
1184 _segmentEnd = _waypt->position();
1185 break;
1186
1187 case HOLD_OUTBOUND:
1188 _targetTrack = _holdCourse + 180.0;
1189 SG_NORMALIZE_RANGE(_targetTrack, 0.0, 360.0);
1190 // start a timer for timed holds?
1191 _segmentEnd = outboundEndPoint();
1192 break;
1193
1194 default:
1195 SG_LOG(SG_INSTR, SG_DEV_WARN, "HoldCOntroller: bad state at exitTurn:" << _state);
1196 }
1197 }
1198
outboundEndPoint()1199 SGGeod HoldCtl::outboundEndPoint()
1200 {
1201 // FIXME flip for left hand-turns
1202 const double turnRadiusM = _rnav->turnRadiusNm() * SG_NM_TO_METER;
1203 const double legLengthM = holdLegLengthNm() * SG_NM_TO_METER;
1204 const auto p1 = SGGeodesy::direct(_waypt->position(), _holdCourse, -legLengthM);
1205 const double turnOffset = _leftHandTurns ? -90 : 90;
1206 return SGGeodesy::direct(p1, _holdCourse + turnOffset, turnRadiusM * 2.0);
1207 }
1208
outboundTurnCenter()1209 SGGeod HoldCtl::outboundTurnCenter()
1210 {
1211 const double turnOffset = _leftHandTurns ? -90 : 90;
1212 return SGGeodesy::direct(_waypt->position(), _holdCourse + turnOffset, _rnav->turnRadiusNm() * SG_NM_TO_METER);
1213 }
1214
inboundTurnCenter()1215 SGGeod HoldCtl::inboundTurnCenter()
1216 {
1217 const double legLengthM = holdLegLengthNm() * SG_NM_TO_METER;
1218 const auto p1 = SGGeodesy::direct(_waypt->position(), _holdCourse, -legLengthM);
1219 const double turnOffset = _leftHandTurns ? -90 : 90;
1220 return SGGeodesy::direct(p1, _holdCourse + turnOffset, _rnav->turnRadiusNm() * SG_NM_TO_METER);
1221 }
1222
distanceToWayptM() const1223 double HoldCtl::distanceToWayptM() const
1224 {
1225 return -1.0;
1226 }
1227
position() const1228 SGGeod HoldCtl::position() const
1229 {
1230 return _waypt->position();
1231 }
1232
inLeftTurn() const1233 bool HoldCtl::inLeftTurn() const
1234 {
1235 return (_state == ENTRY_PARALLEL_INBOUND) ? !_leftHandTurns : _leftHandTurns;
1236 }
1237
holdLegLengthNm() const1238 double HoldCtl::holdLegLengthNm() const
1239 {
1240 const double gs = _rnav->groundSpeedKts();
1241 if (_holdLegTime > 0.0) {
1242 return _holdLegTime * gs / 3600.0;
1243 }
1244
1245 return _holdLegDistance;
1246 }
1247
xtrackErrorNm() const1248 double HoldCtl::xtrackErrorNm() const
1249 {
1250 if (_subController) {
1251 return _subController->xtrackErrorNm();
1252 }
1253
1254 if (_inTurn) {
1255 const double dR = SGGeodesy::distanceNm(_turnCenter, _rnav->position());
1256 const double xtk = dR - _turnRadius;
1257 return inLeftTurn() ? -xtk : xtk;
1258 } else {
1259 const double courseToSegmentEnd = SGGeodesy::courseDeg(_rnav->position(), _segmentEnd);
1260 const double courseDev = courseToSegmentEnd - _targetTrack;
1261 const auto d = SGGeodesy::distanceNm(_rnav->position(), _segmentEnd);
1262 return greatCircleCrossTrackError(d, courseDev);
1263 }
1264 }
1265
courseDeviationDeg() const1266 double HoldCtl::courseDeviationDeg() const
1267 {
1268 if (_subController) {
1269 return _subController->courseDeviationDeg();
1270 }
1271
1272 // convert XTK to 'dots' deviation
1273 // assuming 10-degree peg to peg, this means 0.1nm of course is
1274 // one degree of error, feels about right for a hold
1275 return xtrackErrorNm() * 10.0;
1276 }
1277
status() const1278 std::string HoldCtl::status() const
1279 {
1280 switch (_state) {
1281 case LEG_TO_HOLD: return "leg-to-hold";
1282 case HOLD_INIT: return "hold-init";
1283 case ENTRY_DIRECT: return "entry-direct";
1284 case ENTRY_PARALLEL:
1285 case ENTRY_PARALLEL_OUTBOUND:
1286 case ENTRY_PARALLEL_INBOUND:
1287 return "entry-parallel";
1288 case ENTRY_TEARDROP:
1289 return "entry-teardrop";
1290
1291 case HOLD_OUTBOUND: return "hold-outbound";
1292 case HOLD_INBOUND: return "hold-inbound";
1293 case HOLD_EXITING: return "hold-exiting";
1294 }
1295
1296 throw std::domain_error("Unsupported HoldState.");
1297 }
1298
1299 ///////////////////////////////////////////////////////////////////////////////////
1300
1301 class VectorsCtl : public WayptController
1302 {
1303 public:
VectorsCtl(RNAV * aRNAV,const WayptRef & aWpt)1304 VectorsCtl(RNAV* aRNAV, const WayptRef& aWpt) :
1305 WayptController(aRNAV, aWpt)
1306
1307 {
1308 }
1309
init()1310 bool init() override
1311 {
1312 return true;
1313 }
1314
update(double)1315 virtual void update(double)
1316 {
1317 setDone();
1318 }
1319
distanceToWayptM() const1320 virtual double distanceToWayptM() const
1321 {
1322 return -1.0;
1323 }
1324
position() const1325 virtual SGGeod position() const
1326 {
1327 return _waypt->position();
1328 }
1329
1330 private:
1331 };
1332
1333 ///////////////////////////////////////////////////////////////////////////////
1334
DirectToController(RNAV * aRNAV,const WayptRef & aWpt,const SGGeod & aOrigin)1335 DirectToController::DirectToController(RNAV* aRNAV, const WayptRef& aWpt, const SGGeod& aOrigin) :
1336 WayptController(aRNAV, aWpt),
1337 _origin(aOrigin),
1338 _distanceAircraftTargetMeter(0.0),
1339 _courseDev(0.0),
1340 _courseAircraftToTarget(0.0)
1341 {
1342 }
1343
init()1344 bool DirectToController::init()
1345 {
1346 if (_waypt->flag(WPT_DYNAMIC)) {
1347 SG_LOG(SG_AUTOPILOT, SG_WARN, "can't use a dynamic waypoint for direct-to: " << _waypt->ident());
1348 return false;
1349 }
1350
1351 _targetTrack = SGGeodesy::courseDeg(_origin, _waypt->position());
1352 return true;
1353 }
1354
update(double)1355 void DirectToController::update(double)
1356 {
1357 double courseOriginToAircraft;
1358 double az2, distanceOriginToAircraft;
1359 SGGeodesy::inverse(_origin,_rnav->position(), courseOriginToAircraft, az2, distanceOriginToAircraft);
1360 if (distanceOriginToAircraft < 100.0) {
1361 // if we are very close to the origin point, use the target track so
1362 // course deviation comes out as zero
1363 courseOriginToAircraft = _targetTrack;
1364 }
1365
1366 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
1367 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
1368
1369 _courseDev = _courseAircraftToTarget - _targetTrack;
1370 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
1371
1372 bool isMinimumOverFlightDistanceReached = _distanceAircraftTargetMeter < _rnav->overflightDistanceM();
1373 bool isOverFlightConeArmed = _distanceAircraftTargetMeter < ( _rnav->overflightArmDistanceM() + _rnav->overflightDistanceM() );
1374 bool leavingOverFlightCone = (fabs(_courseAircraftToTarget - _targetTrack) > _rnav->overflightArmAngleDeg());
1375
1376 if( isMinimumOverFlightDistanceReached ){
1377 setDone();
1378 }else{
1379 if( isOverFlightConeArmed && leavingOverFlightCone ){
1380 setDone();
1381 }
1382 }
1383 }
1384
distanceToWayptM() const1385 double DirectToController::distanceToWayptM() const
1386 {
1387 return _distanceAircraftTargetMeter;
1388 }
1389
xtrackErrorNm() const1390 double DirectToController::xtrackErrorNm() const
1391 {
1392 return greatCircleCrossTrackError(_distanceAircraftTargetMeter * SG_METER_TO_NM, _courseDev);
1393 }
1394
courseDeviationDeg() const1395 double DirectToController::courseDeviationDeg() const
1396 {
1397 return _courseDev;
1398 }
1399
trueBearingDeg() const1400 double DirectToController::trueBearingDeg() const
1401 {
1402 return _courseAircraftToTarget;
1403 }
1404
position() const1405 SGGeod DirectToController::position() const
1406 {
1407 return _waypt->position();
1408 }
1409
1410 ///////////////////////////////////////////////////////////////////////////////
1411
OBSController(RNAV * aRNAV,const WayptRef & aWpt)1412 OBSController::OBSController(RNAV* aRNAV, const WayptRef& aWpt) :
1413 WayptController(aRNAV, aWpt),
1414 _distanceAircraftTargetMeter(0.0),
1415 _courseDev(0.0),
1416 _courseAircraftToTarget(0.0)
1417 {
1418 }
1419
init()1420 bool OBSController::init()
1421 {
1422 if (_waypt->flag(WPT_DYNAMIC)) {
1423 SG_LOG(SG_AUTOPILOT, SG_WARN, "can't use a dynamic waypoint for OBS mode" << _waypt->ident());
1424 return false;
1425 }
1426
1427 _targetTrack = _rnav->selectedMagCourse() + _rnav->magvarDeg();
1428 return true;
1429 }
1430
update(double)1431 void OBSController::update(double)
1432 {
1433 _targetTrack = _rnav->selectedMagCourse() + _rnav->magvarDeg();
1434
1435 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
1436 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
1437
1438 // _courseDev inverted we use point target as origin
1439 _courseDev = (_courseAircraftToTarget - _targetTrack);
1440 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
1441 }
1442
toFlag() const1443 bool OBSController::toFlag() const
1444 {
1445 return (fabs(_courseDev) < _rnav->overflightArmAngleDeg());
1446 }
1447
distanceToWayptM() const1448 double OBSController::distanceToWayptM() const
1449 {
1450 return _distanceAircraftTargetMeter;
1451 }
1452
xtrackErrorNm() const1453 double OBSController::xtrackErrorNm() const
1454 {
1455 return greatCircleCrossTrackError(_distanceAircraftTargetMeter * SG_METER_TO_NM, _courseDev);
1456 }
1457
courseDeviationDeg() const1458 double OBSController::courseDeviationDeg() const
1459 {
1460 // if (fabs(_courseDev) > 90.0) {
1461 // double d = -_courseDev;
1462 // SG_NORMALIZE_RANGE(d, -90.0, 90.0);
1463 // return d;
1464 //}
1465
1466 return _courseDev;
1467 }
1468
trueBearingDeg() const1469 double OBSController::trueBearingDeg() const
1470 {
1471 return _courseAircraftToTarget;
1472 }
1473
position() const1474 SGGeod OBSController::position() const
1475 {
1476 return _waypt->position();
1477 }
1478
1479 ///////////////////////////////////////////////////////////////////////////////
1480
createForWaypt(RNAV * aRNAV,const WayptRef & aWpt)1481 WayptController* WayptController::createForWaypt(RNAV* aRNAV, const WayptRef& aWpt)
1482 {
1483 if (!aWpt) {
1484 throw sg_exception("Passed null waypt", "WayptController::createForWaypt");
1485 }
1486
1487 const std::string& wty(aWpt->type());
1488 if (wty == "runway") {
1489 return new RunwayCtl(aRNAV, aWpt);
1490 }
1491
1492 if (wty == "radialIntercept") {
1493 return new InterceptCtl(aRNAV, aWpt);
1494 }
1495
1496 if (wty == "dmeIntercept") {
1497 return new DMEInterceptCtl(aRNAV, aWpt);
1498 }
1499
1500 if (wty == "hdgToAlt") {
1501 return new ConstHdgToAltCtl(aRNAV, aWpt);
1502 }
1503
1504 if (wty == "vectors") {
1505 return new VectorsCtl(aRNAV, aWpt);
1506 }
1507
1508 if (wty == "hold") {
1509 return new HoldCtl(aRNAV, aWpt);
1510 }
1511
1512 return new LegWayptCtl(aRNAV, aWpt);
1513 }
1514
1515 } // of namespace flightgear
1516
1517