From: jmt Date: Mon, 5 Oct 2009 21:09:20 +0000 (+0000) Subject: Land the GPS/route-manager re-write. Many things are better, many other things will... X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=d784810430050457365d203fb453042ba17582cf;p=flightgear.git Land the GPS/route-manager re-write. Many things are better, many other things will be better, some things are no doubt broken. Please be patient and report problems on the mailing list. --- diff --git a/src/Autopilot/route_mgr.cxx b/src/Autopilot/route_mgr.cxx index e8c5674c3..a80c330bc 100644 --- a/src/Autopilot/route_mgr.cxx +++ b/src/Autopilot/route_mgr.cxx @@ -29,34 +29,45 @@ #include -#include -#include
-#include - #include "route_mgr.hxx" +#include +#include + +#include +#include +#include +#include + +#include "Main/fg_props.hxx" +#include "Navaids/positioned.hxx" +#include "Airports/simple.hxx" +#include "Airports/runways.hxx" + +#include "FDM/flight.hxx" // for getting ground speed + #define RM "/autopilot/route-manager/" +static double get_ground_speed() { + // starts in ft/s so we convert to kts + static const SGPropertyNode * speedup_node = fgGetNode("/sim/speed-up"); + + double ft_s = cur_fdm_state->get_V_ground_speed() + * speedup_node->getIntValue(); + double kts = ft_s * SG_FEET_TO_METER * 3600 * SG_METER_TO_NM; + return kts; +} FGRouteMgr::FGRouteMgr() : - route( new SGRoute ), + _route( new SGRoute ), + _autoSequence(true), lon( NULL ), lat( NULL ), alt( NULL ), - true_hdg_deg( NULL ), target_altitude_ft( NULL ), altitude_lock( NULL ), - wp0_id( NULL ), - wp0_dist( NULL ), - wp0_eta( NULL ), - wp1_id( NULL ), - wp1_dist( NULL ), - wp1_eta( NULL ), - wpn_id( NULL ), - wpn_dist( NULL ), - wpn_eta( NULL ), input(fgGetNode( RM "input", true )), - listener(new Listener(this)), + listener(new InputListener(this)), mirror(fgGetNode( RM "route", true )), altitude_set( false ) { @@ -67,77 +78,128 @@ FGRouteMgr::FGRouteMgr() : FGRouteMgr::~FGRouteMgr() { input->removeChangeListener(listener); + delete listener; - delete route; + delete _route; } void FGRouteMgr::init() { - lon = fgGetNode( "/position/longitude-deg", true ); - lat = fgGetNode( "/position/latitude-deg", true ); - alt = fgGetNode( "/position/altitude-ft", true ); - - true_hdg_deg = fgGetNode( "/autopilot/settings/true-heading-deg", true ); - target_altitude_ft = fgGetNode( "/autopilot/settings/target-altitude-ft", true ); - altitude_lock = fgGetNode( "/autopilot/locks/altitude", true ); - - wp0_id = fgGetNode( RM "wp[0]/id", true ); - wp0_dist = fgGetNode( RM "wp[0]/dist", true ); - wp0_eta = fgGetNode( RM "wp[0]/eta", true ); - - wp1_id = fgGetNode( RM "wp[1]/id", true ); - wp1_dist = fgGetNode( RM "wp[1]/dist", true ); - wp1_eta = fgGetNode( RM "wp[1]/eta", true ); - - wpn_id = fgGetNode( RM "wp-last/id", true ); - wpn_dist = fgGetNode( RM "wp-last/dist", true ); - wpn_eta = fgGetNode( RM "wp-last/eta", true ); - - route->clear(); - update_mirror(); + SGPropertyNode_ptr rm(fgGetNode(RM)); + + lon = fgGetNode( "/position/longitude-deg", true ); + lat = fgGetNode( "/position/latitude-deg", true ); + alt = fgGetNode( "/position/altitude-ft", true ); + magvar = fgGetNode("/environment/magnetic-variation-deg", true); + + target_altitude_ft = fgGetNode( "/autopilot/settings/target-altitude-ft", true ); + altitude_lock = fgGetNode( "/autopilot/locks/altitude", true ); + + departure = fgGetNode(RM "departure", true); +// init departure information from current location + SGGeod pos = SGGeod::fromDegFt(lon->getDoubleValue(), lat->getDoubleValue(), alt->getDoubleValue()); + FGAirport* apt = FGAirport::findClosest(pos, 10.0); + if (apt) { + departure->setStringValue("airport", apt->ident().c_str()); + FGRunway* active = apt->getActiveRunwayForUsage(); + departure->setStringValue("runway", active->ident().c_str()); + } else { + departure->setStringValue("airport", ""); + departure->setStringValue("runway", ""); + } + + departure->getChild("etd", 0, true); + departure->getChild("takeoff-time", 0, true); + + destination = fgGetNode(RM "destination", true); + destination->getChild("airport", 0, true); + destination->getChild("runway", 0, true); + destination->getChild("eta", 0, true); + destination->getChild("touchdown-time", 0, true); + + alternate = fgGetNode(RM "alternate", true); + alternate->getChild("airport", 0, true); + alternate->getChild("runway", 0, true); + + cruise = fgGetNode(RM "cruise", true); + cruise->getChild("altitude", 0, true); + cruise->getChild("flight-level", 0, true); + cruise->getChild("speed", 0, true); + + totalDistance = fgGetNode(RM "total-distance", true); + totalDistance->setDoubleValue(0.0); + + ete = fgGetNode(RM "ete", true); + ete->setDoubleValue(0.0); + + elapsedFlightTime = fgGetNode(RM "flight-time", true); + elapsedFlightTime->setDoubleValue(0.0); + + active = fgGetNode(RM "active", true); + active->setBoolValue(false); + + airborne = fgGetNode(RM "airborne", true); + airborne->setBoolValue(false); + + rm->tie("auto-sequence", SGRawValuePointer(&_autoSequence)); + + currentWp = fgGetNode(RM "current-wp", true); + currentWp->setIntValue(_route->current_index()); + + // temporary distance / eta calculations, for backward-compatability + wp0 = fgGetNode(RM "wp", 0, true); + wp0->getChild("id", 0, true); + wp0->getChild("dist", 0, true); + wp0->getChild("eta", 0, true); + wp0->getChild("bearing-deg", 0, true); + + wp1 = fgGetNode(RM "wp", 1, true); + wp1->getChild("id", 0, true); + wp1->getChild("dist", 0, true); + wp1->getChild("eta", 0, true); + + wpn = fgGetNode(RM "wp-last", 0, true); + wpn->getChild("dist", 0, true); + wpn->getChild("eta", 0, true); + + _route->clear(); + update_mirror(); + + _pathNode = fgGetNode(RM "file-path", 0, true); } void FGRouteMgr::postinit() { string_list *waypoints = globals->get_initial_waypoints(); - if (!waypoints) - return; - - vector::iterator it; - for (it = waypoints->begin(); it != waypoints->end(); ++it) + if (waypoints) { + vector::iterator it; + for (it = waypoints->begin(); it != waypoints->end(); ++it) new_waypoint(*it); + } + + weightOnWheels = fgGetNode("/gear/gear[0]/wow", false); + // check airbone flag agrees with presets + } void FGRouteMgr::bind() { } void FGRouteMgr::unbind() { } - -static double get_ground_speed() { - // starts in ft/s so we convert to kts - static const SGPropertyNode * speedup_node = fgGetNode("/sim/speed-up"); - - double ft_s = cur_fdm_state->get_V_ground_speed() - * speedup_node->getIntValue(); - double kts = ft_s * SG_FEET_TO_METER * 3600 * SG_METER_TO_NM; - - return kts; -} - void FGRouteMgr::updateTargetAltitude() { - if (route->size() == 0) { + if (_route->current_index() == _route->size()) { altitude_set = false; return; } - SGWayPoint wp = route->get_waypoint( 0 ); - if (wp.get_target_alt() < -9990.0) { + SGWayPoint wp = _route->get_current(); + if (wp.get_target().getElevationM() < -9990.0) { altitude_set = false; return; } altitude_set = true; - target_altitude_ft->setDoubleValue( wp.get_target_alt() * SG_METER_TO_FEET ); + target_altitude_ft->setDoubleValue(wp.get_target().getElevationFt()); if ( !near_ground() ) { // James Turner [zakalawe]: there's no explanation for this logic, @@ -147,60 +209,79 @@ void FGRouteMgr::updateTargetAltitude() { } } +bool FGRouteMgr::isRouteActive() const +{ + return active->getBoolValue(); +} + void FGRouteMgr::update( double dt ) { - if (route->size() == 0) { - return; // no route set, early return + if (!active->getBoolValue()) { + return; } - double wp_course, wp_distance; - - // first way point - SGWayPoint wp = route->get_waypoint( 0 ); + double groundSpeed = get_ground_speed(); + if (!airborne->getBoolValue()) { + if (weightOnWheels->getBoolValue() || (groundSpeed < 40)) { + return; + } + + airborne->setBoolValue(true); + _takeoffTime = time(NULL); // start the clock + departure->setIntValue("takeoff-time", _takeoffTime); + } + + time_t now = time(NULL); + elapsedFlightTime->setDoubleValue(difftime(now, _takeoffTime)); + + double inboundCourse, dummy, wp_course, wp_distance; + SGWayPoint wp = _route->get_current(); + + wp.CourseAndDistance(_route->get_waypoint(_route->current_index() - 1), + &inboundCourse, &dummy); + wp.CourseAndDistance( lon->getDoubleValue(), lat->getDoubleValue(), alt->getDoubleValue(), &wp_course, &wp_distance ); - true_hdg_deg->setDoubleValue( wp_course ); - if ( wp_distance < 200.0 ) { - pop_waypoint(); - if (route->size() == 0) { - return; // end of route, we're done for the time being - } - - wp = route->get_waypoint( 0 ); + if (_autoSequence && (wp_distance < 2000.0)) { + double courseDeviation = inboundCourse - wp_course; + SG_NORMALIZE_RANGE(courseDeviation, -180.0, 180.0); + if (fabs(courseDeviation) < 90.0) { + SG_LOG( SG_AUTOPILOT, SG_INFO, "route manager doing sequencing"); + sequence(); + } } - // update the property tree info for WP0 - wp0_id->setStringValue( wp.get_id().c_str() ); - double accum = wp_distance; - wp0_dist->setDoubleValue( accum * SG_METER_TO_NM ); - setETAPropertyFromDistance(wp0_eta, accum); - - // next way point - if ( route->size() > 1 ) { - SGWayPoint wp = route->get_waypoint( 1 ); - - // update the property tree info - wp1_id->setStringValue( wp.get_id().c_str() ); - accum += wp.get_distance(); - wp1_dist->setDoubleValue( accum * SG_METER_TO_NM ); - setETAPropertyFromDistance(wp1_eta, accum); + wp0->setDoubleValue("dist", wp_distance * SG_METER_TO_NM); + wp_course -= magvar->getDoubleValue(); // expose magnetic bearing + wp0->setDoubleValue("bearing-deg", wp_course); + setETAPropertyFromDistance(wp0->getChild("eta"), wp_distance); + + if ((_route->current_index() + 1) < _route->size()) { + wp = _route->get_waypoint(_route->current_index() + 1); + double wp1_course, wp1_distance; + wp.CourseAndDistance(lon->getDoubleValue(), lat->getDoubleValue(), + alt->getDoubleValue(), &wp1_course, &wp1_distance); + + wp1->setDoubleValue("dist", wp1_distance * SG_METER_TO_NM); + setETAPropertyFromDistance(wp1->getChild("eta"), wp1_distance); } - - // summarize remaining way points - if ( route->size() > 2 ) { - SGWayPoint wp; - for ( int i = 2; i < route->size(); ++i ) { - wp = route->get_waypoint( i ); - accum += wp.get_distance(); - } - - // update the property tree info - wpn_id->setStringValue( wp.get_id().c_str() ); - wpn_dist->setDoubleValue( accum * SG_METER_TO_NM ); - setETAPropertyFromDistance(wpn_eta, accum); + + double totalDistanceRemaining = wp_distance; // distance to current waypoint + for (int i=_route->current_index() + 1; i<_route->size(); ++i) { + totalDistanceRemaining += _route->get_waypoint(i).get_distance(); } + + wpn->setDoubleValue("dist", totalDistanceRemaining * SG_METER_TO_NM); + ete->setDoubleValue(totalDistanceRemaining * SG_METER_TO_NM / groundSpeed * 3600.0); + setETAPropertyFromDistance(wpn->getChild("eta"), totalDistanceRemaining); + + // get time now at destination tz as tm struct + // add ete seconds + // convert to string ... and stash in property + //destination->setDoubleValue("eta", eta); } + void FGRouteMgr::setETAPropertyFromDistance(SGPropertyNode_ptr aProp, double aDistance) { char eta_str[64]; double eta = aDistance * SG_METER_TO_NM / get_ground_speed(); @@ -209,8 +290,7 @@ void FGRouteMgr::setETAPropertyFromDistance(SGPropertyNode_ptr aProp, double aDi } if ( eta < (1.0/6.0) ) { - // within 10 minutes, bump up to min/secs - eta *= 60.0; + eta *= 60.0; // within 10 minutes, bump up to min/secs } int major = (int)eta, @@ -220,9 +300,9 @@ void FGRouteMgr::setETAPropertyFromDistance(SGPropertyNode_ptr aProp, double aDi } void FGRouteMgr::add_waypoint( const SGWayPoint& wp, int n ) { - route->add_waypoint( wp, n ); + _route->add_waypoint( wp, n ); update_mirror(); - if ((n==0) || (route->size() == 1)) { + if ((n==0) || (_route->size() == 1)) { updateTargetAltitude(); } } @@ -231,29 +311,11 @@ void FGRouteMgr::add_waypoint( const SGWayPoint& wp, int n ) { SGWayPoint FGRouteMgr::pop_waypoint( int n ) { SGWayPoint wp; - if ( route->size() > 0 ) { + if ( _route->size() > 0 ) { if ( n < 0 ) - n = route->size() - 1; - wp = route->get_waypoint(n); - route->delete_waypoint(n); - } - - if ( route->size() <= 2 ) { - wpn_id->setStringValue( "" ); - wpn_dist->setDoubleValue( 0.0 ); - wpn_eta->setStringValue( "" ); - } - - if ( route->size() <= 1 ) { - wp1_id->setStringValue( "" ); - wp1_dist->setDoubleValue( 0.0 ); - wp1_eta->setStringValue( "" ); - } - - if ( route->size() <= 0 ) { - wp0_id->setStringValue( "" ); - wp0_dist->setDoubleValue( 0.0 ); - wp0_eta->setStringValue( "" ); + n = _route->size() - 1; + wp = _route->get_waypoint(n); + _route->delete_waypoint(n); } updateTargetAltitude(); @@ -286,9 +348,9 @@ SGWayPoint* FGRouteMgr::make_waypoint(const string& tgt ) { string target = tgt; // make upper case - for (unsigned int i = 0; i < target.size(); i++) - if (target[i] >= 'a' && target[i] <= 'z') - target[i] -= 'a' - 'A'; + for (unsigned int i = 0; i < target.size(); i++) { + target[i] = toupper(target[i]); + } // extract altitude double alt = -9999.0; @@ -305,49 +367,90 @@ SGWayPoint* FGRouteMgr::make_waypoint(const string& tgt ) { if ( pos != string::npos ) { double lon = atof( target.substr(0, pos).c_str()); double lat = atof( target.c_str() + pos + 1); - - SG_LOG( SG_GENERAL, SG_INFO, "Adding waypoint lon = " << lon << ", lat = " << lat ); return new SGWayPoint( lon, lat, alt, SGWayPoint::WGS84, target ); } SGGeod basePosition; - if (route->size() > 0) { - SGWayPoint wp = get_waypoint(route->size()-1); - basePosition = SGGeod::fromDeg(wp.get_target_lon(), wp.get_target_lat()); + if (_route->size() > 0) { + SGWayPoint wp = get_waypoint(_route->size()-1); + basePosition = wp.get_target(); } else { // route is empty, use current position basePosition = SGGeod::fromDeg( fgGetNode("/position/longitude-deg")->getDoubleValue(), fgGetNode("/position/latitude-deg")->getDoubleValue()); } + + vector pieces(simgear::strutils::split(target, "/")); - FGPositionedRef p = FGPositioned::findClosestWithIdent(target, basePosition); + FGPositionedRef p = FGPositioned::findClosestWithIdent(pieces.front(), basePosition); if (!p) { - SG_LOG( SG_GENERAL, SG_INFO, "Unable to find FGPositioned with ident:" << target); + SG_LOG( SG_AUTOPILOT, SG_INFO, "Unable to find FGPositioned with ident:" << pieces.front()); + return NULL; + } + + if (pieces.size() == 1) { + // simple case + return new SGWayPoint(p->geod(), target, p->name()); + } + + if (pieces.size() == 3) { + // navaid/radial/distance-nm notation + double radial = atof(pieces[1].c_str()), + distanceNm = atof(pieces[2].c_str()), + az2; + radial += magvar->getDoubleValue(); // convert to true bearing + SGGeod offsetPos; + SGGeodesy::direct(p->geod(), radial, distanceNm * SG_NM_TO_METER, offsetPos, az2); + + SG_LOG(SG_AUTOPILOT, SG_INFO, "final offset radial is " << radial); + + if (alt > -9990) { + offsetPos.setElevationM(alt); + } // otherwise use the elevation of navid + return new SGWayPoint(offsetPos, p->ident() + pieces[2], target); + } + + if (pieces.size() == 2) { + FGAirport* apt = dynamic_cast(p.ptr()); + if (!apt) { + SG_LOG(SG_AUTOPILOT, SG_INFO, "Waypoint is not an airport:" << pieces.front()); return NULL; + } + + if (!apt->hasRunwayWithIdent(pieces[1])) { + SG_LOG(SG_AUTOPILOT, SG_INFO, "No runway: " << pieces[1] << " at " << pieces[0]); + return NULL; + } + + FGRunway* runway = apt->getRunwayByIdent(pieces[1]); + SGGeod t = runway->threshold(); + return new SGWayPoint(t.getLongitudeDeg(), t.getLatitudeDeg(), alt, SGWayPoint::WGS84, pieces[1]); } - return new SGWayPoint(p->longitude(), p->latitude(), alt, SGWayPoint::WGS84, target); + SG_LOG(SG_AUTOPILOT, SG_INFO, "Unable to parse waypoint:" << target); + return NULL; } // mirror internal route to the property system for inspection by other subsystems void FGRouteMgr::update_mirror() { mirror->removeChildren("wp"); - for (int i = 0; i < route->size(); i++) { - SGWayPoint wp = route->get_waypoint(i); + for (int i = 0; i < _route->size(); i++) { + SGWayPoint wp = _route->get_waypoint(i); SGPropertyNode *prop = mirror->getChild("wp", i, 1); + const SGGeod& pos(wp.get_target()); prop->setStringValue("id", wp.get_id().c_str()); prop->setStringValue("name", wp.get_name().c_str()); - prop->setDoubleValue("longitude-deg", wp.get_target_lon()); - prop->setDoubleValue("latitude-deg", wp.get_target_lat()); - prop->setDoubleValue("altitude-m", wp.get_target_alt()); - prop->setDoubleValue("altitude-ft", wp.get_target_alt() * SG_METER_TO_FEET); + prop->setDoubleValue("longitude-deg", pos.getLongitudeDeg()); + prop->setDoubleValue("latitude-deg",pos.getLatitudeDeg()); + prop->setDoubleValue("altitude-m", pos.getElevationM()); + prop->setDoubleValue("altitude-ft", pos.getElevationFt()); } // set number as listener attachment point - mirror->setIntValue("num", route->size()); + mirror->setIntValue("num", _route->size()); } @@ -372,12 +475,18 @@ bool FGRouteMgr::near_ground() { // @INSERT2:KSFO@900 ... insert "KSFO@900" as 3rd entry // KSFO@900 ... append "KSFO@900" // -void FGRouteMgr::Listener::valueChanged(SGPropertyNode *prop) +void FGRouteMgr::InputListener::valueChanged(SGPropertyNode *prop) { const char *s = prop->getStringValue(); if (!strcmp(s, "@CLEAR")) mgr->init(); - else if (!strcmp(s, "@POP")) + else if (!strcmp(s, "@ACTIVATE")) + mgr->activate(); + else if (!strcmp(s, "@LOAD")) { + mgr->loadRoute(); + } else if (!strcmp(s, "@SAVE")) { + mgr->saveRoute(); + } else if (!strcmp(s, "@POP")) mgr->pop_waypoint(0); else if (!strncmp(s, "@DELETE", 7)) mgr->pop_waypoint(atoi(s + 7)); @@ -394,4 +503,296 @@ void FGRouteMgr::Listener::valueChanged(SGPropertyNode *prop) mgr->new_waypoint(s); } +// SGWayPoint( const double lon = 0.0, const double lat = 0.0, +// const double alt = 0.0, const modetype m = WGS84, +// const string& s = "", const string& n = "" ); + +bool FGRouteMgr::activate() +{ + const FGAirport* depApt = fgFindAirportID(departure->getStringValue("airport")); + if (!depApt) { + SG_LOG(SG_AUTOPILOT, SG_ALERT, + "unable to activate route: departure airport is invalid:" + << departure->getStringValue("airport") ); + return false; + } + + string runwayId(departure->getStringValue("runway")); + FGRunway* runway = NULL; + if (depApt->hasRunwayWithIdent(runwayId)) { + runway = depApt->getRunwayByIdent(runwayId); + } else { + SG_LOG(SG_AUTOPILOT, SG_INFO, + "route-manager, departure runway not found:" << runwayId); + runway = depApt->getActiveRunwayForUsage(); + } + + SGWayPoint swp(runway->threshold(), + depApt->ident() + "-" + runway->ident(), runway->name()); + add_waypoint(swp, 0); + + const FGAirport* destApt = fgFindAirportID(destination->getStringValue("airport")); + if (!destApt) { + SG_LOG(SG_AUTOPILOT, SG_ALERT, + "unable to activate route: destination airport is invalid:" + << destination->getStringValue("airport") ); + return false; + } + + runwayId = (destination->getStringValue("runway")); + if (destApt->hasRunwayWithIdent(runwayId)) { + FGRunway* runway = depApt->getRunwayByIdent(runwayId); + SGWayPoint swp(runway->end(), + destApt->ident() + "-" + runway->ident(), runway->name()); + add_waypoint(swp); + } else { + // quite likely, since destination runway may not be known until enroute + // probably want a listener on the 'destination' node to allow an enroute + // update + add_waypoint(SGWayPoint(destApt->geod(), destApt->ident(), destApt->name())); + } + + _route->set_current(0); + + double routeDistanceNm = _route->total_distance() * SG_METER_TO_NM; + totalDistance->setDoubleValue(routeDistanceNm); + double cruiseSpeedKts = cruise->getDoubleValue("speed", 0.0); + if (cruiseSpeedKts > 1.0) { + // very very crude approximation, doesn't allow for climb / descent + // performance or anything else at all + ete->setDoubleValue(routeDistanceNm / cruiseSpeedKts * (60.0 * 60.0)); + } + + active->setBoolValue(true); + sequence(); // sequence will sync up wp0, wp1 and current-wp + SG_LOG(SG_AUTOPILOT, SG_INFO, "route-manager, activate route ok"); + return true; +} + + +void FGRouteMgr::sequence() +{ + if (!active->getBoolValue()) { + SG_LOG(SG_AUTOPILOT, SG_ALERT, "trying to sequence waypoints with no active route"); + return; + } + + if (_route->current_index() == _route->size()) { + SG_LOG(SG_AUTOPILOT, SG_INFO, "reached end of active route"); + // what now? + active->setBoolValue(false); + return; + } + + _route->increment_current(); + currentWaypointChanged(); +} + +void FGRouteMgr::jumpToIndex(int index) +{ + if (!active->getBoolValue()) { + SG_LOG(SG_AUTOPILOT, SG_ALERT, "trying to sequence waypoints with no active route"); + return; + } + + if ((index < 0) || (index >= _route->size())) { + SG_LOG(SG_AUTOPILOT, SG_ALERT, "passed invalid index (" << + index << ") to FGRouteMgr::jumpToIndex"); + return; + } + + if (_route->current_index() == index) { + return; // no-op + } + + _route->set_current(index); + currentWaypointChanged(); +} + +void FGRouteMgr::currentWaypointChanged() +{ + SGWayPoint previous = _route->get_previous(); + SGWayPoint cur = _route->get_current(); + + wp0->getChild("id")->setStringValue(cur.get_id()); + if ((_route->current_index() + 1) < _route->size()) { + wp1->getChild("id")->setStringValue(_route->get_next().get_id()); + } else { + wp1->getChild("id")->setStringValue(""); + } + + currentWp->setIntValue(_route->current_index()); + SG_LOG(SG_AUTOPILOT, SG_INFO, "route manager, current-wp is now " << _route->current_index()); +} + +int FGRouteMgr::findWaypoint(const SGGeod& aPos) const +{ + for (int i=0; i<_route->size(); ++i) { + double d = SGGeodesy::distanceM(aPos, _route->get_waypoint(i).get_target()); + if (d < 200.0) { // 200 metres seems close enough + return i; + } + } + + return -1; +} + +SGWayPoint FGRouteMgr::get_waypoint( int i ) const +{ + return _route->get_waypoint(i); +} + +int FGRouteMgr::size() const +{ + return _route->size(); +} + +int FGRouteMgr::currentWaypoint() const +{ + return _route->current_index(); +} + +void FGRouteMgr::saveRoute() +{ + SGPath path(_pathNode->getStringValue()); + SG_LOG(SG_IO, SG_INFO, "Saving route to " << path.str()); + try { + writeProperties(path.str(), mirror, false, SGPropertyNode::ARCHIVE); + } catch (const sg_exception &e) { + SG_LOG(SG_IO, SG_WARN, "Error saving route:" << e.getMessage()); + //guiErrorMessage("Error writing autosave.xml: ", e); + } +} + +void FGRouteMgr::loadRoute() +{ + try { + // deactivate route first + active->setBoolValue(false); + + SGPropertyNode_ptr routeData(new SGPropertyNode); + SGPath path(_pathNode->getStringValue()); + + SG_LOG(SG_IO, SG_INFO, "going to read flight-plan from:" << path.str()); + readProperties(path.str(), routeData); + + // departure nodes + SGPropertyNode* dep = routeData->getChild("departure"); + if (!dep) { + throw sg_io_exception("malformed route file, no departure node"); + } + + string depIdent = dep->getStringValue("airport"); + const FGAirport* depApt = fgFindAirportID(depIdent); + if (!depApt) { + throw sg_io_exception("bad route file, unknown airport:" + depIdent); + } + + departure->setStringValue("runway", dep->getStringValue("runway")); + + // destination + SGPropertyNode* dst = routeData->getChild("destination"); + if (!dst) { + throw sg_io_exception("malformed route file, no destination node"); + } + + destination->setStringValue("airport", dst->getStringValue("airport")); + destination->setStringValue("runay", dst->getStringValue("runway")); + + // alternate + SGPropertyNode* alt = routeData->getChild("alternate"); + if (alt) { + alternate->setStringValue(alt->getStringValue("airport")); + } // of cruise data loading + + // cruise + SGPropertyNode* crs = routeData->getChild("cruise"); + if (crs) { + cruise->setDoubleValue(crs->getDoubleValue("speed")); + } // of cruise data loading + + // route nodes + _route->clear(); + SGPropertyNode_ptr _route = routeData->getChild("route", 0); + SGGeod lastPos(depApt->geod()); + + for (int i=0; i<_route->nChildren(); ++i) { + SGPropertyNode_ptr wp = _route->getChild("wp", i); + parseRouteWaypoint(wp); + } // of route iteration + } catch (sg_exception& e) { + SG_LOG(SG_IO, SG_WARN, "failed to load flight-plan (from '" << e.getOrigin() + << "'):" << e.getMessage()); + } +} +void FGRouteMgr::parseRouteWaypoint(SGPropertyNode* aWP) +{ + SGGeod lastPos; + if (_route->size() > 0) { + lastPos = get_waypoint(_route->size()-1).get_target(); + } else { + // route is empty, use departure airport position + const FGAirport* apt = fgFindAirportID(departure->getStringValue("airport")); + assert(apt); // shouldn't have got this far with an invalid airport + lastPos = apt->geod(); + } + + SGPropertyNode_ptr altProp = aWP->getChild("altitude-ft"); + double alt = -9999.0; + if (altProp) { + alt = altProp->getDoubleValue(); + } + + string ident(aWP->getStringValue("ident")); + if (aWP->hasChild("longitude-deg")) { + // explicit longitude/latitude + if (alt < -9990.0) { + alt = 0.0; // don't export wyapoints with invalid altitude + } + + SGWayPoint swp(aWP->getDoubleValue("longitude-deg"), + aWP->getDoubleValue("latitude-deg"), alt, + SGWayPoint::WGS84, ident, aWP->getStringValue("name")); + add_waypoint(swp); + } else if (aWP->hasChild("navid")) { + // lookup by navid (possibly with offset) + string nid(aWP->getStringValue("navid")); + FGPositionedRef p = FGPositioned::findClosestWithIdent(nid, lastPos); + if (!p) { + throw sg_io_exception("bad route file, unknown navid:" + nid); + } + + SGGeod pos(p->geod()); + if (aWP->hasChild("offset-nm") && aWP->hasChild("offset-radial")) { + double radialDeg = aWP->getDoubleValue("offset-radial"); + // convert magnetic radial to a true radial! + radialDeg += magvar->getDoubleValue(); + double offsetNm = aWP->getDoubleValue("offset-nm"); + double az2; + SGGeodesy::direct(p->geod(), radialDeg, offsetNm * SG_NM_TO_METER, pos, az2); + } + + if (alt < -9990.0) { + alt = p->elevation(); + } + + SGWayPoint swp(pos.getLongitudeDeg(), pos.getLatitudeDeg(), alt, + SGWayPoint::WGS84, ident, ""); + add_waypoint(swp); + } else { + // lookup by ident (symbolic waypoint) + FGPositionedRef p = FGPositioned::findClosestWithIdent(ident, lastPos); + if (!p) { + throw sg_io_exception("bad route file, unknown waypoint:" + ident); + } + + if (alt < -9990.0) { + alt = p->elevation(); + } + + SGWayPoint swp(p->longitude(), p->latitude(), alt, + SGWayPoint::WGS84, p->ident(), p->name()); + add_waypoint(swp); + } +} diff --git a/src/Autopilot/route_mgr.hxx b/src/Autopilot/route_mgr.hxx index 8806cae27..dbbe4577d 100644 --- a/src/Autopilot/route_mgr.hxx +++ b/src/Autopilot/route_mgr.hxx @@ -25,9 +25,12 @@ #define _ROUTE_MGR_HXX 1 #include -#include +#include #include +// forward decls +class SGRoute; +class SGPath; /** * Top level route manager class @@ -38,61 +41,84 @@ class FGRouteMgr : public SGSubsystem { private: - - SGRoute *route; - + SGRoute* _route; + time_t _takeoffTime; + time_t _touchdownTime; + bool _autoSequence; // true if we are doing internal sequencing + // false if other code (FMS/GPS) is managing sequencing + // automatic inputs SGPropertyNode_ptr lon; SGPropertyNode_ptr lat; SGPropertyNode_ptr alt; - + SGPropertyNode_ptr magvar; + // automatic outputs - SGPropertyNode_ptr true_hdg_deg; SGPropertyNode_ptr target_altitude_ft; SGPropertyNode_ptr altitude_lock; - SGPropertyNode_ptr wp0_id; - SGPropertyNode_ptr wp0_dist; - SGPropertyNode_ptr wp0_eta; - - SGPropertyNode_ptr wp1_id; - SGPropertyNode_ptr wp1_dist; - SGPropertyNode_ptr wp1_eta; - - SGPropertyNode_ptr wpn_id; - SGPropertyNode_ptr wpn_dist; - SGPropertyNode_ptr wpn_eta; - - - class Listener : public SGPropertyChangeListener { + SGPropertyNode_ptr departure; ///< departure airport information + SGPropertyNode_ptr destination; ///< destination airport information + SGPropertyNode_ptr alternate; ///< alternate airport information + SGPropertyNode_ptr cruise; ///< cruise information + + SGPropertyNode_ptr totalDistance; + SGPropertyNode_ptr ete; + SGPropertyNode_ptr elapsedFlightTime; + + SGPropertyNode_ptr active; + SGPropertyNode_ptr airborne; + SGPropertyNode_ptr currentWp; + + SGPropertyNode_ptr wp0; + SGPropertyNode_ptr wp1; + SGPropertyNode_ptr wpn; + + + SGPropertyNode_ptr _pathNode; + + void setETAPropertyFromDistance(SGPropertyNode_ptr aProp, double aDistance); + + class InputListener : public SGPropertyChangeListener { public: - Listener(FGRouteMgr *m) : mgr(m) {} + InputListener(FGRouteMgr *m) : mgr(m) {} virtual void valueChanged (SGPropertyNode * prop); private: FGRouteMgr *mgr; }; SGPropertyNode_ptr input; - Listener *listener; + SGPropertyNode_ptr weightOnWheels; + + InputListener *listener; SGPropertyNode_ptr mirror; bool altitude_set; + /** + * Create a SGWayPoint from a string in the following format: + * - simple identifier + * - decimal-lon,decimal-lat + * - airport-id/runway-id + * - navaid/radial-deg/offset-nm + */ SGWayPoint* make_waypoint(const string& target); + + void update_mirror(); bool near_ground(); - - /** - * Helper to set a string property to the estimated arrival time (ETA), - * formatted as either hours:minutes or minutes:seconds, based on a distance - * and the current groundspeed. - */ - void setETAPropertyFromDistance(SGPropertyNode_ptr aProp, double aDistance); + + void currentWaypointChanged(); /** * Helper to update the target_altitude_ft and altitude_set flag when wp0 * changes */ void updateTargetAltitude(); + + /** + * Parse a route/wp node (from a saved, property-lsit formatted route) + */ + void parseRouteWaypoint(SGPropertyNode* aWP); public: FGRouteMgr(); @@ -110,14 +136,41 @@ public: void add_waypoint( const SGWayPoint& wp, int n = -1 ); SGWayPoint pop_waypoint( int i = 0 ); - SGWayPoint get_waypoint( int i ) const { - return route->get_waypoint(i); - } - - int size() const { - return route->size(); - } + SGWayPoint get_waypoint( int i ) const; + int size() const; + + bool isRouteActive() const; + + int currentWaypoint() const; + + /** + * Find a waypoint in the route, by position, and return its index, or + * -1 if no matching waypoint was found in the route. + */ + int findWaypoint(const SGGeod& aPos) const; + + /** + * Activate a built route. This checks for various mandatory pieces of + * data, such as departure and destination airports, and creates waypoints + * for them on the route structure. + * + * returns true if the route was activated successfully, or false if the + * route could not be activated for some reason + */ + bool activate(); + /** + * Step to the next waypoint on the active route + */ + void sequence(); + + /** + * + */ + void jumpToIndex(int index); + + void saveRoute(); + void loadRoute(); }; diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index 2d448f575..a0c1cdbd2 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -9,11 +9,16 @@ #include "gps.hxx" -#include -#include -#include
-#include
// for fgLowPass -#include +#include + +#include "Main/fg_props.hxx" +#include "Main/globals.hxx" // for get_subsystem +#include "Main/util.hxx" // for fgLowPass +#include "Navaids/positioned.hxx" +#include "Navaids/navrecord.hxx" +#include "Airports/simple.hxx" +#include "Airports/runways.hxx" +#include "Autopilot/route_mgr.hxx" #include #include @@ -21,7 +26,8 @@ using std::string; - +/////////////////////////////////////////////////////////////////// + void SGGeodProperty::init(SGPropertyNode* base, const char* lonStr, const char* latStr, const char* altStr) { _lon = base->getChild(lonStr, 0, true); @@ -65,14 +71,203 @@ SGGeod SGGeodProperty::get() const } } +static void tieSGGeod(SGPropertyNode* aNode, SGGeod& aRef, + const char* lonStr, const char* latStr, const char* altStr) +{ + aNode->tie(lonStr, SGRawValueMethods(aRef, &SGGeod::getLongitudeDeg, &SGGeod::setLongitudeDeg)); + aNode->tie(latStr, SGRawValueMethods(aRef, &SGGeod::getLatitudeDeg, &SGGeod::setLatitudeDeg)); + + if (altStr) { + aNode->tie(altStr, SGRawValueMethods(aRef, &SGGeod::getElevationFt, &SGGeod::setElevationFt)); + } +} + +static void tieSGGeodReadOnly(SGPropertyNode* aNode, SGGeod& aRef, + const char* lonStr, const char* latStr, const char* altStr) +{ + aNode->tie(lonStr, SGRawValueMethods(aRef, &SGGeod::getLongitudeDeg, NULL)); + aNode->tie(latStr, SGRawValueMethods(aRef, &SGGeod::getLatitudeDeg, NULL)); + + if (altStr) { + aNode->tie(altStr, SGRawValueMethods(aRef, &SGGeod::getElevationFt, NULL)); + } +} -GPS::GPS ( SGPropertyNode *node) - : _last_valid(false), - _alt_dist_ratio(0), - _distance_m(0), - _course_deg(0), - _name(node->getStringValue("name", "gps")), - _num(node->getIntValue("number", 0)) +static const char* makeTTWString(double TTW) +{ + if ((TTW <= 0.0) || (TTW >= 356400.5)) { // 99 hours + return "--:--:--"; + } + + unsigned int TTW_seconds = (int) (TTW + 0.5); + unsigned int TTW_minutes = 0; + unsigned int TTW_hours = 0; + static char TTW_str[9]; + TTW_hours = TTW_seconds / 3600; + TTW_minutes = (TTW_seconds / 60) % 60; + TTW_seconds = TTW_seconds % 60; + snprintf(TTW_str, 9, "%02d:%02d:%02d", + TTW_hours, TTW_minutes, TTW_seconds); + return TTW_str; +} + +///////////////////////////////////////////////////////////////////////////// + +class GPSListener : public SGPropertyChangeListener +{ +public: + GPSListener(GPS *m) : + _gps(m), + _guard(false) {} + + virtual void valueChanged (SGPropertyNode * prop) + { + if (_guard) { + return; + } + + _guard = true; + if (prop == _gps->_route_current_wp_node) { + _gps->routeManagerSequenced(); + } else if (prop == _gps->_route_active_node) { + _gps->routeActivated(); + } else if (prop == _gps->_ref_navaid_id_node) { + _gps->referenceNavaidSet(prop->getStringValue("")); + } + + _guard = false; + } + + void setGuard(bool g) { + _guard = g; + } +private: + GPS* _gps; + bool _guard; // re-entrancy guard +}; + +//////////////////////////////////////////////////////////////////////////// +/** + * Helper to monitor for Nasal or other code accessing properties we haven't + * defined. For the moment we complain about all such activites, since various + * users assume all kinds of weird, wonderful and non-existent interfaces. + */ + +class DeprecatedPropListener : public SGPropertyChangeListener +{ +public: + DeprecatedPropListener(SGPropertyNode* gps) + { + _parents.insert(gps); + SGPropertyNode* wp = gps->getChild("wp"); + _parents.insert(wp); + _parents.insert(wp->getChild("wp", 0)); + _parents.insert(wp->getChild("wp", 1)); + + std::set::iterator it; + for (it = _parents.begin(); it != _parents.end(); ++it) { + (*it)->addChangeListener(this); + } + } + + virtual void valueChanged (SGPropertyNode * prop) + { + } + + virtual void childAdded (SGPropertyNode * parent, SGPropertyNode * child) + { + if (isDeprecated(parent, child)) { + SG_LOG(SG_INSTR, SG_WARN, "GPS: someone accessed a deprecated property:" + << child->getPath(true)); + } + } +private: + bool isDeprecated(SGPropertyNode * parent, SGPropertyNode * child) const + { + if (_parents.count(parent) < 1) { + return false; + } + + // no child exclusions yet + return true; + } + + std::set _parents; +}; + +//////////////////////////////////////////////////////////////////////////// +// configuration helper object + +GPS::Config::Config() : + _enableTurnAnticipation(true), + _turnRate(3.0), // degrees-per-second, so 180 degree turn takes 60 seconds + _overflightArmDistance(0.5), + _waypointAlertTime(30.0), + _tuneRadio1ToRefVor(true), + _minRunwayLengthFt(0.0), + _requireHardSurface(true), + _cdiMaxDeflectionNm(-1) // default to angular mode +{ + _enableTurnAnticipation = false; + _obsCourseSource = fgGetNode("/instrumentation/nav[0]/radials/selected-deg", true); +} + +void GPS::Config::init(SGPropertyNode* aCfgNode) +{ + aCfgNode->tie("turn-rate-deg-sec", SGRawValuePointer(&_turnRate)); + aCfgNode->tie("turn-anticipation", SGRawValuePointer(&_enableTurnAnticipation)); + aCfgNode->tie("wpt-alert-time", SGRawValuePointer(&_waypointAlertTime)); + aCfgNode->tie("tune-nav-radio-to-ref-vor", SGRawValuePointer(&_tuneRadio1ToRefVor)); + aCfgNode->tie("min-runway-length-ft", SGRawValuePointer(&_minRunwayLengthFt)); + aCfgNode->tie("hard-surface-runways-only", SGRawValuePointer(&_requireHardSurface)); + + aCfgNode->tie("obs-course-source", SGRawValueMethods + (*this, &GPS::Config::getOBSCourseSource, &GPS::Config::setOBSCourseSource)); + + aCfgNode->tie("cdi-max-deflection-nm", SGRawValuePointer(&_cdiMaxDeflectionNm)); +} + +const char* +GPS::Config::getOBSCourseSource() const +{ + if (!_obsCourseSource) { + return ""; + } + + return _obsCourseSource->getPath(true); +} + +void +GPS::Config::setOBSCourseSource(const char* aPath) +{ + SGPropertyNode* nd = fgGetNode(aPath, false); + if (!nd) { + SG_LOG(SG_INSTR, SG_WARN, "couldn't find OBS course source at:" << aPath); + _obsCourseSource = NULL; + } + + _obsCourseSource = nd; +} + +double +GPS::Config::getOBSCourse() const +{ + if (!_obsCourseSource) { + return 0.0; + } + + return _obsCourseSource->getDoubleValue(); +} + +//////////////////////////////////////////////////////////////////////////// + +GPS::GPS ( SGPropertyNode *node) : + _last_valid(false), + _name(node->getStringValue("name", "gps")), + _num(node->getIntValue("number", 0)), + _computeTurnData(false), + _anticipateTurn(false), + _inTurn(false) { } @@ -83,137 +278,188 @@ GPS::~GPS () void GPS::init () { + _routeMgr = (FGRouteMgr*) globals->get_subsystem("route-manager"); + assert(_routeMgr); + string branch; branch = "/instrumentation/" + _name; - SGPropertyNode *node = fgGetNode(branch.c_str(), _num, true ); - _position.init("/position/longitude-deg", "/position/latitude-deg", "/position/altitude-ft"); - _magvar_node = fgGetNode("/environment/magnetic-variation-deg", true); - _serviceable_node = node->getChild("serviceable", 0, true); - _electrical_node = fgGetNode("/systems/electrical/outputs/gps", true); - - SGPropertyNode *wp_node = node->getChild("wp", 0, true); - SGPropertyNode *wp0_node = wp_node->getChild("wp", 0, true); - SGPropertyNode *wp1_node = wp_node->getChild("wp", 1, true); - - _wp0_position.init(wp0_node, "longitude-deg", "latitude-deg", "altitude-ft"); - _wp0_ID_node = wp0_node->getChild("ID", 0, true); - _wp0_name_node = wp0_node->getChild("name", 0, true); - _wp0_course_node = wp0_node->getChild("desired-course-deg", 0, true); - _wp0_distance_node = wp0_node->getChild("distance-nm", 0, true); - _wp0_ttw_node = wp0_node->getChild("TTW", 0, true); - _wp0_bearing_node = wp0_node->getChild("bearing-true-deg", 0, true); - _wp0_mag_bearing_node = wp0_node->getChild("bearing-mag-deg", 0, true); - _wp0_course_deviation_node = - wp0_node->getChild("course-deviation-deg", 0, true); - _wp0_course_error_nm_node = wp0_node->getChild("course-error-nm", 0, true); - _wp0_to_flag_node = wp0_node->getChild("to-flag", 0, true); - _true_wp0_bearing_error_node = - wp0_node->getChild("true-bearing-error-deg", 0, true); - _magnetic_wp0_bearing_error_node = - wp0_node->getChild("magnetic-bearing-error-deg", 0, true); - - _wp1_position.init(wp1_node, "longitude-deg", "latitude-deg", "altitude-ft"); - _wp1_ID_node = wp1_node->getChild("ID", 0, true); - _wp1_name_node = wp1_node->getChild("name", 0, true); - _wp1_course_node = wp1_node->getChild("desired-course-deg", 0, true); - _wp1_distance_node = wp1_node->getChild("distance-nm", 0, true); - _wp1_ttw_node = wp1_node->getChild("TTW", 0, true); - _wp1_bearing_node = wp1_node->getChild("bearing-true-deg", 0, true); - _wp1_mag_bearing_node = wp1_node->getChild("bearing-mag-deg", 0, true); - _wp1_course_deviation_node = - wp1_node->getChild("course-deviation-deg", 0, true); - _wp1_course_error_nm_node = wp1_node->getChild("course-error-nm", 0, true); - _wp1_to_flag_node = wp1_node->getChild("to-flag", 0, true); - _true_wp1_bearing_error_node = - wp1_node->getChild("true-bearing-error-deg", 0, true); - _magnetic_wp1_bearing_error_node = - wp1_node->getChild("magnetic-bearing-error-deg", 0, true); - _get_nearest_airport_node = - wp1_node->getChild("get-nearest-airport", 0, true); - - _tracking_bug_node = node->getChild("tracking-bug", 0, true); - _raim_node = node->getChild("raim", 0, true); - - _indicated_pos.init(node, "indicated-longitude-deg", + SGPropertyNode *node = fgGetNode(branch.c_str(), _num, true ); + _config.init(node->getChild("config", 0, true)); + + _position.init("/position/longitude-deg", "/position/latitude-deg", "/position/altitude-ft"); + _magvar_node = fgGetNode("/environment/magnetic-variation-deg", true); + _serviceable_node = node->getChild("serviceable", 0, true); + _serviceable_node->setBoolValue(true); + _electrical_node = fgGetNode("/systems/electrical/outputs/gps", true); + +// basic GPS outputs + node->tie("selected-course-deg", SGRawValueMethods(*this, &GPS::getSelectedCourse, NULL)); + + _raim_node = node->getChild("raim", 0, true); + + tieSGGeodReadOnly(node, _indicated_pos, "indicated-longitude-deg", "indicated-latitude-deg", "indicated-altitude-ft"); + + node->tie("indicated-vertical-speed", SGRawValueMethods + (*this, &GPS::getVerticalSpeed, NULL)); + node->tie("indicated-track-true-deg", SGRawValueMethods + (*this, &GPS::getTrueTrack, NULL)); + node->tie("indicated-track-magnetic-deg", SGRawValueMethods + (*this, &GPS::getMagTrack, NULL)); + node->tie("indicated-ground-speed-kt", SGRawValueMethods + (*this, &GPS::getGroundspeedKts, NULL)); - _indicated_vertical_speed_node = - node->getChild("indicated-vertical-speed", 0, true); - _true_track_node = - node->getChild("indicated-track-true-deg", 0, true); - _magnetic_track_node = - node->getChild("indicated-track-magnetic-deg", 0, true); - _speed_node = - node->getChild("indicated-ground-speed-kt", 0, true); - _odometer_node = - node->getChild("odometer", 0, true); - _trip_odometer_node = - node->getChild("trip-odometer", 0, true); - _true_bug_error_node = - node->getChild("true-bug-error-deg", 0, true); - _magnetic_bug_error_node = - node->getChild("magnetic-bug-error-deg", 0, true); - - _leg_distance_node = - wp_node->getChild("leg-distance-nm", 0, true); - _leg_course_node = - wp_node->getChild("leg-true-course-deg", 0, true); - _leg_magnetic_course_node = - wp_node->getChild("leg-mag-course-deg", 0, true); - _alt_dist_ratio_node = - wp_node->getChild("alt-dist-ratio", 0, true); - _leg_course_deviation_node = - wp_node->getChild("leg-course-deviation-deg", 0, true); - _leg_course_error_nm_node = - wp_node->getChild("leg-course-error-nm", 0, true); - _leg_to_flag_node = - wp_node->getChild("leg-to-flag", 0, true); - _alt_deviation_node = - wp_node->getChild("alt-deviation-ft", 0, true); + _odometer_node = node->getChild("odometer", 0, true); + _trip_odometer_node = node->getChild("trip-odometer", 0, true); + _true_bug_error_node = node->getChild("true-bug-error-deg", 0, true); + _magnetic_bug_error_node = node->getChild("magnetic-bug-error-deg", 0, true); + +// command system + _mode = "obs"; + node->tie("mode", SGRawValueMethods(*this, &GPS::getMode, NULL)); + node->tie("command", SGRawValueMethods(*this, &GPS::getCommand, &GPS::setCommand)); + + _scratchNode = node->getChild("scratch", 0, true); + tieSGGeod(_scratchNode, _scratchPos, "longitude-deg", "latitude-deg", "altitude-ft"); + _scratchNode->tie("valid", SGRawValueMethods(*this, &GPS::getScratchValid, NULL)); + _scratchNode->tie("distance-nm", SGRawValueMethods(*this, &GPS::getScratchDistance, NULL)); + _scratchNode->tie("true-bearing-deg", SGRawValueMethods(*this, &GPS::getScratchTrueBearing, NULL)); + _scratchNode->tie("mag-bearing-deg", SGRawValueMethods(*this, &GPS::getScratchMagBearing, NULL)); + _scratchNode->tie("has-next", SGRawValueMethods(*this, &GPS::getScratchHasNext, NULL)); + _scratchValid = false; + +// waypoint data (including various historical things) + SGPropertyNode *wp_node = node->getChild("wp", 0, true); + SGPropertyNode *wp0_node = wp_node->getChild("wp", 0, true); + SGPropertyNode *wp1_node = wp_node->getChild("wp", 1, true); + + tieSGGeodReadOnly(wp0_node, _wp0_position, "longitude-deg", "latitude-deg", "altitude-ft"); + wp0_node->tie("ID", SGRawValueMethods + (*this, &GPS::getWP0Ident, NULL)); + wp0_node->tie("name", SGRawValueMethods + (*this, &GPS::getWP0Name, NULL)); + + tieSGGeodReadOnly(wp1_node, _wp1_position, "longitude-deg", "latitude-deg", "altitude-ft"); + wp1_node->tie("ID", SGRawValueMethods + (*this, &GPS::getWP1Ident, NULL)); + wp1_node->tie("name", SGRawValueMethods + (*this, &GPS::getWP1Name, NULL)); + + // for compatability, alias selected course down to wp/wp[1]/desired-course-deg + SGPropertyNode* wp1Crs = wp1_node->getChild("desired-course-deg", 0, true); + wp1Crs->alias(node->getChild("selected-course-deg")); + +// _true_wp1_bearing_error_node = +// wp1_node->getChild("true-bearing-error-deg", 0, true); +// _magnetic_wp1_bearing_error_node = + // wp1_node->getChild("magnetic-bearing-error-deg", 0, true); + + wp1_node->tie("distance-nm", SGRawValueMethods + (*this, &GPS::getWP1Distance, NULL)); + wp1_node->tie("bearing-true-deg", SGRawValueMethods + (*this, &GPS::getWP1Bearing, NULL)); + wp1_node->tie("bearing-mag-deg", SGRawValueMethods + (*this, &GPS::getWP1MagBearing, NULL)); + wp1_node->tie("TTW-sec", SGRawValueMethods + (*this, &GPS::getWP1TTW, NULL)); + wp1_node->tie("TTW", SGRawValueMethods + (*this, &GPS::getWP1TTWString, NULL)); + + wp1_node->tie("course-deviation-deg", SGRawValueMethods + (*this, &GPS::getWP1CourseDeviation, NULL)); + wp1_node->tie("course-error-nm", SGRawValueMethods + (*this, &GPS::getWP1CourseErrorNm, NULL)); + wp1_node->tie("to-flag", SGRawValueMethods + (*this, &GPS::getWP1ToFlag, NULL)); + + _tracking_bug_node = node->getChild("tracking-bug", 0, true); + +// leg properties (only valid in DTO/LEG modes, not OBS) + wp_node->tie("leg-distance-nm", SGRawValueMethods(*this, &GPS::getLegDistance, NULL)); + wp_node->tie("leg-true-course-deg", SGRawValueMethods(*this, &GPS::getLegCourse, NULL)); + wp_node->tie("leg-mag-course-deg", SGRawValueMethods(*this, &GPS::getLegMagCourse, NULL)); + wp_node->tie("alt-dist-ratio", SGRawValueMethods(*this, &GPS::getAltDistanceRatio, NULL)); + +// reference navid + SGPropertyNode_ptr ref_navaid = node->getChild("ref-navaid", 0, true); + _ref_navaid_id_node = ref_navaid->getChild("id", 0, true); + _ref_navaid_name_node = ref_navaid->getChild("name", 0, true); + _ref_navaid_bearing_node = ref_navaid->getChild("bearing-deg", 0, true); + _ref_navaid_frequency_node = ref_navaid->getChild("frequency-mhz", 0, true); + _ref_navaid_distance_node = ref_navaid->getChild("distance-nm", 0, true); + _ref_navaid_mag_bearing_node = ref_navaid->getChild("mag-bearing-deg", 0, true); + _ref_navaid_elapsed = 0.0; + _ref_navaid_set = false; + +// route properties + // should these move to the route manager? + _routeDistanceNm = node->getChild("route-distance-nm", 0, true); + _routeETE = node->getChild("ETE", 0, true); + + // disable auto-sequencing in the route manager; we'll deal with it + // ourselves using turn anticipation + SGPropertyNode_ptr autoSeq = fgGetNode("/autopilot/route-manager/auto-sequence", true); + autoSeq->setBoolValue(false); + +// add listener to various things + _listener = new GPSListener(this); + _route_current_wp_node = fgGetNode("/autopilot/route-manager/current-wp", true); + _route_current_wp_node->addChangeListener(_listener); + _route_active_node = fgGetNode("/autopilot/route-manager/active", true); + _route_active_node->addChangeListener(_listener); + + _ref_navaid_id_node->addChangeListener(_listener); - _serviceable_node->setBoolValue(true); +// navradio slaving properties + node->tie("cdi-deflection", SGRawValueMethods + (*this, &GPS::getCDIDeflection)); + + SGPropertyNode* toFlag = node->getChild("to-flag", 0, true); + toFlag->alias(wp1_node->getChild("to-flag")); + + _fromFlagNode = node->getChild("from-flag", 0, true); + + // last thing, add the deprecated prop watcher + new DeprecatedPropListener(node); } void GPS::clearOutput() { - _last_valid = false; - _last_speed_kts = 0; - _last_pos = SGGeod(); - _raim_node->setDoubleValue(false); - _indicated_pos = SGGeod(); - _indicated_vertical_speed_node->setDoubleValue(0); - _true_track_node->setDoubleValue(0); - _magnetic_track_node->setDoubleValue(0); - _speed_node->setDoubleValue(0); - _wp1_distance_node->setDoubleValue(0); - _wp1_bearing_node->setDoubleValue(0); - _wp1_position = SGGeod(); - _wp1_course_node->setDoubleValue(0); - _odometer_node->setDoubleValue(0); - _trip_odometer_node->setDoubleValue(0); - _tracking_bug_node->setDoubleValue(0); - _true_bug_error_node->setDoubleValue(0); - _magnetic_bug_error_node->setDoubleValue(0); - _true_wp1_bearing_error_node->setDoubleValue(0); - _magnetic_wp1_bearing_error_node->setDoubleValue(0); + _last_valid = false; + _last_speed_kts = 0.0; + _last_pos = SGGeod(); + _indicated_pos = SGGeod(); + _last_vertical_speed = 0.0; + _last_true_track = 0.0; + + _raim_node->setDoubleValue(false); + _indicated_pos = SGGeod(); + _wp1DistanceM = 0.0; + _wp1TrueBearing = 0.0; + _wp1_position = SGGeod(); + _odometer_node->setDoubleValue(0); + _trip_odometer_node->setDoubleValue(0); + _tracking_bug_node->setDoubleValue(0); + _true_bug_error_node->setDoubleValue(0); + _magnetic_bug_error_node->setDoubleValue(0); + + _fromFlagNode->setBoolValue(false); } void GPS::update (double delta_time_sec) { - // If it's off, don't bother. - if (!_serviceable_node->getBoolValue() || !_electrical_node->getBoolValue()) { - clearOutput(); - return; - } + // If it's off, don't bother. + if (!_serviceable_node->getBoolValue() || !_electrical_node->getBoolValue()) { + clearOutput(); + return; + } - UpdateContext ctx; - ctx.dt = delta_time_sec; - ctx.waypoint_changed = false; - ctx.pos = _position.get(); - + if (delta_time_sec <= 0.0) { + return; // paused, don't bother + } // TODO: Add noise and other errors. /* @@ -257,341 +503,1061 @@ GPS::update (double delta_time_sec) */ _raim_node->setBoolValue(true); - _indicated_pos = ctx.pos; + _indicated_pos = _position.get(); if (_last_valid) { - updateWithValid(ctx); + updateWithValid(delta_time_sec); } else { - _true_track_node->setDoubleValue(0.0); - _magnetic_track_node->setDoubleValue(0.0); - _speed_node->setDoubleValue(0.0); - _last_valid = true; + _last_valid = true; + + if (_route_active_node->getBoolValue()) { + // GPS init with active route + SG_LOG(SG_INSTR, SG_INFO, "GPS init with active route"); + _listener->setGuard(true); + routeActivated(); + routeManagerSequenced(); + _listener->setGuard(false); + } } - _last_pos = ctx.pos; + _last_pos = _indicated_pos; } void -GPS::updateNearestAirport(UpdateContext& ctx) +GPS::updateWithValid(double dt) { - if (!_get_nearest_airport_node->getBoolValue()) { - return; - } + assert(_last_valid); - // If the get-nearest-airport-node is true. - // Get the nearest airport, and set it as waypoint 1. + updateBasicData(dt); + + if (_mode == "obs") { + _selectedCourse = _config.getOBSCourse(); + } else { + updateTurn(); + } - FGPositioned::TypeFilter aptFilter(FGPositioned::AIRPORT); - FGPositionedRef a = FGPositioned::findClosest(ctx.pos, 360.0, &aptFilter); - if (!a) { - return; - } - - _wp1_position = a->geod(); - _wp1_ID_node->setStringValue(a->ident().c_str()); - _wp1_name_node->setStringValue(a->name().c_str()); - _get_nearest_airport_node->setBoolValue(false); - _last_wp1_ID = a->ident(); // don't trigger updateWaypoint1(); - ctx.waypoint_changed = true; + updateWaypoints(); + updateTrackingBug(); + updateReferenceNavaid(dt); + updateRouteData(); } void -GPS::updateWithValid(UpdateContext& ctx) +GPS::updateBasicData(double dt) { - assert(_last_valid); - double distance_m; - SGGeodesy::inverse(_last_pos, ctx.pos, ctx.track1_deg, ctx.track2_deg, distance_m ); + double distance_m; + double track2_deg; + SGGeodesy::inverse(_last_pos, _indicated_pos, _last_true_track, track2_deg, distance_m ); - ctx.speed_kt = ((distance_m * SG_METER_TO_NM) * ((1 / ctx.dt) * 3600.0)); - - double vertical_speed_mpm = ((ctx.pos.getElevationM() - _last_pos.getElevationM()) * 60 / - ctx.dt); - _indicated_vertical_speed_node->setDoubleValue(vertical_speed_mpm * SG_METER_TO_FEET); - _true_track_node->setDoubleValue(ctx.track1_deg); - - ctx.magvar_deg = _magvar_node->getDoubleValue(); - double mag_track_bearing = ctx.track1_deg - ctx.magvar_deg; - SG_NORMALIZE_RANGE(mag_track_bearing, 0.0, 360.0); - _magnetic_track_node->setDoubleValue(mag_track_bearing); - ctx.speed_kt = fgGetLowPass(_last_speed_kts, ctx.speed_kt, ctx.dt/20.0); - _last_speed_kts = ctx.speed_kt; - _speed_node->setDoubleValue(ctx.speed_kt); - - double odometer = _odometer_node->getDoubleValue(); - _odometer_node->setDoubleValue(odometer + distance_m * SG_METER_TO_NM); - odometer = _trip_odometer_node->getDoubleValue(); - _trip_odometer_node->setDoubleValue(odometer + distance_m * SG_METER_TO_NM); - - updateNearestAirport(ctx); - updateWaypoint0(ctx); - updateWaypoint1(ctx); - - ctx.wp0_pos = _wp0_position.get(); - ctx.wp1_pos = _wp1_position.get(); - // if this flag is set, we need to recompute leg data, because either - // WP0 or WP1 has been updated - if (ctx.waypoint_changed) { - waypointChanged(ctx); - } + double speed_kt = ((distance_m * SG_METER_TO_NM) * ((1 / dt) * 3600.0)); + double vertical_speed_mpm = ((_indicated_pos.getElevationM() - _last_pos.getElevationM()) * 60 / dt); + _last_vertical_speed = vertical_speed_mpm * SG_METER_TO_FEET; + + speed_kt = fgGetLowPass(_last_speed_kts, speed_kt, dt/20.0); + _last_speed_kts = speed_kt; - ctx.wp0_course_deg = _wp0_course_node->getDoubleValue(); - ctx.wp1_course_deg = _wp1_course_node->getDoubleValue(); - - updateWaypoint0Course(ctx); - updateWaypoint1Course(ctx); - updateLegCourse(ctx); - - // Altitude deviation - //double desired_altitude_m = wp1_altitude_m - // + wp1_distance * _alt_dist_ratio; - //double altitude_deviation_m = altitude_m - desired_altitude_m; - // _alt_deviation_node->setDoubleValue(altitude_deviation_m * SG_METER_TO_FEET); - - updateTrackingBug(ctx); + double odometer = _odometer_node->getDoubleValue(); + _odometer_node->setDoubleValue(odometer + distance_m * SG_METER_TO_NM); + odometer = _trip_odometer_node->getDoubleValue(); + _trip_odometer_node->setDoubleValue(odometer + distance_m * SG_METER_TO_NM); } void -GPS::updateLegCourse(UpdateContext& ctx) +GPS::updateTrackingBug() { - // Leg course deviation is the diffenrence between the bearing - // and the course. - double course_deviation_deg = ctx.wp1_bearing_deg - _course_deg; - SG_NORMALIZE_RANGE(course_deviation_deg, -180.0, 180.0); - - // If the course deviation is less than 90 degrees to either side, - // our desired course is towards the waypoint. - // It does not matter if we are actually moving - // towards or from the waypoint. - if (fabs(course_deviation_deg) < 90.0) { - _leg_to_flag_node->setBoolValue(true); - } - // If it's more than 90 degrees the desired - // course is from the waypoint. - else if (fabs(course_deviation_deg) > 90.0) { - _leg_to_flag_node->setBoolValue(false); - // When the course is away from the waypoint, - // it makes sense to change the sign of the deviation. - course_deviation_deg *= -1.0; - SG_NORMALIZE_RANGE(course_deviation_deg, -90.0, 90.0); - } - - _leg_course_deviation_node->setDoubleValue(course_deviation_deg); - - // Cross track error. - double course_error_m = sin(course_deviation_deg * SG_PI / 180.0) - * (_distance_m); - _leg_course_error_nm_node->setDoubleValue(course_error_m * SG_METER_TO_NM); + double tracking_bug = _tracking_bug_node->getDoubleValue(); + double true_bug_error = tracking_bug - getTrueTrack(); + double magnetic_bug_error = tracking_bug - getMagTrack(); + // Get the errors into the (-180,180) range. + SG_NORMALIZE_RANGE(true_bug_error, -180.0, 180.0); + SG_NORMALIZE_RANGE(magnetic_bug_error, -180.0, 180.0); + + _true_bug_error_node->setDoubleValue(true_bug_error); + _magnetic_bug_error_node->setDoubleValue(magnetic_bug_error); } void -GPS::updateTrackingBug(UpdateContext& ctx) +GPS::updateWaypoints() +{ + double az2; + SGGeodesy::inverse(_indicated_pos, _wp1_position, _wp1TrueBearing, az2,_wp1DistanceM); + bool toWp = getWP1ToFlag(); + _fromFlagNode->setBoolValue(!toWp); +} + +void GPS::updateReferenceNavaid(double dt) +{ + if (!_ref_navaid_set) { + _ref_navaid_elapsed += dt; + if (_ref_navaid_elapsed > 5.0) { + _ref_navaid_elapsed = 0.0; + + FGPositioned::TypeFilter vorFilter(FGPositioned::VOR); + FGPositionedRef nav = FGPositioned::findClosest(_indicated_pos, 400.0, &vorFilter); + if (!nav) { + SG_LOG(SG_INSTR, SG_INFO, "GPS couldn't find a reference navid"); + _ref_navaid_id_node->setStringValue(""); + _ref_navaid_name_node->setStringValue(""); + _ref_navaid_bearing_node->setDoubleValue(0.0); + _ref_navaid_mag_bearing_node->setDoubleValue(0.0); + _ref_navaid_distance_node->setDoubleValue(0.0); + _ref_navaid_frequency_node->setStringValue(""); + } else if (nav != _ref_navaid) { + SG_LOG(SG_INSTR, SG_INFO, "GPS code selected new ref-navaid:" << nav->ident()); + _listener->setGuard(true); + _ref_navaid_id_node->setStringValue(nav->ident().c_str()); + _ref_navaid_name_node->setStringValue(nav->name().c_str()); + FGNavRecord* vor = (FGNavRecord*) nav.ptr(); + _ref_navaid_frequency_node->setDoubleValue(vor->get_freq() / 100.0); + _listener->setGuard(false); + tuneNavRadios(); + } else { + // SG_LOG(SG_INSTR, SG_ALERT, "matched existing"); + } + + _ref_navaid = nav; + } + } + + if (_ref_navaid) { + double trueCourse, distanceM, az2; + SGGeodesy::inverse(_indicated_pos, _ref_navaid->geod(), trueCourse, az2, distanceM); + _ref_navaid_distance_node->setDoubleValue(distanceM * SG_METER_TO_NM); + _ref_navaid_bearing_node->setDoubleValue(trueCourse); + _ref_navaid_mag_bearing_node->setDoubleValue(trueCourse - _magvar_node->getDoubleValue()); + } +} + +void GPS::referenceNavaidSet(const std::string& aNavaid) { - double tracking_bug = _tracking_bug_node->getDoubleValue(); - double true_bug_error = tracking_bug - ctx.track1_deg; - double magnetic_bug_error = tracking_bug - _magnetic_track_node->getDoubleValue(); + _ref_navaid = NULL; + // allow setting an empty string to restore normal nearest-vor selection + if (aNavaid.size() > 0) { + FGPositioned::TypeFilter vorFilter(FGPositioned::VOR); + _ref_navaid = FGPositioned::findClosestWithIdent(aNavaid, + _position.get(), &vorFilter); + + if (!_ref_navaid) { + SG_LOG(SG_INSTR, SG_ALERT, "GPS: unknown ref navaid:" << aNavaid); + } + } - // Get the errors into the (-180,180) range. - SG_NORMALIZE_RANGE(true_bug_error, -180.0, 180.0); - SG_NORMALIZE_RANGE(magnetic_bug_error, -180.0, 180.0); + if (_ref_navaid) { + _ref_navaid_set = true; + SG_LOG(SG_INSTR, SG_INFO, "GPS code set explict ref-navaid:" << _ref_navaid->ident()); + _ref_navaid_id_node->setStringValue(_ref_navaid->ident().c_str()); + _ref_navaid_name_node->setStringValue(_ref_navaid->name().c_str()); + FGNavRecord* vor = (FGNavRecord*) _ref_navaid.ptr(); + _ref_navaid_frequency_node->setDoubleValue(vor->get_freq() / 100.0); + tuneNavRadios(); + } else { + _ref_navaid_set = false; + _ref_navaid_elapsed = 9999.0; // update next tick + } +} - _true_bug_error_node->setDoubleValue(true_bug_error); - _magnetic_bug_error_node->setDoubleValue(magnetic_bug_error); +void GPS::tuneNavRadios() +{ + if (!_ref_navaid || !_config.tuneNavRadioToRefVor()) { + return; + } + + SGPropertyNode_ptr navRadio1 = fgGetNode("/instrumentation/nav", false); + if (!navRadio1) { + return; + } + + FGNavRecord* vor = (FGNavRecord*) _ref_navaid.ptr(); + SGPropertyNode_ptr freqs = navRadio1->getChild("frequencies"); + freqs->setDoubleValue("selected-mhz", vor->get_freq() / 100.0); } -void -GPS::waypointChanged(UpdateContext& ctx) +void GPS::routeActivated() { - // If any of the two waypoints have changed - // we need to calculate a new course between them, - // and values for vertical navigation. - assert(ctx.waypoint_changed); + if (_route_active_node->getBoolValue()) { + SG_LOG(SG_INSTR, SG_INFO, "GPS::route activated, switching to LEG mode"); + _mode = "leg"; + _computeTurnData = true; + } else if (_mode == "leg") { + SG_LOG(SG_INSTR, SG_INFO, "GPS::route deactivated, switching to OBS mode"); + _mode = "obs"; + } +} - double track2; - SGGeodesy::inverse(ctx.wp0_pos, ctx.wp1_pos, _course_deg, track2, _distance_m); +void GPS::routeManagerSequenced() +{ + if (_mode != "leg") { + SG_LOG(SG_INSTR, SG_INFO, "GPS ignoring route sequencing, not in LEG mode"); + return; + } + + int index = _routeMgr->currentWaypoint(), + count = _routeMgr->size(); + if ((index < 1) || (index >= count)) { + SG_LOG(SG_INSTR, SG_ALERT, "GPS: malformed route, index=" << index); + return; + } + + SG_LOG(SG_INSTR, SG_INFO, "GPS waypoint index is now " << index); + SGWayPoint wp0(_routeMgr->get_waypoint(index - 1)); + SGWayPoint wp1(_routeMgr->get_waypoint(index)); - double leg_mag_course = _course_deg - _magvar_node->getDoubleValue(); - SG_NORMALIZE_RANGE(leg_mag_course, 0.0, 360.0); + _wp0Ident = wp0.get_id(); + _wp0Name = wp0.get_name(); + _wp0_position = wp0.get_target(); - // Get the altitude / distance ratio - if ( _distance_m > 0.0 ) { - double alt_difference_m = ctx.wp0_pos.getElevationM() - ctx.wp1_pos.getElevationM(); - _alt_dist_ratio = alt_difference_m / _distance_m; - } + _wp1Ident = wp1.get_id(); + _wp1Name = wp1.get_name(); + _wp1_position = wp1.get_target(); - _leg_distance_node->setDoubleValue(_distance_m * SG_METER_TO_NM); - _leg_course_node->setDoubleValue(_course_deg); - _leg_magnetic_course_node->setDoubleValue(leg_mag_course); - _alt_dist_ratio_node->setDoubleValue(_alt_dist_ratio); + _selectedCourse = getLegMagCourse(); } -void -GPS::updateWaypoint0(UpdateContext& ctx) +void GPS::updateTurn() { - string id(_wp0_ID_node->getStringValue()); - if (_last_wp0_ID == id) { - return; // easy, nothing to do + bool printProgress = false; + + if (_computeTurnData) { + if (_last_speed_kts < 60) { + // need valid leg course and sensible ground speed to compute the turn + return; } - FGPositionedRef result = FGPositioned::findClosestWithIdent(id, ctx.pos); - if (!result) { - // not found, hmm - _last_wp0_ID = id; - return; - } + computeTurnData(); + printProgress = true; + } + + if (!_anticipateTurn) { + updateOverflight(); + return; + } + + updateTurnData(); + // find bearing to turn centre + double bearing, az2, distanceM; + SGGeodesy::inverse(_indicated_pos, _turnCentre, bearing, az2, distanceM); + double progress = computeTurnProgress(bearing); + + if (printProgress) { + SG_LOG(SG_INSTR, SG_INFO,"turn progress=" << progress); + } + + if (!_inTurn && (progress > 0.0)) { + beginTurn(); + } + + if (_inTurn && !_turnSequenced && (progress > 0.5)) { + _turnSequenced = true; + SG_LOG(SG_INSTR, SG_INFO, "turn passed midpoint, sequencing"); + _routeMgr->sequence(); + } + + if (_inTurn && (progress >= 1.0)) { + endTurn(); + } + + if (_inTurn) { + // drive deviation and desired course + double desiredCourse = bearing - copysign(90, _turnAngle); + SG_NORMALIZE_RANGE(desiredCourse, 0.0, 360.0); + double deviationNm = (distanceM * SG_METER_TO_NM) - _turnRadius; + double deviationDeg = desiredCourse - getMagTrack(); + deviationNm = copysign(deviationNm, deviationDeg); + // FXIME + //_wp1_course_deviation_node->setDoubleValue(deviationDeg); + //_wp1_course_error_nm_node->setDoubleValue(deviationNm); + //_cdiDeflectionNode->setDoubleValue(deviationDeg); + } +} + +void GPS::updateOverflight() +{ + if ((_wp1DistanceM * SG_METER_TO_NM) > _config.overflightArmDistanceNm()) { + return; + } + + if (getWP1ToFlag()) { + return; // still heading towards the WP + } + + if (_mode == "dto") { + SG_LOG(SG_INSTR, SG_INFO, "GPS DTO reached destination point"); - _wp0_position = result->geod(); - _wp0_name_node->setStringValue(result->name().c_str()); - _last_wp0_ID = id; - ctx.waypoint_changed = true; + // check for wp1 being on active route - resume leg mode + if (_routeMgr->isRouteActive()) { + int index = _routeMgr->findWaypoint(_wp1_position); + if (index >= 0) { + SG_LOG(SG_INSTR, SG_INFO, "GPS DTO, resuming LEG mode at wp:" << index); + _mode = "leg"; + _routeMgr->jumpToIndex(index); + } + } + } else if (_mode == "leg") { + SG_LOG(SG_INSTR, SG_INFO, "GPS doing overflight sequencing"); + _routeMgr->sequence(); + } else if (_mode == "obs") { + // nothing to do here, TO/FROM will update but that's fine + } + + _computeTurnData = true; } -void -GPS::updateWaypoint1(UpdateContext& ctx) +void GPS::beginTurn() { - string id(_wp1_ID_node->getStringValue()); - if (_last_wp1_ID == id) { - return; // easy, nothing to do + _inTurn = true; + _turnSequenced = false; + SG_LOG(SG_INSTR, SG_INFO, "begining turn"); +} + +void GPS::endTurn() +{ + _inTurn = false; + SG_LOG(SG_INSTR, SG_INFO, "ending turn"); + _computeTurnData = true; +} + +double GPS::computeTurnProgress(double aBearing) const +{ + double startBearing = _turnStartBearing + copysign(90, _turnAngle); + return (aBearing - startBearing) / _turnAngle; +} + +void GPS::computeTurnData() +{ + _computeTurnData = false; + if (_mode != "leg") { // and approach modes in the future + _anticipateTurn = false; + return; + } + + int curIndex = _routeMgr->currentWaypoint(); + if ((curIndex + 1) >= _routeMgr->size()) { + _anticipateTurn = false; + return; + } + + if (!_config.turnAnticipationEnabled()) { + _anticipateTurn = false; + return; + } + + _turnStartBearing = _selectedCourse; +// compute next leg course + SGWayPoint wp1(_routeMgr->get_waypoint(curIndex)), + wp2(_routeMgr->get_waypoint(curIndex + 1)); + double crs, dist; + wp2.CourseAndDistance(wp1, &crs, &dist); + + +// compute offset bearing + _turnAngle = crs - _turnStartBearing; + SG_NORMALIZE_RANGE(_turnAngle, -180.0, 180.0); + double median = _turnStartBearing + (_turnAngle * 0.5); + double offsetBearing = median + copysign(90, _turnAngle); + SG_NORMALIZE_RANGE(offsetBearing, 0.0, 360.0); + + SG_LOG(SG_INSTR, SG_INFO, "GPS computeTurnData: in=" << _turnStartBearing << + ", out=" << crs << "; turnAngle=" << _turnAngle << ", median=" << median + << ", offset=" << offsetBearing); + + SG_LOG(SG_INSTR, SG_INFO, "next leg is now:" << wp1.get_id() << "->" << wp2.get_id()); + + _turnPt = _wp1_position; + _anticipateTurn = true; +} + +void GPS::updateTurnData() +{ + // depends on ground speed, so needs to be updated per-frame + _turnRadius = computeTurnRadiusNm(_last_speed_kts); + + // compute the turn centre, based on the turn radius. + // key thing is to understand that we're working a right-angle triangle, + // where the right-angle is the point we start the turn. From that point, + // one side is the inbound course to the turn pt, and the other is the + // perpendicular line, of length 'r', to the turn centre. + // the triangle's hypotenuse, which we need to find, is the distance from the + // turn pt to the turn center (in the direction of the offset bearing) + // note that d - _turnRadius tell us how much we're 'cutting' the corner. + + double halfTurnAngle = fabs(_turnAngle * 0.5) * SG_DEGREES_TO_RADIANS; + double d = _turnRadius / cos(halfTurnAngle); + + // SG_LOG(SG_INSTR, SG_INFO, "turnRadius=" << _turnRadius << ", d=" << d + // << " (cut distance=" << d - _turnRadius << ")"); + + double median = _turnStartBearing + (_turnAngle * 0.5); + double offsetBearing = median + copysign(90, _turnAngle); + SG_NORMALIZE_RANGE(offsetBearing, 0.0, 360.0); + + double az2; + SGGeodesy::direct(_turnPt, offsetBearing, d * SG_NM_TO_METER, _turnCentre, az2); +} + +double GPS::computeTurnRadiusNm(double aGroundSpeedKts) const +{ + // turn time is seconds to execute a 360 turn. + double turnTime = 360.0 / _config.turnRateDegSec(); + + // c is ground distance covered in that time (circumference of the circle) + double c = turnTime * (aGroundSpeedKts / 3600.0); // convert knts to nm/sec + + // divide by 2PI to go from circumference -> radius + return c / (2 * M_PI); +} + +void GPS::updateRouteData() +{ + double totalDistance = _wp1DistanceM * SG_METER_TO_NM; + // walk all waypoints from wp2 to route end, and sum + for (int i=_routeMgr->currentWaypoint()+1; i<_routeMgr->size(); ++i) { + totalDistance += _routeMgr->get_waypoint(i).get_distance(); + } + + _routeDistanceNm->setDoubleValue(totalDistance * SG_METER_TO_NM); + double TTW = ((totalDistance * SG_METER_TO_NM) / _last_speed_kts) * 3600.0; + _routeETE->setStringValue(makeTTWString(TTW)); +} + +///////////////////////////////////////////////////////////////////////////// +// property getter/setters + +double GPS::getLegDistance() const +{ + if (_mode == "obs") { + return -1; + } + + return SGGeodesy::distanceNm(_wp0_position, _wp1_position); +} + +double GPS::getLegCourse() const +{ + if (_mode == "obs") { + return -9999.0; + } + + return SGGeodesy::courseDeg(_wp0_position, _wp1_position); +} + +double GPS::getLegMagCourse() const +{ + if (!_last_valid || (_mode == "obs")) { + return 0.0; + } + + double m = getLegCourse() - _magvar_node->getDoubleValue(); + SG_NORMALIZE_RANGE(m, 0.0, 360.0); + return m; +} + +double GPS::getAltDistanceRatio() const +{ + if (!_last_valid || (_mode == "obs")) { + return 0.0; + } + + double dist = SGGeodesy::distanceM(_wp0_position, _wp1_position); + if ( dist <= 0.0 ) { + return 0.0; + } + + double alt_difference_m = _wp0_position.getElevationM() - _wp1_position.getElevationM(); + return alt_difference_m / dist; +} + +double GPS::getMagTrack() const +{ + if (!_last_valid) { + return 0.0; + } + + double m = getTrueTrack() - _magvar_node->getDoubleValue(); + SG_NORMALIZE_RANGE(m, 0.0, 360.0); + return m; +} + +double GPS::getCDIDeflection() const +{ + if (!_last_valid) { + return 0.0; + } + + double defl; + if (_config.cdiDeflectionIsAngular()) { + defl = getWP1CourseDeviation(); + SG_CLAMP_RANGE(defl, -10.0, 10.0); // as in navradio.cxx + } else { + double fullScale = _config.cdiDeflectionLinearPeg(); + double normError = getWP1CourseErrorNm() / fullScale; + SG_CLAMP_RANGE(normError, -1.0, 1.0); + defl = normError * 10.0; // re-scale to navradio limits, i.e [-10.0 .. 10.0] + } + + return defl; +} + +const char* GPS::getWP0Ident() const +{ + if (_mode != "leg") { + return ""; + } + + return _wp0Ident.c_str(); +} + +const char* GPS::getWP0Name() const +{ + if (_mode != "leg") { + return ""; + } + + return _wp0Name.c_str(); +} + +const char* GPS::getWP1Ident() const +{ + return _wp1Ident.c_str(); +} + +const char* GPS::getWP1Name() const +{ + return _wp1Name.c_str(); +} + +double GPS::getWP1Distance() const +{ + if (!_last_valid) { + return -1.0; + } + + return _wp1DistanceM * SG_METER_TO_NM; +} + +double GPS::getWP1TTW() const +{ + if (!_last_valid) { + return -1.0; + } + + if (_last_speed_kts <= 0.0) { + return -1.0; + } + + return (getWP1Distance() / _last_speed_kts) * 3600.0; +} + +const char* GPS::getWP1TTWString() const +{ + return makeTTWString(getWP1TTW()); +} + +double GPS::getWP1Bearing() const +{ + if (!_last_valid) { + return -9999.0; + } + + return _wp1TrueBearing; +} + +double GPS::getWP1MagBearing() const +{ + if (!_last_valid) { + return -9999.0; + } + + return _wp1TrueBearing - _magvar_node->getDoubleValue(); +} + +double GPS::getWP1CourseDeviation() const +{ + if (!_last_valid) { + return 0.0; + } + + double dev = getWP1MagBearing() - _selectedCourse; + SG_NORMALIZE_RANGE(dev, -180.0, 180.0); + + if (fabs(dev) > 90.0) { + // When the course is away from the waypoint, + // it makes sense to change the sign of the deviation. + dev *= -1.0; + SG_NORMALIZE_RANGE(dev, -90.0, 90.0); + } + + return dev; +} + +double GPS::getWP1CourseErrorNm() const +{ + if (!_last_valid) { + return 0.0; + } + + double radDev = getWP1CourseDeviation() * SG_DEGREES_TO_RADIANS; + double course_error_m = sin(radDev) * _wp1DistanceM; + return course_error_m * SG_METER_TO_NM; +} + +bool GPS::getWP1ToFlag() const +{ + if (!_last_valid) { + return 0.0; + } + + double dev = getWP1MagBearing() - _selectedCourse; + SG_NORMALIZE_RANGE(dev, -180.0, 180.0); + + return (fabs(dev) < 90.0); +} + +double GPS::getScratchDistance() const +{ + if (!_scratchValid) { + return 0.0; + } + + return SGGeodesy::distanceNm(_indicated_pos, _scratchPos); +} + +double GPS::getScratchTrueBearing() const +{ + if (!_scratchValid) { + return 0.0; + } + + return SGGeodesy::courseDeg(_indicated_pos, _scratchPos); +} + +double GPS::getScratchMagBearing() const +{ + if (!_scratchValid) { + return 0.0; + } + + double crs = getScratchTrueBearing() - _magvar_node->getDoubleValue(); + SG_NORMALIZE_RANGE(crs, 0.0, 360.0); + return crs; +} + +///////////////////////////////////////////////////////////////////////////// +// command / scratch / search system + +void GPS::setCommand(const char* aCmd) +{ + SG_LOG(SG_INSTR, SG_INFO, "GPS command:" << aCmd); + + if (!strcmp(aCmd, "direct")) { + directTo(); + } else if (!strcmp(aCmd, "obs")) { + selectOBSMode(); + } else if (!strcmp(aCmd, "leg")) { + selectLegMode(); + } else if (!strcmp(aCmd, "load-route-wpt")) { + loadRouteWaypoint(); + } else if (!strcmp(aCmd, "nearest")) { + loadNearest(); + } else if (!strcmp(aCmd, "search")) { + _searchNames = false; + search(); + } else if (!strcmp(aCmd, "search-names")) { + _searchNames = true; + search(); + } else if (!strcmp(aCmd, "next")) { + nextResult(); + } else if (!strcmp(aCmd, "previous")) { + previousResult(); + } else if (!strcmp(aCmd, "define-user-wpt")) { + defineWaypoint(); + } else { + SG_LOG(SG_INSTR, SG_WARN, "GPS:unrecognzied command:" << aCmd); + } +} + +void GPS::clearScratch() +{ + _scratchPos = SGGeod::fromDegFt(-9999.0, -9999.0, -9999.0); + _scratchValid = false; + _scratchNode->setStringValue("type", ""); + _scratchNode->setStringValue("query", ""); +} + +bool GPS::isScratchPositionValid() const +{ + if ((_scratchPos.getLongitudeDeg() < -9990.0) || + (_scratchPos.getLatitudeDeg() < -9990.0)) { + return false; + } + + return true; +} + +void GPS::directTo() +{ + if (!isScratchPositionValid()) { + SG_LOG(SG_INSTR, SG_WARN, "invalid DTO lat/lon"); + return; + } + + _wp0_position = _indicated_pos; + _wp1Ident = _scratchNode->getStringValue("ident"); + _wp1Name = _scratchNode->getStringValue("name"); + _wp1_position = _scratchPos; + + _mode = "dto"; + _selectedCourse = getLegMagCourse(); + clearScratch(); +} + +void GPS::loadRouteWaypoint() +{ + _scratchValid = false; +// if (!_routeMgr->isRouteActive()) { +// SG_LOG(SG_INSTR, SG_WARN, "GPS:loadWaypoint: no active route"); +// return; +// } + + int index = _scratchNode->getIntValue("index", -9999); + clearScratch(); + + if (index == -9999) { // no index supplied, use current wp + index = _routeMgr->currentWaypoint(); + } + + _searchIsRoute = true; + setScratchFromRouteWaypoint(index); +} + +void GPS::setScratchFromRouteWaypoint(int aIndex) +{ + assert(_searchIsRoute); + if ((aIndex < 0) || (aIndex >= _routeMgr->size())) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:setScratchFromRouteWaypoint: route-index out of bounds"); + return; + } + + _searchResultIndex = aIndex; + SGWayPoint wp(_routeMgr->get_waypoint(aIndex)); + _scratchNode->setStringValue("name", wp.get_name()); + _scratchNode->setStringValue("ident", wp.get_id()); + _scratchPos = wp.get_target(); + _scratchValid = true; + _scratchNode->setDoubleValue("course", wp.get_track()); + _scratchNode->setIntValue("index", aIndex); + + int lastResult = _routeMgr->size() - 1; + _searchHasNext = (_searchResultIndex < lastResult); +} + +void GPS::loadNearest() +{ + string sty(_scratchNode->getStringValue("type")); + FGPositioned::Type ty = FGPositioned::typeFromName(sty); + if (ty == FGPositioned::INVALID) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:loadNearest: request type is invalid:" << sty); + return; + } + + auto_ptr f(createFilter(ty)); + int limitCount = _scratchNode->getIntValue("max-results", 1); + double cutoffDistance = _scratchNode->getDoubleValue("cutoff-nm", 400.0); + + clearScratch(); // clear now, regardless of whether we find a match or not + + _searchResults = + FGPositioned::findClosestN(_indicated_pos, limitCount, cutoffDistance, f.get()); + _searchResultsCached = true; + _searchResultIndex = 0; + _searchIsRoute = false; + _searchHasNext = false; + + if (_searchResults.empty()) { + SG_LOG(SG_INSTR, SG_INFO, "GPS:loadNearest: no matches at all"); + return; + } + + _searchHasNext = (_searchResults.size() > 1); + setScratchFromCachedSearchResult(); +} + +bool GPS::SearchFilter::pass(FGPositioned* aPos) const +{ + switch (aPos->type()) { + case FGPositioned::AIRPORT: + // heliport and seaport too? + case FGPositioned::VOR: + case FGPositioned::NDB: + case FGPositioned::FIX: + case FGPositioned::TACAN: + case FGPositioned::WAYPOINT: + return true; + default: + return false; + } +} + +FGPositioned::Type GPS::SearchFilter::minType() const +{ + return FGPositioned::AIRPORT; +} + +FGPositioned::Type GPS::SearchFilter::maxType() const +{ + return FGPositioned::WAYPOINT; +} + +FGPositioned::Filter* GPS::createFilter(FGPositioned::Type aTy) +{ + if (aTy == FGPositioned::AIRPORT) { + return new FGAirport::HardSurfaceFilter(_config.minRunwayLengthFt()); + } + + // if we were passed INVALID, assume it means 'all types interesting to a GPS' + if (aTy == FGPositioned::INVALID) { + return new SearchFilter; + } + + return new FGPositioned::TypeFilter(aTy); +} + +void GPS::search() +{ + // parse search terms into local members, and exec the first search + string sty(_scratchNode->getStringValue("type")); + _searchType = FGPositioned::typeFromName(sty); + _searchQuery = _scratchNode->getStringValue("query"); + _searchExact = _scratchNode->getBoolValue("exact", true); + _searchOrderByRange = _scratchNode->getBoolValue("order-by-distance", true); + _searchResultIndex = 0; + _searchIsRoute = false; + _searchHasNext = false; + + if (_searchExact && _searchOrderByRange) { + // immediate mode search, get all the results now and cache them + auto_ptr f(createFilter(_searchType)); + if (_searchNames) { + _searchResults = FGPositioned::findAllWithNameSortedByRange(_searchQuery, _indicated_pos, f.get()); + } else { + _searchResults = FGPositioned::findAllWithIdentSortedByRange(_searchQuery, _indicated_pos, f.get()); } - FGPositionedRef result = FGPositioned::findClosestWithIdent(id, ctx.pos); - if (!result) { - // not found, hmm - _last_wp1_ID = id; - return; + _searchResultsCached = true; + + if (_searchResults.empty()) { + clearScratch(); + return; } - _wp1_position = result->geod(); - _wp1_name_node->setStringValue(result->name().c_str()); - _last_wp1_ID = id; - ctx.waypoint_changed = true; + _searchHasNext = (_searchResults.size() > 1); + setScratchFromCachedSearchResult(); + } else { + // iterative search, look up result zero + _searchResultsCached = false; + performSearch(); + } } -void -GPS::updateTTWNode(UpdateContext& ctx, double distance_m, SGPropertyNode_ptr node) -{ - // Estimate time to waypoint. - // The estimation does not take track into consideration, - // so if you are going away from the waypoint the TTW will - // increase. Makes most sense when travelling directly towards - // the waypoint. - double TTW = 0.0; - if (ctx.speed_kt > 0.0 && distance_m > 0.0) { - TTW = (distance_m * SG_METER_TO_NM) / (ctx.speed_kt / 3600); +void GPS::performSearch() +{ + auto_ptr f(createFilter(_searchType)); + clearScratch(); + + FGPositionedRef r; + if (_searchNames) { + if (_searchOrderByRange) { + r = FGPositioned::findClosestWithPartialName(_indicated_pos, _searchQuery, f.get(), _searchResultIndex, _searchHasNext); + } else { + r = FGPositioned::findWithPartialName(_searchQuery, f.get(), _searchResultIndex, _searchHasNext); } - if (TTW < 356400.5) { // That's 99 hours - unsigned int TTW_seconds = (int) (TTW + 0.5); - unsigned int TTW_minutes = 0; - unsigned int TTW_hours = 0; - char TTW_str[9]; - TTW_hours = TTW_seconds / 3600; - TTW_minutes = (TTW_seconds / 60) % 60; - TTW_seconds = TTW_seconds % 60; - snprintf(TTW_str, 9, "%02d:%02d:%02d", - TTW_hours, TTW_minutes, TTW_seconds); - node->setStringValue(TTW_str); + } else { + if (_searchOrderByRange) { + r = FGPositioned::findClosestWithPartialId(_indicated_pos, _searchQuery, f.get(), _searchResultIndex, _searchHasNext); } else { - node->setStringValue("--:--:--"); + r = FGPositioned::findWithPartialId(_searchQuery, f.get(), _searchResultIndex, _searchHasNext); } + } + + if (!r) { + return; + } + + setScratchFromPositioned(r.get(), _searchResultIndex); } -void -GPS::updateWaypoint0Course(UpdateContext& ctx) +void GPS::setScratchFromCachedSearchResult() { - // Find the bearing and distance to waypoint 0. - double az2; - SGGeodesy::inverse(ctx.pos, ctx.wp0_pos, ctx.wp0_bearing_deg, az2,ctx.wp0_distance); - _wp0_distance_node->setDoubleValue(ctx.wp0_distance * SG_METER_TO_NM); - _wp0_bearing_node->setDoubleValue(ctx.wp0_bearing_deg); - - double mag_bearing_deg = ctx.wp0_bearing_deg - ctx.magvar_deg; - SG_NORMALIZE_RANGE(mag_bearing_deg, 0.0, 360.0); - _wp0_mag_bearing_node->setDoubleValue(mag_bearing_deg); - double bearing_error_deg = ctx.track1_deg - ctx.wp0_bearing_deg; - SG_NORMALIZE_RANGE(bearing_error_deg, -180.0, 180.0); - _true_wp0_bearing_error_node->setDoubleValue(bearing_error_deg); - - updateTTWNode(ctx, ctx.wp0_distance, _wp0_ttw_node); - - // Course deviation is the diffenrence between the bearing - // and the course. - double course_deviation_deg = ctx.wp0_bearing_deg - - ctx.wp0_course_deg; - SG_NORMALIZE_RANGE(course_deviation_deg, -180.0, 180.0); - - // If the course deviation is less than 90 degrees to either side, - // our desired course is towards the waypoint. - // It does not matter if we are actually moving - // towards or from the waypoint. - if (fabs(course_deviation_deg) < 90.0) { - _wp0_to_flag_node->setBoolValue(true); - } - // If it's more than 90 degrees the desired - // course is from the waypoint. - else if (fabs(course_deviation_deg) > 90.0) { - _wp0_to_flag_node->setBoolValue(false); - // When the course is away from the waypoint, - // it makes sense to change the sign of the deviation. - course_deviation_deg *= -1.0; - SG_NORMALIZE_RANGE(course_deviation_deg, -90.0, 90.0); - } + assert(_searchResultsCached); + int index = _searchResultIndex; + + if ((index < 0) || (index >= (int) _searchResults.size())) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:setScratchFromCachedSearchResult: index out of bounds:" << index); + return; + } + + setScratchFromPositioned(_searchResults[index], index); + + int lastResult = (int) _searchResults.size() - 1; + _searchHasNext = (_searchResultIndex < lastResult); +} - _wp0_course_deviation_node->setDoubleValue(course_deviation_deg); +void GPS::setScratchFromPositioned(FGPositioned* aPos, int aIndex) +{ + clearScratch(); + assert(aPos); - // Cross track error. - double course_error_m = sin(course_deviation_deg * SG_PI / 180.0) - * (ctx.wp0_distance); - _wp0_course_error_nm_node->setDoubleValue(course_error_m * SG_METER_TO_NM); + _scratchPos = aPos->geod(); + _scratchNode->setStringValue("name", aPos->name()); + _scratchNode->setStringValue("ident", aPos->ident()); + _scratchNode->setStringValue("type", FGPositioned::nameForType(aPos->type())); + + if (aIndex >= 0) { + _scratchNode->setIntValue("index", aIndex); + } + + _scratchValid = true; + if (_searchResultsCached) { + _scratchNode->setIntValue("result-count", _searchResults.size()); + } + + switch (aPos->type()) { + case FGPositioned::VOR: + _scratchNode->setDoubleValue("frequency-mhz", static_cast(aPos)->get_freq() / 100.0); + break; + + case FGPositioned::NDB: + _scratchNode->setDoubleValue("frequency-khz", static_cast(aPos)->get_freq() / 100.0); + break; + + case FGPositioned::AIRPORT: + addAirportToScratch((FGAirport*)aPos); + break; + + default: + // no-op + break; + } + + // look for being on the route and set? } -void -GPS::updateWaypoint1Course(UpdateContext& ctx) +void GPS::addAirportToScratch(FGAirport* aAirport) { - // Find the bearing and distance to waypoint 0. - double az2; - SGGeodesy::inverse(ctx.pos, ctx.wp1_pos, ctx.wp1_bearing_deg, az2,ctx.wp1_distance); - _wp1_distance_node->setDoubleValue(ctx.wp1_distance * SG_METER_TO_NM); - _wp1_bearing_node->setDoubleValue(ctx.wp1_bearing_deg); - - double mag_bearing_deg = ctx.wp1_bearing_deg - ctx.magvar_deg; - SG_NORMALIZE_RANGE(mag_bearing_deg, 0.0, 360.0); - _wp1_mag_bearing_node->setDoubleValue(mag_bearing_deg); - double bearing_error_deg = ctx.track1_deg - ctx.wp1_bearing_deg; - SG_NORMALIZE_RANGE(bearing_error_deg, -180.0, 180.0); - _true_wp1_bearing_error_node->setDoubleValue(bearing_error_deg); + for (unsigned int r=0; rnumRunways(); ++r) { + SGPropertyNode* rwyNd = _scratchNode->getChild("runways", r, true); + FGRunway* rwy = aAirport->getRunwayByIndex(r); + // TODO - filter out unsuitable runways in the future + // based on config again - updateTTWNode(ctx, ctx.wp1_distance, _wp1_ttw_node); - - // Course deviation is the diffenrence between the bearing - // and the course. - double course_deviation_deg = ctx.wp1_bearing_deg - - ctx.wp1_course_deg; - SG_NORMALIZE_RANGE(course_deviation_deg, -180.0, 180.0); - - // If the course deviation is less than 90 degrees to either side, - // our desired course is towards the waypoint. - // It does not matter if we are actually moving - // towards or from the waypoint. - if (fabs(course_deviation_deg) < 90.0) { - _wp1_to_flag_node->setBoolValue(true); - } - // If it's more than 90 degrees the desired - // course is from the waypoint. - else if (fabs(course_deviation_deg) > 90.0) { - _wp1_to_flag_node->setBoolValue(false); - // When the course is away from the waypoint, - // it makes sense to change the sign of the deviation. - course_deviation_deg *= -1.0; - SG_NORMALIZE_RANGE(course_deviation_deg, -90.0, 90.0); + rwyNd->setStringValue("id", rwy->ident().c_str()); + rwyNd->setIntValue("length-ft", rwy->lengthFt()); + rwyNd->setIntValue("width-ft", rwy->widthFt()); + rwyNd->setIntValue("heading-deg", rwy->headingDeg()); + // map surface code to a string + // TODO - lighting information + + if (rwy->ILS()) { + rwyNd->setDoubleValue("ils-frequency-mhz", rwy->ILS()->get_freq() / 100.0); } + } // of runways iteration +} - _wp1_course_deviation_node->setDoubleValue(course_deviation_deg); - // Cross track error. - double course_error_m = sin(course_deviation_deg * SG_PI / 180.0) - * (ctx.wp1_distance); - _wp1_course_error_nm_node->setDoubleValue(course_error_m * SG_METER_TO_NM); +void GPS::selectOBSMode() +{ + if (!isScratchPositionValid()) { + SG_LOG(SG_INSTR, SG_WARN, "invalid OBS lat/lon"); + return; + } + + SG_LOG(SG_INSTR, SG_INFO, "GPS switching to OBS mode"); + _mode = "obs"; + + _wp1Ident = _scratchNode->getStringValue("ident"); + _wp1Name = _scratchNode->getStringValue("name"); + _wp1_position = _scratchPos; + _wp0_position = _indicated_pos; +} + +void GPS::selectLegMode() +{ + if (_mode == "leg") { + return; + } + + if (!_routeMgr->isRouteActive()) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:selectLegMode: no active route"); + return; + } + + SG_LOG(SG_INSTR, SG_INFO, "GPS switching to LEG mode"); + _mode = "leg"; + + // not really sequenced, but does all the work of updating wp0/1 + routeManagerSequenced(); +} +void GPS::nextResult() +{ + if (!_searchHasNext) { + return; + } + + clearScratch(); + if (_searchIsRoute) { + setScratchFromRouteWaypoint(++_searchResultIndex); + } else if (_searchResultsCached) { + ++_searchResultIndex; + setScratchFromCachedSearchResult(); + } else { + ++_searchResultIndex; + performSearch(); + } // of iterative search case +} + +void GPS::previousResult() +{ + if (_searchResultIndex <= 0) { + return; + } + + clearScratch(); + --_searchResultIndex; + + if (_searchIsRoute) { + setScratchFromRouteWaypoint(_searchResultIndex); + } else if (_searchResultsCached) { + setScratchFromCachedSearchResult(); + } else { + performSearch(); + } +} + +void GPS::defineWaypoint() +{ + if (!isScratchPositionValid()) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:defineWaypoint: invalid lat/lon"); + return; + } + + string ident = _scratchNode->getStringValue("ident"); + if (ident.size() < 2) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:defineWaypoint: waypoint identifier must be at least two characters"); + return; + } + +// check for duplicate idents + FGPositioned::TypeFilter f(FGPositioned::WAYPOINT); + FGPositioned::List dups = FGPositioned::findAllWithIdentSortedByRange(ident, _indicated_pos, &f); + if (!dups.empty()) { + SG_LOG(SG_INSTR, SG_WARN, "GPS:defineWaypoint: non-unique waypoint identifier, ho-hum"); + } + + SG_LOG(SG_INSTR, SG_INFO, "GPS:defineWaypoint: creating waypoint:" << ident); + FGPositionedRef wpt = FGPositioned::createUserWaypoint(ident, _scratchPos); + _searchResultsCached = false; + setScratchFromPositioned(wpt.get(), -1); } // end of gps.cxx diff --git a/src/Instrumentation/gps.hxx b/src/Instrumentation/gps.hxx index 24c98239c..15d9a8b14 100644 --- a/src/Instrumentation/gps.hxx +++ b/src/Instrumentation/gps.hxx @@ -11,8 +11,13 @@ #include #include +#include "Navaids/positioned.hxx" + // forward decls class SGRoute; +class FGRouteMgr; +class FGAirport; +class GPSListener; class SGGeodProperty { @@ -42,15 +47,6 @@ private: * /systems/electrical/outputs/gps * /instrumentation/gps/serviceable * - * /instrumentation/gps/wp-longitude-deg - * /instrumentation/gps/wp-latitude-deg - * /instrumentation/gps/wp-altitude-ft - * /instrumentation/gps/wp-ID - * /instrumentation/gps/wp-name - * /instrumentation/gps/desired-course-deg - * /instrumentation/gps/get-nearest-airport - * /instrumentation/gps/waypoint-type - * /instrumentation/gps/tracking-bug * * Output properties: * @@ -73,8 +69,7 @@ private: * /instrumentation/gps/trip-odometer * /instrumentation/gps/true-bug-error-deg * /instrumentation/gps/magnetic-bug-error-deg - * /instrumentation/gps/true-bearing-error-deg - * /instrumentation/gps/magnetic-bearing-error-deg + */ class GPS : public SGSubsystem { @@ -89,117 +84,294 @@ public: virtual void update (double delta_time_sec); private: - typedef struct { - double dt; - SGGeod pos; - SGGeod wp0_pos; - SGGeod wp1_pos; - bool waypoint_changed; - double speed_kt; - double track1_deg; - double track2_deg; - double magvar_deg; - double wp0_distance; - double wp0_course_deg; - double wp0_bearing_deg; - double wp1_distance; - double wp1_course_deg; - double wp1_bearing_deg; - } UpdateContext; + friend class GPSListener; + friend class SearchFilter; + + /** + * Configuration manager, track data relating to aircraft installation + */ + class Config + { + public: + Config(); + + void init(SGPropertyNode*); + + bool turnAnticipationEnabled() const + { return _enableTurnAnticipation; } + + /** + * Desired turn rate in degrees/second. From this we derive the turn + * radius and hence how early we need to anticipate it. + */ + double turnRateDegSec() const + { return _turnRate; } + + /** + * Distance at which we arm overflight sequencing. Once inside this + * distance, a change of the wp1 'TO' flag to false will be considered + * overlight of the wp. + */ + double overflightArmDistanceNm() const + { return _overflightArmDistance; } + + /** + * Time before the next WP to activate an external annunciator + */ + double waypointAlertTime() const + { return _waypointAlertTime; } + + bool tuneNavRadioToRefVor() const + { return _tuneRadio1ToRefVor; } + + bool requireHardSurface() const + { return _requireHardSurface; } + + double minRunwayLengthFt() const + { return _minRunwayLengthFt; } + + double getOBSCourse() const; + + bool cdiDeflectionIsAngular() const + { return (_cdiMaxDeflectionNm <= 0.0); } + + double cdiDeflectionLinearPeg() const + { + assert(_cdiMaxDeflectionNm > 0.0); + return _cdiMaxDeflectionNm; + } + private: + bool _enableTurnAnticipation; + + // desired turn rate in degrees per second + double _turnRate; + + // distance from waypoint to arm overflight sequencing (in nm) + double _overflightArmDistance; + + // time before reaching a waypoint to trigger annunicator light/sound + // (in seconds) + double _waypointAlertTime; + + // should GPS automatically tune NAV1 to the reference VOR? + bool _tuneRadio1ToRefVor; + + // minimum runway length to require when filtering + double _minRunwayLengthFt; + + // should we require a hard-surfaced runway when filtering? + bool _requireHardSurface; + + // helpers to tie obs-course-source property + const char* getOBSCourseSource() const; + void setOBSCourseSource(const char* aPropPath); + + // property to retrieve the OBS course from + SGPropertyNode_ptr _obsCourseSource; + + double _cdiMaxDeflectionNm; + }; + + class SearchFilter : public FGPositioned::Filter + { + public: + virtual bool pass(FGPositioned* aPos) const; + + virtual FGPositioned::Type minType() const; + virtual FGPositioned::Type maxType() const; + }; - void search (double frequency, const SGGeod& pos); - /** * reset all output properties to default / non-service values */ void clearOutput(); - void updateWithValid(UpdateContext& ctx); + void updateWithValid(double dt); + void updateBasicData(double dt); + void updateWaypoints(); + + void updateTrackingBug(); + void updateReferenceNavaid(double dt); + void referenceNavaidSet(const std::string& aNavaid); + void tuneNavRadios(); + void updateRouteData(); + + void routeActivated(); + void routeManagerSequenced(); + + void updateTurn(); + void updateOverflight(); + void beginTurn(); + void endTurn(); + + double computeTurnProgress(double aBearing) const; + void computeTurnData(); + void updateTurnData(); + double computeTurnRadiusNm(double aGroundSpeedKts) const; - void updateNearestAirport(UpdateContext& ctx); - void updateWaypoint0(UpdateContext& ctx); - void updateWaypoint1(UpdateContext& ctx); - void updateLegCourse(UpdateContext& ctx); - void updateWaypoint0Course(UpdateContext& ctx); - void updateWaypoint1Course(UpdateContext& ctx); +// scratch maintenence utilities + void setScratchFromPositioned(FGPositioned* aPos, int aIndex); + void setScratchFromCachedSearchResult(); + void setScratchFromRouteWaypoint(int aIndex); + + /** + * Add airport-specific information to a scratch result + */ + void addAirportToScratch(FGAirport* aAirport); + + void clearScratch(); + + /** + * Predicate, determine if the lon/lat position in the scratch is + * valid or not. + */ + bool isScratchPositionValid() const; + + FGPositioned::Filter* createFilter(FGPositioned::Type aTy); + + /** + * Search kernel - called each time we step through a result + */ + void performSearch(); + +// command handlers + void selectLegMode(); + void selectOBSMode(); + void directTo(); + void loadRouteWaypoint(); + void loadNearest(); + void search(); + void nextResult(); + void previousResult(); + void defineWaypoint(); + +// tied-property getter/setters + void setCommand(const char* aCmd); + const char* getCommand() const { return ""; } + + const char* getMode() const { return _mode.c_str(); } + + bool getScratchValid() const { return _scratchValid; } + double getScratchDistance() const; + double getScratchMagBearing() const; + double getScratchTrueBearing() const; + bool getScratchHasNext() const { return _searchHasNext; } + + double getSelectedCourse() const { return _selectedCourse; } + double getCDIDeflection() const; + + double getLegDistance() const; + double getLegCourse() const; + double getLegMagCourse() const; + double getAltDistanceRatio() const; + + double getTrueTrack() const { return _last_true_track; } + double getMagTrack() const; + double getGroundspeedKts() const { return _last_speed_kts; } + double getVerticalSpeed() const { return _last_vertical_speed; } + + //bool getLegMode() const { return _mode == "leg"; } + //bool getObsMode() const { return _mode == "obs"; } + + const char* getWP0Ident() const; + const char* getWP0Name() const; + + const char* getWP1Ident() const; + const char* getWP1Name() const; + + double getWP1Distance() const; + double getWP1TTW() const; + const char* getWP1TTWString() const; + double getWP1Bearing() const; + double getWP1MagBearing() const; + double getWP1CourseDeviation() const; + double getWP1CourseErrorNm() const; + bool getWP1ToFlag() const; + // true-bearing-error and mag-bearing-error + + + +// members + SGPropertyNode_ptr _magvar_node; + SGPropertyNode_ptr _serviceable_node; + SGPropertyNode_ptr _electrical_node; + SGPropertyNode_ptr _tracking_bug_node; + SGPropertyNode_ptr _raim_node; - void waypointChanged(UpdateContext& ctx); - void updateTTWNode(UpdateContext& ctx, double distance_m, SGPropertyNode_ptr node); - void updateTrackingBug(UpdateContext& ctx); - - SGPropertyNode_ptr _magvar_node; - SGPropertyNode_ptr _serviceable_node; - SGPropertyNode_ptr _electrical_node; - SGPropertyNode_ptr _wp0_ID_node; - SGPropertyNode_ptr _wp0_name_node; - SGPropertyNode_ptr _wp0_course_node; - SGPropertyNode_ptr _get_nearest_airport_node; - SGPropertyNode_ptr _wp1_ID_node; - SGPropertyNode_ptr _wp1_name_node; - SGPropertyNode_ptr _wp1_course_node; - SGPropertyNode_ptr _tracking_bug_node; - - SGPropertyNode_ptr _raim_node; - SGPropertyNode_ptr _indicated_vertical_speed_node; - SGPropertyNode_ptr _true_track_node; - SGPropertyNode_ptr _magnetic_track_node; - SGPropertyNode_ptr _speed_node; - SGPropertyNode_ptr _wp0_distance_node; - SGPropertyNode_ptr _wp0_ttw_node; - SGPropertyNode_ptr _wp0_bearing_node; - SGPropertyNode_ptr _wp0_mag_bearing_node; - SGPropertyNode_ptr _wp0_course_deviation_node; - SGPropertyNode_ptr _wp0_course_error_nm_node; - SGPropertyNode_ptr _wp0_to_flag_node; - SGPropertyNode_ptr _wp1_distance_node; - SGPropertyNode_ptr _wp1_ttw_node; - SGPropertyNode_ptr _wp1_bearing_node; - SGPropertyNode_ptr _wp1_mag_bearing_node; - SGPropertyNode_ptr _wp1_course_deviation_node; - SGPropertyNode_ptr _wp1_course_error_nm_node; - SGPropertyNode_ptr _wp1_to_flag_node; - SGPropertyNode_ptr _odometer_node; + SGPropertyNode_ptr _odometer_node; SGPropertyNode_ptr _trip_odometer_node; SGPropertyNode_ptr _true_bug_error_node; SGPropertyNode_ptr _magnetic_bug_error_node; - SGPropertyNode_ptr _true_wp0_bearing_error_node; - SGPropertyNode_ptr _magnetic_wp0_bearing_error_node; - SGPropertyNode_ptr _true_wp1_bearing_error_node; - SGPropertyNode_ptr _magnetic_wp1_bearing_error_node; - SGPropertyNode_ptr _leg_distance_node; - SGPropertyNode_ptr _leg_course_node; - SGPropertyNode_ptr _leg_magnetic_course_node; - SGPropertyNode_ptr _alt_dist_ratio_node; - SGPropertyNode_ptr _leg_course_deviation_node; - SGPropertyNode_ptr _leg_course_error_nm_node; - SGPropertyNode_ptr _leg_to_flag_node; - SGPropertyNode_ptr _alt_deviation_node; - + + SGPropertyNode_ptr _ref_navaid_id_node; + SGPropertyNode_ptr _ref_navaid_bearing_node; + SGPropertyNode_ptr _ref_navaid_distance_node; + SGPropertyNode_ptr _ref_navaid_mag_bearing_node; + SGPropertyNode_ptr _ref_navaid_frequency_node; + SGPropertyNode_ptr _ref_navaid_name_node; + + SGPropertyNode_ptr _route_active_node; + SGPropertyNode_ptr _route_current_wp_node; + SGPropertyNode_ptr _routeDistanceNm; + SGPropertyNode_ptr _routeETE; + + SGPropertyNode_ptr _fromFlagNode; + + double _selectedCourse; + bool _last_valid; SGGeod _last_pos; double _last_speed_kts; - - std::string _last_wp0_ID; - std::string _last_wp1_ID; - - double _alt_dist_ratio; - double _distance_m; - double _course_deg; - - double _bias_length; - double _bias_angle; - double _azimuth_error; - double _range_error; - double _elapsed_time; - + double _last_true_track; + double _last_vertical_speed; + + std::string _mode; + GPSListener* _listener; + Config _config; + FGRouteMgr* _routeMgr; + + bool _ref_navaid_set; + double _ref_navaid_elapsed; + FGPositionedRef _ref_navaid; + std::string _name; int _num; - - SGGeodProperty _position; - SGGeodProperty _wp0_position; - SGGeodProperty _wp1_position; - SGGeodProperty _indicated_pos; + + SGGeodProperty _position; + SGGeod _wp0_position; + SGGeod _wp1_position; + SGGeod _indicated_pos; + std::string _wp0Ident, _wp0Name, _wp1Ident, _wp1Name; + double _wp1DistanceM, _wp1TrueBearing; + +// scratch data + SGGeod _scratchPos; + SGPropertyNode_ptr _scratchNode; + bool _scratchValid; + +// search data + int _searchResultIndex; + std::string _searchQuery; + FGPositioned::Type _searchType; + bool _searchExact; + bool _searchOrderByRange; + bool _searchResultsCached; + FGPositioned::List _searchResults; + bool _searchIsRoute; ///< set if 'search' is actually the current route + bool _searchHasNext; ///< is there a result after this one? + bool _searchNames; ///< set if we're searching names instead of idents + + // turn data + bool _computeTurnData; ///< do we need to update the turn data? + bool _anticipateTurn; ///< are we anticipating the next turn or not? + bool _inTurn; // is a turn in progress? + bool _turnSequenced; // have we sequenced the new leg? + double _turnAngle; // angle to turn through, in degrees + double _turnStartBearing; // bearing of inbound leg + double _turnRadius; // radius of turn in nm + SGGeod _turnPt; + SGGeod _turnCentre; }; diff --git a/src/Instrumentation/instrument_mgr.cxx b/src/Instrumentation/instrument_mgr.cxx index 21b2a47e7..e79f94630 100644 --- a/src/Instrumentation/instrument_mgr.cxx +++ b/src/Instrumentation/instrument_mgr.cxx @@ -128,7 +128,7 @@ bool FGInstrumentMgr::build () set_subsystem( id, new Altimeter( node ) ); } else if ( name == "gps" ) { - set_subsystem( id, new GPS( node ), 0.45 ); + set_subsystem( id, new GPS( node ) ); } else if ( name == "gsdi" ) { set_subsystem( id, new GSDI( node ) ); diff --git a/src/Instrumentation/navradio.cxx b/src/Instrumentation/navradio.cxx index 3e779bbcd..eacb7a6d6 100644 --- a/src/Instrumentation/navradio.cxx +++ b/src/Instrumentation/navradio.cxx @@ -244,6 +244,7 @@ FGNavRadio::init () gps_to_flag_node = fgGetNode("/instrumentation/gps/to-flag", true); gps_from_flag_node = fgGetNode("/instrumentation/gps/from-flag", true); gps_has_gs_node = fgGetNode("/instrumentation/gps/has-gs", true); + gps_course_node = fgGetNode("/instrumentation/gps/selected-course-deg", true); std::ostringstream temp; temp << _name << "nav-ident" << _num; @@ -605,6 +606,8 @@ void FGNavRadio::updateGPSSlaved() _cdiCrossTrackErrorM = 0.0; // FIXME, supply this _gsNeedleDeflection = 0.0; // FIXME, supply this + + //sel_radial_node->setDoubleValue(gps_course_node->getDoubleValue()); } void FGNavRadio::updateCDI(double dt) diff --git a/src/Instrumentation/navradio.hxx b/src/Instrumentation/navradio.hxx index 116a90ce7..760c67f20 100644 --- a/src/Instrumentation/navradio.hxx +++ b/src/Instrumentation/navradio.hxx @@ -113,7 +113,8 @@ class FGNavRadio : public SGSubsystem SGPropertyNode_ptr gps_to_flag_node; SGPropertyNode_ptr gps_from_flag_node; SGPropertyNode_ptr gps_has_gs_node; - + SGPropertyNode_ptr gps_course_node; + // internal (private) values int play_count; diff --git a/src/Instrumentation/testgps.cxx b/src/Instrumentation/testgps.cxx new file mode 100644 index 000000000..23e491b17 --- /dev/null +++ b/src/Instrumentation/testgps.cxx @@ -0,0 +1,212 @@ + +#include
+#include
+#include
+ +#include +#include +#include + +using std::string; + +char *homedir = ::getenv( "HOME" ); +char *hostname = ::getenv( "HOSTNAME" ); +bool free_hostname = false; + +void testSetPosition(const SGGeod& aPos) +{ + fgSetDouble("/position/longitude-deg", aPos.getLongitudeDeg()); + fgSetDouble("/position/latitude-deg", aPos.getLatitudeDeg()); + fgSetDouble("/position/altitude-ft", aPos.getElevationFt()); +} + +void printScratch(SGPropertyNode* scratch) +{ + if (!scratch->getBoolValue("valid", false)) { + SG_LOG(SG_GENERAL, SG_ALERT, "Scratch is invalid."); + return; + } + + SG_LOG(SG_GENERAL, SG_ALERT, "Scratch:" << + scratch->getStringValue("ident") << "/" << scratch->getStringValue("name")); + + SG_LOG(SG_GENERAL, SG_ALERT, "\t" << scratch->getDoubleValue("longitude-deg") + << " " << scratch->getDoubleValue("latitude-deg") << " @ " << scratch->getDoubleValue("altitude-ft")); + + SG_LOG(SG_GENERAL, SG_ALERT, "\t" << scratch->getDoubleValue("true-bearing-deg") << + " (" << scratch->getDoubleValue("mag-bearing-deg") << " magnetic) " << scratch->getDoubleValue("distance-nm")); + + if (scratch->hasChild("result-index")) { + SG_LOG(SG_GENERAL, SG_ALERT, "\tresult-index:" << scratch->getIntValue("result-index")); + } + + if (scratch->hasChild("route-index")) { + SG_LOG(SG_GENERAL, SG_ALERT, "\troute-index:" << scratch->getIntValue("route-index")); + } +} + +void createDummyRoute(FGRouteMgr* rm) +{ + SGPropertyNode* rmInput = fgGetNode("/autopilot/route-manager/input", true); + rmInput->setStringValue("UW"); + rmInput->setStringValue("TLA/347/13"); + rmInput->setStringValue("TLA"); + rmInput->setStringValue("HAVEN"); + rmInput->setStringValue("NEW/305/29"); + rmInput->setStringValue("NEW"); + rmInput->setStringValue("OTR"); +} + +int main(int argc, char* argv[]) +{ + globals = new FGGlobals; + + fgInitFGRoot(argc, argv); + if (!fgInitConfig(argc, argv) ) { + SG_LOG( SG_GENERAL, SG_ALERT, "Config option parsing failed" ); + exit(-1); + } + + + fgInitNav(); + + SG_LOG(SG_GENERAL, SG_ALERT, "hello world!"); + + FGRouteMgr* rm = new FGRouteMgr; + globals->add_subsystem( "route-manager", rm ); + + // FGEnvironmentMgr* envMgr = new FGEnvironmentMgr; + // globals->add_subsystem("environment", envMgr); + // envMgr->init(); + + + SGPropertyNode* nd = fgGetNode("/instrumentation/gps", true); + GPS* gps = new GPS(nd); + globals->add_subsystem("gps", gps); + + const FGAirport* egph = fgFindAirportID("EGPH"); + testSetPosition(egph->geod()); + + // startup the route manager + rm->init(); + + nd->setBoolValue("serviceable", true); + fgSetBool("/systems/electrical/outputs/gps", true); + + gps->init(); + SGPropertyNode* scratch = nd->getChild("scratch", 0, true); + SGPropertyNode* wp = nd->getChild("wp", 0, true); + SGPropertyNode* wp1 = wp->getChild("wp", 1, true); + + // update a few times + gps->update(0.05); + gps->update(0.05); + + scratch->setStringValue("ident", "TL"); + scratch->setStringValue("type", "Vor"); + scratch->setBoolValue("exact", false); + nd->setStringValue("command", "search"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + +// alphanumeric sort, partial matching + nd->setDoubleValue("config/min-runway-length-ft", 5000.0); + scratch->setBoolValue("exact", false); + scratch->setBoolValue("order-by-distance", false); + scratch->setStringValue("ident", "KS"); + scratch->setStringValue("type", "apt"); + + nd->setStringValue("command", "search"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + +// alphanumeric sort, explicit matching + scratch->setBoolValue("exact", true); + scratch->setBoolValue("order-by-distance", true); + scratch->setStringValue("type", "vor"); + scratch->setStringValue("ident", "DCS"); + + nd->setStringValue("command", "search"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + +// search on totally missing + scratch->setBoolValue("exact", true); + scratch->setBoolValue("order-by-distance", true); + scratch->setStringValue("ident", "FOFOFOFOF"); + nd->setStringValue("command", "search"); + printScratch(scratch); + +// nearest + scratch->setStringValue("type", "apt"); + scratch->setIntValue("max-results", 10); + nd->setStringValue("command", "nearest"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + +// direct to + nd->setStringValue("command", "direct"); + SG_LOG(SG_GENERAL, SG_ALERT, "mode:" << nd->getStringValue("mode") << "\n\t" + << wp1->getStringValue("ID") << " " << wp1->getDoubleValue("longitude-deg") + << " " << wp1->getDoubleValue("latitude-deg")); + +// OBS mode + scratch->setStringValue("ident", "UW"); + scratch->setBoolValue("order-by-distance", true); + nd->setStringValue("command", "search"); + printScratch(scratch); + + nd->setStringValue("command", "obs"); + SG_LOG(SG_GENERAL, SG_ALERT, "mode:" << nd->getStringValue("mode") << "\n\t" + << wp1->getStringValue("ID") << " " << wp1->getDoubleValue("longitude-deg") + << " " << wp1->getDoubleValue("latitude-deg")); + +// load route waypoints + createDummyRoute(rm); + + scratch->setIntValue("route-index", 5); + nd->setStringValue("command", "load-route-wpt"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + + nd->setStringValue("command", "next"); + printScratch(scratch); + + scratch->setIntValue("route-index", 2); + nd->setStringValue("command", "load-route-wpt"); + nd->setStringValue("command", "direct"); + SG_LOG(SG_GENERAL, SG_ALERT, "mode:" << nd->getStringValue("mode") << "\n\t" + << wp1->getStringValue("ID") << " " << wp1->getDoubleValue("longitude-deg") + << " " << wp1->getDoubleValue("latitude-deg")); + +// route editing + SGGeod pos = egph->geod(); + scratch->setStringValue("ident", "FOOBAR"); + scratch->setDoubleValue("longitude-deg", pos.getLongitudeDeg()); + scratch->setDoubleValue("latitude-deg", pos.getLatitudeDeg()); + nd->setStringValue("command", "define-user-wpt"); + printScratch(scratch); + + + + return EXIT_SUCCESS; +}