1 // rnav_waypt_controller.cxx - Waypoint-specific behaviours for RNAV systems
2 // Written by James Turner, started 2009.
4 // Copyright (C) 2009 Curtis L. Olson
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.
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.
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.
20 #include "rnav_waypt_controller.hxx"
24 #include <simgear/sg_inlines.h>
25 #include <simgear/structure/exception.hxx>
27 #include <Airports/runways.hxx>
32 const double KNOTS_TO_METRES_PER_SECOND = SG_NM_TO_METER / 3600.0;
34 double pmod(double x, double y)
44 // http://williams.best.vwh.net/avform.htm#Intersection
45 bool geocRadialIntersection(const SGGeoc& a, double r1, const SGGeoc& b, double r2, SGGeoc& result)
47 double crs13 = r1 * SG_DEGREES_TO_RADIANS;
48 double crs23 = r2 * SG_DEGREES_TO_RADIANS;
49 double dst12 = SGGeodesy::distanceRad(a, b);
52 // crs12=acos((sin(lat2)-sin(lat1)*cos(dst12))/(sin(dst12)*cos(lat1)))
53 // crs21=2.*pi-acos((sin(lat1)-sin(lat2)*cos(dst12))/(sin(dst12)*cos(lat2)))
55 // crs12=2.*pi-acos((sin(lat2)-sin(lat1)*cos(dst12))/(sin(dst12)*cos(lat1)))
56 // crs21=acos((sin(lat1)-sin(lat2)*cos(dst12))/(sin(dst12)*cos(lat2)))
60 // double diffLon = b.getLongitudeRad() - a.getLongitudeRad();
62 double sinLat1 = sin(a.getLatitudeRad());
63 double cosLat1 = cos(a.getLatitudeRad());
64 // double sinLat2 = sin(b.getLatitudeRad());
65 //double cosLat2 = cos(b.getLatitudeRad());
66 double sinDst12 = sin(dst12);
67 double cosDst12 = cos(dst12);
69 double crs12 = SGGeodesy::courseRad(a, b),
70 crs21 = SGGeodesy::courseRad(b, a);
72 //double degCrs12 = crs12 * SG_RADIANS_TO_DEGREES;
73 //double degCrs21 = crs21 * SG_RADIANS_TO_DEGREES;
76 if (sin(diffLon) < 0.0) {
77 crs12 = acos((sinLat2 - sinLat1 * cosDst12) / (sinDst12 * cosLat1));
78 crs21 = SGMiscd::twopi() - acos((sinLat1 - sinLat2*cosDst12)/(sinDst12*cosLat2));
80 crs12 = SGMiscd::twopi() - acos((sinLat2 - sinLat1 * cosDst12)/(sinDst12 * cosLat1));
81 crs21 = acos((sinLat1 - sinLat2 * cosDst12)/(sinDst12 * cosLat2));
85 double ang1 = SGMiscd::normalizeAngle2(crs13-crs12);
86 double ang2 = SGMiscd::normalizeAngle2(crs21-crs23);
88 if ((sin(ang1) == 0.0) && (sin(ang2) == 0.0)) {
89 SG_LOG(SG_INSTR, SG_WARN, "geocRadialIntersection: infinity of intersections");
93 if ((sin(ang1)*sin(ang2))<0.0) {
94 SG_LOG(SG_INSTR, SG_WARN, "geocRadialIntersection: intersection ambiguous");
101 //ang3=acos(-cos(ang1)*cos(ang2)+sin(ang1)*sin(ang2)*cos(dst12))
102 //dst13=atan2(sin(dst12)*sin(ang1)*sin(ang2),cos(ang2)+cos(ang1)*cos(ang3))
103 //lat3=asin(sin(lat1)*cos(dst13)+cos(lat1)*sin(dst13)*cos(crs13))
105 //lon3=mod(lon1-dlon+pi,2*pi)-pi
107 double ang3 = acos(-cos(ang1) * cos(ang2) + sin(ang1) * sin(ang2) * cosDst12);
108 double dst13 = atan2(sinDst12 * sin(ang1) * sin(ang2), cos(ang2) + cos(ang1)*cos(ang3));
111 SGGeodesy::advanceRadM(a, crs13, dst13 * SG_RAD_TO_NM * SG_NM_TO_METER, pt3);
113 double lat3 = asin(sinLat1 * cos(dst13) + cosLat1 * sin(dst13) * cos(crs13));
115 //dlon=atan2(sin(crs13)*sin(dst13)*cos(lat1),cos(dst13)-sin(lat1)*sin(lat3))
116 double dlon = atan2(sin(crs13)*sin(dst13)*cosLat1, cos(dst13)- (sinLat1 * sin(lat3)));
117 double lon3 = SGMiscd::normalizeAngle(-a.getLongitudeRad()-dlon);
119 result = SGGeoc::fromRadM(-lon3, lat3, a.getRadiusM());
126 * Helper function Cross track error:
127 * http://williams.best.vwh.net/avform.htm#XTE
129 * param distanceOriginToPosition in Nautical Miles
130 * param courseDev difference between courseOriginToAircraft(AD) and courseOriginToTarget(AB)
131 * return XTE in Nautical Miles
135 * D(aircraft) perhaps off course
143 double greatCircleCrossTrackError(double distanceOriginToPosition,double courseDev){
144 double crossTrackError = asin(
145 sin(distanceOriginToPosition * SG_NM_TO_RAD) *
146 sin(courseDev * SG_DEGREES_TO_RADIANS)
148 return crossTrackError * SG_RAD_TO_NM;
152 ////////////////////////////////////////////////////////////////////////////
154 WayptController::~WayptController()
158 void WayptController::init()
162 void WayptController::setDone()
165 SG_LOG(SG_AUTOPILOT, SG_WARN, "already done @ WayptController::setDone");
171 double WayptController::timeToWaypt() const
173 double gs = _rnav->groundSpeedKts();
175 return -1.0; // stationary
178 gs*= KNOTS_TO_METRES_PER_SECOND;
179 return (distanceToWayptM() / gs);
184 class BasicWayptCtl : public WayptController
187 BasicWayptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
188 WayptController(aRNAV, aWpt),
189 _distanceAircraftTargetMeter(0.0),
192 if (aWpt->flag(WPT_DYNAMIC)) {
193 throw sg_exception("BasicWayptCtrl doesn't work with dynamic waypoints");
199 _targetTrack = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
202 virtual void update()
204 double bearingAircraftToTarget;
206 bearingAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
207 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(), _waypt->position());
209 _courseDev = bearingAircraftToTarget - _targetTrack;
210 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
212 if ((fabs(_courseDev) > _rnav->overflightArmAngleDeg()) && (_distanceAircraftTargetMeter < _rnav->overflightArmDistanceM())) {
217 virtual double distanceToWayptM() const
219 return _distanceAircraftTargetMeter;
222 virtual double xtrackErrorNm() const
224 double x = sin(courseDeviationDeg() * SG_DEGREES_TO_RADIANS) * _distanceAircraftTargetMeter;
225 return x * SG_METER_TO_NM;
228 virtual bool toFlag() const
230 return (fabs(_courseDev) < _rnav->overflightArmAngleDeg());
233 virtual double courseDeviationDeg() const
238 virtual double trueBearingDeg() const
240 return SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
243 virtual SGGeod position() const
245 return _waypt->position();
249 double _distanceAircraftTargetMeter;
254 * Controller for leg course interception.
255 * In leg mode, we want to intercept the leg between 2 waypoints(A->B).
256 * If we can't reach the the selected waypoint leg,we going direct to B.
259 class LegWayptCtl : public WayptController
262 LegWayptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
263 WayptController(aRNAV, aWpt),
265 _distanceOriginTargetMeter(0.0),
266 _distanceOriginAircraftMeter(0.0),
267 _distanceAircraftTargetMeter(0.0),
268 _courseOriginToAircraft(0.0),
269 _courseAircraftToTarget(0.0),
273 if (aWpt->flag(WPT_DYNAMIC)) {
274 throw sg_exception("LegWayptCtl doesn't work with dynamic waypoints");
280 double courseOriginTarget;
281 bool isPreviousLegValid = false;
283 _waypointOrigin = _rnav->previousLegWaypointPosition(isPreviousLegValid);
285 courseOriginTarget = SGGeodesy::courseDeg(_waypointOrigin,_waypt->position());
287 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
288 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
291 // check reach the leg in 45Deg or going direct
292 bool canReachLeg = (fabs(courseOriginTarget -_courseAircraftToTarget) < 45.0);
294 if ( isPreviousLegValid && canReachLeg){
295 _targetTrack = courseOriginTarget;
297 _targetTrack = _courseAircraftToTarget;
298 _waypointOrigin = _rnav->position();
303 virtual void update()
305 _courseOriginToAircraft = SGGeodesy::courseDeg(_waypointOrigin,_rnav->position());
306 _distanceOriginAircraftMeter = SGGeodesy::distanceM(_waypointOrigin,_rnav->position());
308 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
309 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
311 _courseDev = -(_courseOriginToAircraft - _targetTrack);
313 bool isMinimumOverFlightDistanceReached = _distanceAircraftTargetMeter < _rnav->overflightDistanceM();
314 bool isOverFlightConeArmed = _distanceAircraftTargetMeter < ( _rnav->overflightArmDistanceM() + _rnav->overflightDistanceM() );
315 bool leavingOverFlightCone = (fabs(_courseAircraftToTarget - _targetTrack) > _rnav->overflightArmAngleDeg());
317 if( isMinimumOverFlightDistanceReached ){
321 if( isOverFlightConeArmed ){
323 if ( leavingOverFlightCone ) {
332 virtual double distanceToWayptM() const
334 return _distanceAircraftTargetMeter;
337 virtual double xtrackErrorNm() const
339 return greatCircleCrossTrackError(_distanceOriginAircraftMeter * SG_METER_TO_NM, _courseDev);
342 virtual bool toFlag() const
347 virtual double courseDeviationDeg() const
352 virtual double trueBearingDeg() const
354 return _courseAircraftToTarget;
357 virtual SGGeod position() const
359 return _waypt->position();
366 * A(from), B(to), D(position) perhaps off course
368 SGGeod _waypointOrigin;
369 double _distanceOriginTargetMeter;
370 double _distanceOriginAircraftMeter;
371 double _distanceAircraftTargetMeter;
372 double _courseOriginToAircraft;
373 double _courseAircraftToTarget;
380 * Special controller for runways. For runways, we want very narrow deviation
381 * constraints, and to understand that any point along the paved area is
382 * equivalent to being 'at' the runway.
384 class RunwayCtl : public WayptController
387 RunwayCtl(RNAV* aRNAV, const WayptRef& aWpt) :
388 WayptController(aRNAV, aWpt),
390 _distanceAircraftRunwayEnd(0.0),
397 _runway = static_cast<RunwayWaypt*>(_waypt.get())->runway();
398 _targetTrack = _runway->headingDeg();
401 virtual void update()
403 double bearingAircraftRunwayEnd;
404 // use the far end of the runway for course deviation calculations.
405 // this should do the correct thing both for takeoffs (including entering
406 // the runway at a taxiway after the threshold) and also landings.
407 // seperately compute the distance to the threshold for timeToWaypt calc
409 bearingAircraftRunwayEnd = SGGeodesy::courseDeg(_rnav->position(), _runway->end());
410 _distanceAircraftRunwayEnd = SGGeodesy::distanceM(_rnav->position(), _runway->end());
412 double _courseDev = bearingAircraftRunwayEnd - _targetTrack;
413 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
415 if ((fabs(_courseDev) > _rnav->overflightArmAngleDeg()) && (_distanceAircraftRunwayEnd < _rnav->overflightArmDistanceM())) {
420 virtual double distanceToWayptM() const
422 return SGGeodesy::distanceM(_rnav->position(), _runway->threshold());
425 virtual double xtrackErrorNm() const
427 double x = sin(_courseDev * SG_RADIANS_TO_DEGREES) * _distanceAircraftRunwayEnd;
428 return x * SG_METER_TO_NM;
431 virtual double courseDeviationDeg() const
436 virtual double trueBearingDeg() const
438 // as in update(), use runway->end here, so the value remains
439 // sensible whether taking off or landing.
440 return SGGeodesy::courseDeg(_rnav->position(), _runway->end());
443 virtual SGGeod position() const
445 return _runway->threshold();
449 double _distanceAircraftRunwayEnd;
453 class ConstHdgToAltCtl : public WayptController
456 ConstHdgToAltCtl(RNAV* aRNAV, const WayptRef& aWpt) :
457 WayptController(aRNAV, aWpt)
460 if (_waypt->type() != "hdgToAlt") {
461 throw sg_exception("invalid waypoint type", "ConstHdgToAltCtl ctor");
464 if (_waypt->altitudeRestriction() == RESTRICT_NONE) {
465 throw sg_exception("invalid waypoint alt restriction", "ConstHdgToAltCtl ctor");
471 HeadingToAltitude* w = (HeadingToAltitude*) _waypt.get();
472 _targetTrack = w->headingDegMagnetic() + _rnav->magvarDeg();
475 virtual void update()
477 double curAlt = _rnav->position().getElevationFt();
479 switch (_waypt->altitudeRestriction()) {
481 case RESTRICT_COMPUTED:
483 double d = curAlt - _waypt->altitudeFt();
484 if (fabs(d) < 50.0) {
485 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, reached target altitude " << _waypt->altitudeFt());
491 if (curAlt >= _waypt->altitudeFt()) {
492 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, above target altitude " << _waypt->altitudeFt());
498 if (curAlt <= _waypt->altitudeFt()) {
499 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, below target altitude " << _waypt->altitudeFt());
509 virtual double timeToWaypt() const
511 double d = fabs(_rnav->position().getElevationFt() - _waypt->altitudeFt());
512 return (d / _rnav->vspeedFPM()) * 60.0; // low pass filter here, probably
515 virtual double distanceToWayptM() const
517 double gsMsec = _rnav->groundSpeedKts() * KNOTS_TO_METRES_PER_SECOND;
518 return timeToWaypt() * gsMsec;
521 virtual SGGeod position() const
525 SGGeodesy::direct(_rnav->position(), _targetTrack, distanceToWayptM(), p, az2);
530 class InterceptCtl : public WayptController
533 InterceptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
534 WayptController(aRNAV, aWpt),
537 if (_waypt->type() != "radialIntercept") {
538 throw sg_exception("invalid waypoint type", "InterceptCtl ctor");
544 RadialIntercept* w = (RadialIntercept*) _waypt.get();
545 _trueRadial = w->radialDegMagnetic() + _rnav->magvarDeg();
546 _targetTrack = w->courseDegMagnetic() + _rnav->magvarDeg();
549 virtual void update()
551 // note we want the outbound radial from the waypt, hence the ordering
552 // of arguments to courseDeg
553 double r = SGGeodesy::courseDeg(_waypt->position(), _rnav->position());
554 SG_LOG(SG_AUTOPILOT, SG_INFO, "current radial=" << r);
555 if (fabs(r - _trueRadial) < 0.5) {
556 SG_LOG(SG_INSTR, SG_INFO, "InterceptCtl, intercepted radial " << _trueRadial);
561 virtual double distanceToWayptM() const
563 return SGGeodesy::distanceM(_rnav->position(), position());
566 virtual SGGeod position() const
569 geocRadialIntersection(SGGeoc::fromGeod(_rnav->position()), _rnav->trackDeg(),
570 SGGeoc::fromGeod(_waypt->position()), _trueRadial, c);
571 return SGGeod::fromGeoc(c);
577 class DMEInterceptCtl : public WayptController
580 DMEInterceptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
581 WayptController(aRNAV, aWpt),
585 if (_waypt->type() != "dmeIntercept") {
586 throw sg_exception("invalid waypoint type", "DMEInterceptCtl ctor");
592 _dme = (DMEIntercept*) _waypt.get();
593 _targetTrack = _dme->courseDegMagnetic() + _rnav->magvarDeg();
596 virtual void update()
598 _distanceNm = SGGeodesy::distanceNm(_rnav->position(), _dme->position());
599 double d = fabs(_distanceNm - _dme->dmeDistanceNm());
601 SG_LOG(SG_INSTR, SG_INFO, "DMEInterceptCtl, intercepted DME " << _dme->dmeDistanceNm());
606 virtual double distanceToWayptM() const
608 return fabs(_distanceNm - _dme->dmeDistanceNm()) * SG_NM_TO_METER;
611 virtual SGGeod position() const
615 SGGeodesy::direct(_rnav->position(), _targetTrack, distanceToWayptM(), p, az2);
624 class HoldCtl : public WayptController
627 HoldCtl(RNAV* aRNAV, const WayptRef& aWpt) :
628 WayptController(aRNAV, aWpt)
638 virtual void update()
640 // fly inbound / outbound sides, or execute the turn
644 targetTrack += dt * turnRateSec * turnDirection;
646 if .. targetTrack has passed inbound radial, doen with this turn
648 if target track has passed reciprocal radial done with turn
651 check time / distance elapsed
656 nextHeading = inbound;
658 nextHeading += 180.0;
659 SG_NORMALIZE_RANGE(nextHeading, 0.0, 360.0);
669 virtual double distanceToWayptM() const
674 virtual SGGeod position() const
676 return _waypt->position();
680 class VectorsCtl : public WayptController
683 VectorsCtl(RNAV* aRNAV, const WayptRef& aWpt) :
684 WayptController(aRNAV, aWpt)
694 virtual void update()
699 virtual double distanceToWayptM() const
704 virtual SGGeod position() const
706 return _waypt->position();
712 ///////////////////////////////////////////////////////////////////////////////
714 DirectToController::DirectToController(RNAV* aRNAV, const WayptRef& aWpt, const SGGeod& aOrigin) :
715 WayptController(aRNAV, aWpt),
717 _distanceAircraftTargetMeter(0.0),
719 _courseAircraftToTarget(0.0)
723 void DirectToController::init()
725 if (_waypt->flag(WPT_DYNAMIC)) {
726 throw sg_exception("can't direct-to a dynamic waypoint");
729 _targetTrack = SGGeodesy::courseDeg(_origin, _waypt->position());
732 void DirectToController::update()
734 double courseOriginToAircraft;
736 courseOriginToAircraft = SGGeodesy::courseDeg(_origin,_rnav->position());
738 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
739 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
741 _courseDev = -(courseOriginToAircraft - _targetTrack);
743 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
745 bool isMinimumOverFlightDistanceReached = _distanceAircraftTargetMeter < _rnav->overflightDistanceM();
746 bool isOverFlightConeArmed = _distanceAircraftTargetMeter < ( _rnav->overflightArmDistanceM() + _rnav->overflightDistanceM() );
747 bool leavingOverFlightCone = (fabs(_courseAircraftToTarget - _targetTrack) > _rnav->overflightArmAngleDeg());
749 if( isMinimumOverFlightDistanceReached ){
752 if( isOverFlightConeArmed && leavingOverFlightCone ){
758 double DirectToController::distanceToWayptM() const
760 return _distanceAircraftTargetMeter;
763 double DirectToController::xtrackErrorNm() const
765 return greatCircleCrossTrackError(_distanceAircraftTargetMeter * SG_METER_TO_NM, _courseDev);
768 double DirectToController::courseDeviationDeg() const
773 double DirectToController::trueBearingDeg() const
775 return _courseAircraftToTarget;
778 SGGeod DirectToController::position() const
780 return _waypt->position();
783 ///////////////////////////////////////////////////////////////////////////////
785 OBSController::OBSController(RNAV* aRNAV, const WayptRef& aWpt) :
786 WayptController(aRNAV, aWpt),
787 _distanceAircraftTargetMeter(0.0),
789 _courseAircraftToTarget(0.0)
793 void OBSController::init()
795 if (_waypt->flag(WPT_DYNAMIC)) {
796 throw sg_exception("can't use a dynamic waypoint for OBS mode");
799 _targetTrack = _rnav->selectedMagCourse() + _rnav->magvarDeg();
802 void OBSController::update()
804 _targetTrack = _rnav->selectedMagCourse() + _rnav->magvarDeg();
806 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
807 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
809 // _courseDev inverted we use point target as origin
810 _courseDev = (_courseAircraftToTarget - _targetTrack);
811 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
814 bool OBSController::toFlag() const
816 return (fabs(_courseDev) < _rnav->overflightArmAngleDeg());
819 double OBSController::distanceToWayptM() const
821 return _distanceAircraftTargetMeter;
824 double OBSController::xtrackErrorNm() const
826 return greatCircleCrossTrackError(_distanceAircraftTargetMeter * SG_METER_TO_NM, _courseDev);
829 double OBSController::courseDeviationDeg() const
831 // if (fabs(_courseDev) > 90.0) {
832 // double d = -_courseDev;
833 // SG_NORMALIZE_RANGE(d, -90.0, 90.0);
840 double OBSController::trueBearingDeg() const
842 return _courseAircraftToTarget;
845 SGGeod OBSController::position() const
847 return _waypt->position();
850 ///////////////////////////////////////////////////////////////////////////////
852 WayptController* WayptController::createForWaypt(RNAV* aRNAV, const WayptRef& aWpt)
855 throw sg_exception("Passed null waypt", "WayptController::createForWaypt");
858 const std::string& wty(aWpt->type());
859 if (wty == "runway") {
860 return new RunwayCtl(aRNAV, aWpt);
863 if (wty == "radialIntercept") {
864 return new InterceptCtl(aRNAV, aWpt);
867 if (wty == "dmeIntercept") {
868 return new DMEInterceptCtl(aRNAV, aWpt);
871 if (wty == "hdgToAlt") {
872 return new ConstHdgToAltCtl(aRNAV, aWpt);
875 if (wty == "vectors") {
876 return new VectorsCtl(aRNAV, aWpt);
880 return new HoldCtl(aRNAV, aWpt);
883 return new LegWayptCtl(aRNAV, aWpt);
886 } // of namespace flightgear