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>
28 #include "Main/util.hxx" // for fgLowPass
30 extern double pointsKnownDistanceFromGC(const SGGeoc& a, const SGGeoc&b, const SGGeoc& d, double dist);
36 // http://williams.best.vwh.net/avform.htm#Intersection
37 bool geocRadialIntersection(const SGGeoc& a, double r1, const SGGeoc& b, double r2, SGGeoc& result)
39 double crs13 = r1 * SG_DEGREES_TO_RADIANS;
40 double crs23 = r2 * SG_DEGREES_TO_RADIANS;
41 double dst12 = SGGeodesy::distanceRad(a, b);
44 // crs12=acos((sin(lat2)-sin(lat1)*cos(dst12))/(sin(dst12)*cos(lat1)))
45 // crs21=2.*pi-acos((sin(lat1)-sin(lat2)*cos(dst12))/(sin(dst12)*cos(lat2)))
47 // crs12=2.*pi-acos((sin(lat2)-sin(lat1)*cos(dst12))/(sin(dst12)*cos(lat1)))
48 // crs21=acos((sin(lat1)-sin(lat2)*cos(dst12))/(sin(dst12)*cos(lat2)))
51 double sinLat1 = sin(a.getLatitudeRad());
52 double cosLat1 = cos(a.getLatitudeRad());
53 double sinDst12 = sin(dst12);
54 double cosDst12 = cos(dst12);
56 double crs12 = SGGeodesy::courseRad(a, b),
57 crs21 = SGGeodesy::courseRad(b, a);
60 // normalise to -pi .. pi range
61 double ang1 = SGMiscd::normalizeAngle(crs13-crs12);
62 double ang2 = SGMiscd::normalizeAngle(crs21-crs23);
64 if ((sin(ang1) == 0.0) && (sin(ang2) == 0.0)) {
65 SG_LOG(SG_INSTR, SG_INFO, "geocRadialIntersection: infinity of intersections");
69 if ((sin(ang1)*sin(ang2))<0.0) {
70 SG_LOG(SG_INSTR, SG_INFO, "geocRadialIntersection: intersection ambiguous:"
71 << ang1 << " " << ang2 << " sin1 " << sin(ang1) << " sin2 " << sin(ang2));
78 //ang3=acos(-cos(ang1)*cos(ang2)+sin(ang1)*sin(ang2)*cos(dst12))
79 //dst13=atan2(sin(dst12)*sin(ang1)*sin(ang2),cos(ang2)+cos(ang1)*cos(ang3))
80 //lat3=asin(sin(lat1)*cos(dst13)+cos(lat1)*sin(dst13)*cos(crs13))
82 //lon3=mod(lon1-dlon+pi,2*pi)-pi
84 double ang3 = acos(-cos(ang1) * cos(ang2) + sin(ang1) * sin(ang2) * cosDst12);
85 double dst13 = atan2(sinDst12 * sin(ang1) * sin(ang2), cos(ang2) + cos(ang1)*cos(ang3));
88 SGGeodesy::advanceRadM(a, crs13, dst13 * SG_RAD_TO_NM * SG_NM_TO_METER, pt3);
90 double lat3 = asin(sinLat1 * cos(dst13) + cosLat1 * sin(dst13) * cos(crs13));
92 //dlon=atan2(sin(crs13)*sin(dst13)*cos(lat1),cos(dst13)-sin(lat1)*sin(lat3))
93 double dlon = atan2(sin(crs13)*sin(dst13)*cosLat1, cos(dst13)- (sinLat1 * sin(lat3)));
94 double lon3 = SGMiscd::normalizeAngle(-a.getLongitudeRad()-dlon);
96 result = SGGeoc::fromRadM(-lon3, lat3, a.getRadiusM());
102 * Helper function Cross track error:
103 * http://williams.best.vwh.net/avform.htm#XTE
105 * param distanceOriginToPosition in Nautical Miles
106 * param courseDev difference between courseOriginToAircraft(AD) and courseOriginToTarget(AB)
107 * return XTE in Nautical Miles
111 * D(aircraft) perhaps off course
119 double greatCircleCrossTrackError(double distanceOriginToPosition,double courseDev){
120 double crossTrackError = asin(
121 sin(distanceOriginToPosition * SG_NM_TO_RAD) *
122 sin(courseDev * SG_DEGREES_TO_RADIANS)
124 return crossTrackError * SG_RAD_TO_NM;
128 ////////////////////////////////////////////////////////////////////////////
130 WayptController::~WayptController()
134 void WayptController::init()
138 void WayptController::setDone()
141 SG_LOG(SG_AUTOPILOT, SG_WARN, "already done @ WayptController::setDone");
147 double WayptController::timeToWaypt() const
149 double gs = _rnav->groundSpeedKts();
151 return -1.0; // stationary
155 return (distanceToWayptM() / gs);
160 class BasicWayptCtl : public WayptController
163 BasicWayptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
164 WayptController(aRNAV, aWpt),
165 _distanceAircraftTargetMeter(0.0),
168 if (aWpt->flag(WPT_DYNAMIC)) {
169 throw sg_exception("BasicWayptCtrl doesn't work with dynamic waypoints");
175 _targetTrack = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
178 virtual void update(double)
180 double bearingAircraftToTarget;
182 bearingAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
183 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(), _waypt->position());
185 _courseDev = bearingAircraftToTarget - _targetTrack;
186 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
188 if ((fabs(_courseDev) > _rnav->overflightArmAngleDeg()) && (_distanceAircraftTargetMeter < _rnav->overflightArmDistanceM())) {
193 virtual double distanceToWayptM() const
195 return _distanceAircraftTargetMeter;
198 virtual double xtrackErrorNm() const
200 double x = sin(courseDeviationDeg() * SG_DEGREES_TO_RADIANS) * _distanceAircraftTargetMeter;
201 return x * SG_METER_TO_NM;
204 virtual bool toFlag() const
206 return (fabs(_courseDev) < _rnav->overflightArmAngleDeg());
209 virtual double courseDeviationDeg() const
214 virtual double trueBearingDeg() const
216 return SGGeodesy::courseDeg(_rnav->position(), _waypt->position());
219 virtual SGGeod position() const
221 return _waypt->position();
225 double _distanceAircraftTargetMeter;
230 * Controller for leg course interception.
231 * In leg mode, we want to intercept the leg between 2 waypoints(A->B).
232 * If we can't reach the the selected waypoint leg,we going direct to B.
235 class LegWayptCtl : public WayptController
238 LegWayptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
239 WayptController(aRNAV, aWpt),
241 _distanceOriginAircraftMeter(0.0),
242 _distanceAircraftTargetMeter(0.0),
243 _courseOriginToAircraft(0.0),
244 _courseAircraftToTarget(0.0),
248 if (aWpt->flag(WPT_DYNAMIC)) {
249 throw sg_exception("LegWayptCtl doesn't work with dynamic waypoints");
255 double courseOriginTarget;
256 bool isPreviousLegValid = false;
258 _waypointOrigin = _rnav->previousLegWaypointPosition(isPreviousLegValid);
260 courseOriginTarget = SGGeodesy::courseDeg(_waypointOrigin,_waypt->position());
262 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
263 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
266 // check reach the leg in 45Deg or going direct
267 bool canReachLeg = (fabs(courseOriginTarget -_courseAircraftToTarget) < 45.0);
269 if ( isPreviousLegValid && canReachLeg){
270 _targetTrack = courseOriginTarget;
272 _targetTrack = _courseAircraftToTarget;
273 _waypointOrigin = _rnav->position();
278 virtual void update(double)
280 _courseOriginToAircraft = SGGeodesy::courseDeg(_waypointOrigin,_rnav->position());
281 _distanceOriginAircraftMeter = SGGeodesy::distanceM(_waypointOrigin,_rnav->position());
283 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
284 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
286 _courseDev = -(_courseOriginToAircraft - _targetTrack);
288 bool isMinimumOverFlightDistanceReached = _distanceAircraftTargetMeter < _rnav->overflightDistanceM();
289 bool isOverFlightConeArmed = _distanceAircraftTargetMeter < ( _rnav->overflightArmDistanceM() + _rnav->overflightDistanceM() );
290 bool leavingOverFlightCone = (fabs(_courseAircraftToTarget - _targetTrack) > _rnav->overflightArmAngleDeg());
292 if( isMinimumOverFlightDistanceReached ){
296 if( isOverFlightConeArmed ){
298 if ( leavingOverFlightCone ) {
307 virtual double distanceToWayptM() const
309 return _distanceAircraftTargetMeter;
312 virtual double xtrackErrorNm() const
314 return greatCircleCrossTrackError(_distanceOriginAircraftMeter * SG_METER_TO_NM, _courseDev);
317 virtual bool toFlag() const
322 virtual double courseDeviationDeg() const
327 virtual double trueBearingDeg() const
329 return _courseAircraftToTarget;
332 virtual SGGeod position() const
334 return _waypt->position();
341 * A(from), B(to), D(position) perhaps off course
343 SGGeod _waypointOrigin;
344 double _distanceOriginAircraftMeter;
345 double _distanceAircraftTargetMeter;
346 double _courseOriginToAircraft;
347 double _courseAircraftToTarget;
354 * Special controller for runways. For runways, we want very narrow deviation
355 * constraints, and to understand that any point along the paved area is
356 * equivalent to being 'at' the runway.
358 class RunwayCtl : public WayptController
361 RunwayCtl(RNAV* aRNAV, const WayptRef& aWpt) :
362 WayptController(aRNAV, aWpt),
364 _distanceAircraftRunwayEnd(0.0),
371 _runway = static_cast<RunwayWaypt*>(_waypt.get())->runway();
372 _targetTrack = _runway->headingDeg();
375 virtual void update(double)
377 double bearingAircraftRunwayEnd;
378 // use the far end of the runway for course deviation calculations.
379 // this should do the correct thing both for takeoffs (including entering
380 // the runway at a taxiway after the threshold) and also landings.
381 // seperately compute the distance to the threshold for timeToWaypt calc
383 bearingAircraftRunwayEnd = SGGeodesy::courseDeg(_rnav->position(), _runway->end());
384 _distanceAircraftRunwayEnd = SGGeodesy::distanceM(_rnav->position(), _runway->end());
386 double _courseDev = bearingAircraftRunwayEnd - _targetTrack;
387 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
389 if ((fabs(_courseDev) > _rnav->overflightArmAngleDeg()) && (_distanceAircraftRunwayEnd < _rnav->overflightArmDistanceM())) {
394 virtual double distanceToWayptM() const
396 return SGGeodesy::distanceM(_rnav->position(), _runway->threshold());
399 virtual double xtrackErrorNm() const
401 double x = sin(_courseDev * SG_RADIANS_TO_DEGREES) * _distanceAircraftRunwayEnd;
402 return x * SG_METER_TO_NM;
405 virtual double courseDeviationDeg() const
410 virtual double trueBearingDeg() const
412 // as in update(), use runway->end here, so the value remains
413 // sensible whether taking off or landing.
414 return SGGeodesy::courseDeg(_rnav->position(), _runway->end());
417 virtual SGGeod position() const
419 return _runway->threshold();
423 double _distanceAircraftRunwayEnd;
427 class ConstHdgToAltCtl : public WayptController
430 ConstHdgToAltCtl(RNAV* aRNAV, const WayptRef& aWpt) :
431 WayptController(aRNAV, aWpt)
434 if (_waypt->type() != "hdgToAlt") {
435 throw sg_exception("invalid waypoint type", "ConstHdgToAltCtl ctor");
438 if (_waypt->altitudeRestriction() == RESTRICT_NONE) {
439 throw sg_exception("invalid waypoint alt restriction", "ConstHdgToAltCtl ctor");
445 HeadingToAltitude* w = (HeadingToAltitude*) _waypt.get();
446 _targetTrack = w->headingDegMagnetic() + _rnav->magvarDeg();
447 _filteredFPM = _lastFPM = _rnav->vspeedFPM();
450 virtual void update(double dt)
452 double curAlt = _rnav->position().getElevationFt();
453 // adjust to get a stable FPM value; bigger values mean slower
454 // response but more stable.
455 const double RESPONSIVENESS = 1.0;
457 _filteredFPM = fgGetLowPass(_lastFPM, _rnav->vspeedFPM(), dt * RESPONSIVENESS);
458 _lastFPM = _rnav->vspeedFPM();
460 switch (_waypt->altitudeRestriction()) {
462 case RESTRICT_COMPUTED:
464 double d = curAlt - _waypt->altitudeFt();
465 if (fabs(d) < 50.0) {
466 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, reached target altitude " << _waypt->altitudeFt());
472 if (curAlt >= _waypt->altitudeFt()) {
473 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, above target altitude " << _waypt->altitudeFt());
479 if (curAlt <= _waypt->altitudeFt()) {
480 SG_LOG(SG_INSTR, SG_INFO, "ConstHdgToAltCtl, below target altitude " << _waypt->altitudeFt());
490 virtual double timeToWaypt() const
492 double d = fabs(_rnav->position().getElevationFt() - _waypt->altitudeFt());
493 return (d / _filteredFPM) * 60.0;
496 virtual double distanceToWayptM() const
498 // we could filter ground speed here, but it's likely stable enough,
499 // and timeToWaypt already filters the FPM value
500 double gsMsec = _rnav->groundSpeedKts() * SG_KT_TO_MPS;
501 return timeToWaypt() * gsMsec;
504 virtual SGGeod position() const
508 SGGeodesy::direct(_rnav->position(), _targetTrack, distanceToWayptM(), p, az2);
513 double _lastFPM, _filteredFPM;
516 class InterceptCtl : public WayptController
519 InterceptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
520 WayptController(aRNAV, aWpt),
523 if (_waypt->type() != "radialIntercept") {
524 throw sg_exception("invalid waypoint type", "InterceptCtl ctor");
530 RadialIntercept* w = (RadialIntercept*) _waypt.get();
531 _trueRadial = w->radialDegMagnetic() + _rnav->magvarDeg();
532 _targetTrack = w->courseDegMagnetic() + _rnav->magvarDeg();
535 virtual void update(double)
537 // note we want the outbound radial from the waypt, hence the ordering
538 // of arguments to courseDeg
539 double r = SGGeodesy::courseDeg(_waypt->position(), _rnav->position());
540 if (fabs(r - _trueRadial) < 0.5) {
545 virtual double distanceToWayptM() const
547 return SGGeodesy::distanceM(_rnav->position(), position());
550 virtual SGGeod position() const
553 geocPos = SGGeoc::fromGeod(_rnav->position()),
554 geocWayptPos = SGGeoc::fromGeod(_waypt->position());
556 bool ok = geocRadialIntersection(geocPos, _rnav->trackDeg(),
557 geocWayptPos, _trueRadial, c);
559 // try with a backwards offset from the waypt pos, in case the
560 // procedure waypt location is too close. (eg, KSFO OCEAN SID)
562 SGGeoc navidAdjusted;
563 SGGeodesy::advanceRadM(geocWayptPos, _trueRadial, SG_NM_TO_METER * -10, navidAdjusted);
565 ok = geocRadialIntersection(geocPos, _rnav->trackDeg(),
566 navidAdjusted, _trueRadial, c);
568 // fallback for the broken case
569 return _waypt->position();
573 return SGGeod::fromGeoc(c);
579 class DMEInterceptCtl : public WayptController
582 DMEInterceptCtl(RNAV* aRNAV, const WayptRef& aWpt) :
583 WayptController(aRNAV, aWpt),
587 if (_waypt->type() != "dmeIntercept") {
588 throw sg_exception("invalid waypoint type", "DMEInterceptCtl ctor");
594 _dme = (DMEIntercept*) _waypt.get();
595 _targetTrack = _dme->courseDegMagnetic() + _rnav->magvarDeg();
598 virtual void update(double)
600 _distanceNm = SGGeodesy::distanceNm(_rnav->position(), _dme->position());
601 double d = fabs(_distanceNm - _dme->dmeDistanceNm());
607 virtual double distanceToWayptM() const
609 return fabs(_distanceNm - _dme->dmeDistanceNm()) * SG_NM_TO_METER;
612 virtual SGGeod position() const
614 SGGeoc geocPos = SGGeoc::fromGeod(_rnav->position());
615 SGGeoc navid = SGGeoc::fromGeod(_dme->position());
616 double distRad = _dme->dmeDistanceNm() * SG_NM_TO_RAD;
618 // compute radial GC course
620 SGGeodesy::advanceRadM(geocPos, _targetTrack, 100 * SG_NM_TO_RAD, bPt);
622 double dNm = pointsKnownDistanceFromGC(geocPos, bPt, navid, distRad);
624 SG_LOG(SG_AUTOPILOT, SG_WARN, "DMEInterceptCtl::position failed");
625 return _dme->position(); // horrible fallback
628 return SGGeodesy::direct(_rnav->position(), _targetTrack, dNm * SG_NM_TO_METER);
636 class HoldCtl : public WayptController
639 HoldCtl(RNAV* aRNAV, const WayptRef& aWpt) :
640 WayptController(aRNAV, aWpt)
650 virtual void update(double)
652 // fly inbound / outbound sides, or execute the turn
656 targetTrack += dt * turnRateSec * turnDirection;
658 if .. targetTrack has passed inbound radial, doen with this turn
660 if target track has passed reciprocal radial done with turn
663 check time / distance elapsed
668 nextHeading = inbound;
670 nextHeading += 180.0;
671 SG_NORMALIZE_RANGE(nextHeading, 0.0, 360.0);
681 virtual double distanceToWayptM() const
686 virtual SGGeod position() const
688 return _waypt->position();
692 class VectorsCtl : public WayptController
695 VectorsCtl(RNAV* aRNAV, const WayptRef& aWpt) :
696 WayptController(aRNAV, aWpt)
706 virtual void update(double)
711 virtual double distanceToWayptM() const
716 virtual SGGeod position() const
718 return _waypt->position();
724 ///////////////////////////////////////////////////////////////////////////////
726 DirectToController::DirectToController(RNAV* aRNAV, const WayptRef& aWpt, const SGGeod& aOrigin) :
727 WayptController(aRNAV, aWpt),
729 _distanceAircraftTargetMeter(0.0),
731 _courseAircraftToTarget(0.0)
735 void DirectToController::init()
737 if (_waypt->flag(WPT_DYNAMIC)) {
738 throw sg_exception("can't direct-to a dynamic waypoint");
741 _targetTrack = SGGeodesy::courseDeg(_origin, _waypt->position());
744 void DirectToController::update(double)
746 double courseOriginToAircraft;
748 courseOriginToAircraft = SGGeodesy::courseDeg(_origin,_rnav->position());
750 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
751 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
753 _courseDev = -(courseOriginToAircraft - _targetTrack);
755 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
757 bool isMinimumOverFlightDistanceReached = _distanceAircraftTargetMeter < _rnav->overflightDistanceM();
758 bool isOverFlightConeArmed = _distanceAircraftTargetMeter < ( _rnav->overflightArmDistanceM() + _rnav->overflightDistanceM() );
759 bool leavingOverFlightCone = (fabs(_courseAircraftToTarget - _targetTrack) > _rnav->overflightArmAngleDeg());
761 if( isMinimumOverFlightDistanceReached ){
764 if( isOverFlightConeArmed && leavingOverFlightCone ){
770 double DirectToController::distanceToWayptM() const
772 return _distanceAircraftTargetMeter;
775 double DirectToController::xtrackErrorNm() const
777 return greatCircleCrossTrackError(_distanceAircraftTargetMeter * SG_METER_TO_NM, _courseDev);
780 double DirectToController::courseDeviationDeg() const
785 double DirectToController::trueBearingDeg() const
787 return _courseAircraftToTarget;
790 SGGeod DirectToController::position() const
792 return _waypt->position();
795 ///////////////////////////////////////////////////////////////////////////////
797 OBSController::OBSController(RNAV* aRNAV, const WayptRef& aWpt) :
798 WayptController(aRNAV, aWpt),
799 _distanceAircraftTargetMeter(0.0),
801 _courseAircraftToTarget(0.0)
805 void OBSController::init()
807 if (_waypt->flag(WPT_DYNAMIC)) {
808 throw sg_exception("can't use a dynamic waypoint for OBS mode");
811 _targetTrack = _rnav->selectedMagCourse() + _rnav->magvarDeg();
814 void OBSController::update(double)
816 _targetTrack = _rnav->selectedMagCourse() + _rnav->magvarDeg();
818 _courseAircraftToTarget = SGGeodesy::courseDeg(_rnav->position(),_waypt->position());
819 _distanceAircraftTargetMeter = SGGeodesy::distanceM(_rnav->position(),_waypt->position());
821 // _courseDev inverted we use point target as origin
822 _courseDev = (_courseAircraftToTarget - _targetTrack);
823 SG_NORMALIZE_RANGE(_courseDev, -180.0, 180.0);
826 bool OBSController::toFlag() const
828 return (fabs(_courseDev) < _rnav->overflightArmAngleDeg());
831 double OBSController::distanceToWayptM() const
833 return _distanceAircraftTargetMeter;
836 double OBSController::xtrackErrorNm() const
838 return greatCircleCrossTrackError(_distanceAircraftTargetMeter * SG_METER_TO_NM, _courseDev);
841 double OBSController::courseDeviationDeg() const
843 // if (fabs(_courseDev) > 90.0) {
844 // double d = -_courseDev;
845 // SG_NORMALIZE_RANGE(d, -90.0, 90.0);
852 double OBSController::trueBearingDeg() const
854 return _courseAircraftToTarget;
857 SGGeod OBSController::position() const
859 return _waypt->position();
862 ///////////////////////////////////////////////////////////////////////////////
864 WayptController* WayptController::createForWaypt(RNAV* aRNAV, const WayptRef& aWpt)
867 throw sg_exception("Passed null waypt", "WayptController::createForWaypt");
870 const std::string& wty(aWpt->type());
871 if (wty == "runway") {
872 return new RunwayCtl(aRNAV, aWpt);
875 if (wty == "radialIntercept") {
876 return new InterceptCtl(aRNAV, aWpt);
879 if (wty == "dmeIntercept") {
880 return new DMEInterceptCtl(aRNAV, aWpt);
883 if (wty == "hdgToAlt") {
884 return new ConstHdgToAltCtl(aRNAV, aWpt);
887 if (wty == "vectors") {
888 return new VectorsCtl(aRNAV, aWpt);
892 return new HoldCtl(aRNAV, aWpt);
895 return new LegWayptCtl(aRNAV, aWpt);
898 } // of namespace flightgear