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