// routePath.hxx - compute data about planned route // // Copyright (C) 2018 James Turner // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #include "config.h" #include #include #include #include #include #include
#include #include #include #include #include namespace flightgear { // implementation of // http://williams.best.vwh.net/avform.htm#Intersection bool geocRadialIntersection(const SGGeoc& a, double r1, const SGGeoc& b, double r2, SGGeoc& result) { double crs13 = r1 * SG_DEGREES_TO_RADIANS; double crs23 = r2 * SG_DEGREES_TO_RADIANS; double dst12 = SGGeodesy::distanceRad(a, b); //IF sin(lon2-lon1)<0 // crs12=acos((sin(lat2)-sin(lat1)*cos(dst12))/(sin(dst12)*cos(lat1))) // crs21=2.*pi-acos((sin(lat1)-sin(lat2)*cos(dst12))/(sin(dst12)*cos(lat2))) // ELSE // crs12=2.*pi-acos((sin(lat2)-sin(lat1)*cos(dst12))/(sin(dst12)*cos(lat1))) // crs21=acos((sin(lat1)-sin(lat2)*cos(dst12))/(sin(dst12)*cos(lat2))) // ENDIF double sinLat1 = sin(a.getLatitudeRad()); double cosLat1 = cos(a.getLatitudeRad()); double sinDst12 = sin(dst12); double cosDst12 = cos(dst12); double crs12 = SGGeodesy::courseRad(a, b), crs21 = SGGeodesy::courseRad(b, a); // normalise to -pi .. pi range double ang1 = SGMiscd::normalizeAngle(crs13-crs12); double ang2 = SGMiscd::normalizeAngle(crs21-crs23); if ((sin(ang1) == 0.0) && (sin(ang2) == 0.0)) { SG_LOG(SG_INSTR, SG_INFO, "geocRadialIntersection: infinity of intersections"); return false; } if ((sin(ang1)*sin(ang2))<0.0) { // SG_LOG(SG_INSTR, SG_INFO, "geocRadialIntersection: intersection ambiguous:" // << ang1 << " " << ang2 << " sin1 " << sin(ang1) << " sin2 " << sin(ang2)); return false; } ang1 = fabs(ang1); ang2 = fabs(ang2); //ang3=acos(-cos(ang1)*cos(ang2)+sin(ang1)*sin(ang2)*cos(dst12)) //dst13=atan2(sin(dst12)*sin(ang1)*sin(ang2),cos(ang2)+cos(ang1)*cos(ang3)) //lat3=asin(sin(lat1)*cos(dst13)+cos(lat1)*sin(dst13)*cos(crs13)) //lon3=mod(lon1-dlon+pi,2*pi)-pi double ang3 = acos(-cos(ang1) * cos(ang2) + sin(ang1) * sin(ang2) * cosDst12); double dst13 = atan2(sinDst12 * sin(ang1) * sin(ang2), cos(ang2) + cos(ang1)*cos(ang3)); SGGeoc pt3; SGGeodesy::advanceRadM(a, crs13, dst13 * SG_RAD_TO_NM * SG_NM_TO_METER, pt3); double lat3 = asin(sinLat1 * cos(dst13) + cosLat1 * sin(dst13) * cos(crs13)); //dlon=atan2(sin(crs13)*sin(dst13)*cos(lat1),cos(dst13)-sin(lat1)*sin(lat3)) double dlon = atan2(sin(crs13)*sin(dst13)*cosLat1, cos(dst13)- (sinLat1 * sin(lat3))); double lon3 = SGMiscd::normalizeAngle(-a.getLongitudeRad()-dlon); result = SGGeoc::fromRadM(-lon3, lat3, a.getRadiusM()); //result = pt3; return true; } } using namespace flightgear; // implement Point(s) known distance from a great circle static double sqr(const double x) { return x * x; } // http://williams.best.vwh.net/avform.htm#POINTDME double pointsKnownDistanceFromGC(const SGGeoc& a, const SGGeoc&b, const SGGeoc& d, double dist) { double A = SGGeodesy::courseRad(a, d) - SGGeodesy::courseRad(a, b); double bDist = SGGeodesy::distanceRad(a, d); // r=(cos(b)^2+sin(b)^2*cos(A)^2)^(1/2) double r = pow(sqr(cos(bDist)) + sqr(sin(bDist)) * sqr(cos(A)), 0.5); double p = atan2(sin(bDist)*cos(A), cos(bDist)); if (sqr(cos(dist)) > sqr(r)) { SG_LOG(SG_NAVAID, SG_INFO, "pointsKnownDistanceFromGC, no points exist"); return -1.0; } double dp1 = p + acos(cos(dist)/r); double dp2 = p - acos(cos(dist)/r); double dp1Nm = fabs(dp1 * SG_RAD_TO_NM); double dp2Nm = fabs(dp2 * SG_RAD_TO_NM); return SGMiscd::min(dp1Nm, dp2Nm); } // http://williams.best.vwh.net/avform.htm#Int double latitudeForGCLongitude(const SGGeoc& a, const SGGeoc& b, double lon) { #if 0 Intermediate points {lat,lon} lie on the great circle connecting points 1 and 2 when: lat=atan((sin(lat1)*cos(lat2)*sin(lon-lon2) -sin(lat2)*cos(lat1)*sin(lon-lon1))/(cos(lat1)*cos(lat2)*sin(lon1-lon2))) #endif double lonDiff = a.getLongitudeRad() - b.getLongitudeRad(); double cosLat1 = cos(a.getLatitudeRad()), cosLat2 = cos(b.getLatitudeRad()); double x = sin(a.getLatitudeRad()) * cosLat2 * sin(lon - b.getLongitudeRad()); double y = sin(b.getLatitudeRad()) * cosLat1 * sin(lon - a.getLongitudeRad()); double denom = cosLat1 * cosLat2 * sin(lonDiff); double lat = atan((x - y) / denom); return lat; } static double magVarFor(const SGGeod& geod) { double jd = globals->get_time_params()->getJD(); return sgGetMagVar(geod, jd) * SG_RADIANS_TO_DEGREES; } SGGeod turnCenterOverflight(const SGGeod& pt, double inHeadingDeg, double turnAngleDeg, double turnRadiusM) { double p = copysign(90.0, turnAngleDeg); return SGGeodesy::direct(pt, inHeadingDeg + p, turnRadiusM); } SGGeod turnCenterFlyBy(const SGGeod& pt, double inHeadingDeg, double turnAngleDeg, double turnRadiusM) { double halfAngle = turnAngleDeg * 0.5; double turnCenterOffset = turnRadiusM / cos(halfAngle * SG_DEGREES_TO_RADIANS); double p = copysign(90.0, turnAngleDeg); return SGGeodesy::direct(pt, inHeadingDeg + halfAngle + p, turnCenterOffset); } SGGeod turnCenterFromExit(const SGGeod& pt, double outHeadingDeg, double turnAngleDeg, double turnRadiusM) { double p = copysign(90.0, turnAngleDeg); return SGGeodesy::direct(pt, outHeadingDeg + p, turnRadiusM); } struct TurnInfo { SGGeod turnCenter; double inboundCourseDeg = 0.0; double turnAngleDeg = 0.0; bool valid = false; }; /** * given a turn exit position and heading, and an arbitrary origin position, * compute the turn center / angle that matches. Certain configurations may * fail, especially if the origin is less than two turn radii from the exit pos. */ TurnInfo turnCenterAndAngleFromExit(const SGGeod& pt, double outHeadingDeg, double turnRadiusM, const SGGeod&origin) { double bearingToExit = SGGeodesy::courseDeg(origin, pt); // not the final turn angle, but we need to know which side of the // exit course we're on, to decide if it's a left-hand or right-hand turn double turnAngle = outHeadingDeg - bearingToExit; SG_NORMALIZE_RANGE(turnAngle, -180.0, 180.0); double p = copysign(90.0, turnAngle); TurnInfo r; r.turnCenter = SGGeodesy::direct(pt, outHeadingDeg + p, turnRadiusM); double courseToTC, distanceToTC, az2; SGGeodesy::inverse(origin, r.turnCenter, courseToTC, az2, distanceToTC); if (distanceToTC < turnRadiusM) { SG_LOG(SG_NAVAID, SG_WARN, "turnCenterAndAngleFromExit: origin point too close to turn center"); return r; } // find additional course angle away from the exit pos to intersect // the turn circle. double theta = asin(turnRadiusM / distanceToTC) * SG_RADIANS_TO_DEGREES; // invert angle sign so we increase the turn angle theta = -copysign(theta, turnAngle); r.inboundCourseDeg = courseToTC + theta; SG_NORMALIZE_RANGE(r.inboundCourseDeg, 0.0, 360.0); // turn angle must have same direction (sign) as turnAngle above, even if // the turn radius means the sign would cross over (happens if origin point // is close by r.turnAngleDeg = outHeadingDeg - r.inboundCourseDeg; if (r.turnAngleDeg > 0.0) { if (turnAngle < 0.0) r.turnAngleDeg -= 360.0; } else { if (turnAngle > 0.0) r.turnAngleDeg += 360.0; } r.valid = true; return r; } class WayptData; using WayptDataVec = std::vector; using WpDataIt = WayptDataVec::iterator; class WayptData { public: explicit WayptData(WayptRef w) : wpt(w), hasEntry(false), posValid(false), legCourseValid(false), skipped(false), turnEntryAngle(0.0), turnExitAngle(0.0), turnRadius(0.0), legCourseTrue(0.0), pathDistanceM(0.0), turnPathDistanceM(0.0), overflightCompensationAngle(0.0), flyOver(w->flag(WPT_OVERFLIGHT)) { } void initPass0() { const std::string& ty(wpt->type()); if (wpt->flag(WPT_DYNAMIC)) { // presumption is that we always overfly such a waypoint if (ty == "hdgToAlt") { flyOver = true; } } else if (ty == "discontinuity") { } else { pos = wpt->position(); posValid = true; } // of static waypt } /** * test if course of this leg can be adjusted or is contrained to an exact value */ bool isCourseConstrained() const { const std::string& ty(wpt->type()); return (ty == "hdgToAlt") || (ty == "radialIntercept") || (ty == "dmeIntercept"); } // compute leg courses for all static legs (both ends are fixed) void initPass1(const WayptData* previous, WayptData* next) { const auto ty = wpt->type(); if ((ty == "vectors") || (ty == "discontinuity")) { // relying on the fact vectors will be followed by a static fix/wpt if (next && next->posValid) { posValid = true; pos = next->pos; } } const bool previousPosValid = previous && previous->posValid; if (ty == "via") { // even though both ends may be known, we don't // want to compute a leg course for a VIA } else if (posValid && !legCourseValid && previousPosValid) { const auto prevWP = previous->wpt; // check for duplicate points now if (prevWP->matches(wpt)) { skipped = true; } // we can compute leg course now if (prevWP->type() == "runway") { // use the runway departure end pos FGRunway* rwy = static_cast(prevWP.get())->runway(); legCourseTrue = SGGeodesy::courseDeg(rwy->end(), pos); } else if (ty == "discontinuity") { // we want to keep the path across DISCONs sharp (no turn entry/exit) // so we keep the leg course invalid, even though we actually // could compute it } else if (ty != "runway") { // need to wait to compute runway leg course legCourseTrue = SGGeodesy::courseDeg(previous->pos, pos); legCourseValid = true; } } // of not a VIA } void computeLegCourse(const WayptData* previous, double radiusM) { if (legCourseValid) { return; } if ((wpt->type() == "via") || (wpt->type() == "discontinuty")) { // do nothing, we can't compute a valid leg course for these types // we'll generate sharp turns in the path but that's no problem. return; } if (!previous || !previous->posValid) { // all the cases below assume previous pas exists and has a valid position SG_LOG(SG_NAVAID, SG_WARN, "RoutePath: Asked to compute leg course, but, previous is invalid"); return; } if (wpt->type() == "runway") { FGRunway* rwy = static_cast(wpt.get())->runway(); flyOver = true; TurnInfo ti = turnCenterAndAngleFromExit(rwy->threshold(), rwy->headingDeg(), radiusM, previous->pos); if (ti.valid) { legCourseTrue = ti.inboundCourseDeg; turnEntryAngle = ti.turnAngleDeg; turnEntryCenter = ti.turnCenter; turnRadius = radiusM; hasEntry = true; turnEntryPos = pointOnEntryTurnFromHeading(ti.inboundCourseDeg); } else { // couldn't compute entry, never mind legCourseTrue = SGGeodesy::courseDeg(previous->pos, rwy->threshold()); } legCourseValid = true; return; } SGGeod previousPos = previous->pos; // use correct location for runway, otherwise use threshold // and get weird courses if (previous->wpt->type() == "runway") { FGRunway* rwy = static_cast(previous->wpt.get())->runway(); previousPos = rwy->end(); } if (posValid) { legCourseTrue = SGGeodesy::courseDeg(previousPos, pos); legCourseValid = true; } else if (isCourseConstrained()) { double magVar = magVarFor(previousPos); legCourseTrue = wpt->headingRadialDeg() + magVar; legCourseValid = true; } // of pos not valid } SGGeod pointOnEntryTurnFromHeading(double headingDeg) const { assert(hasEntry); double p = copysign(90.0, turnEntryAngle); return SGGeodesy::direct(turnEntryCenter, headingDeg - p, turnRadius); } SGGeod pointOnExitTurnFromHeading(double headingDeg) const { double p = copysign(90.0, turnExitAngle); return SGGeodesy::direct(turnExitCenter, headingDeg - p, turnRadius); } double pathDistanceForTurnAngle(double angleDeg) const { return turnRadius * fabs(angleDeg) * SG_DEGREES_TO_RADIANS; } void computeTurn(double radiusM, bool constrainLegCourse, double maxFlyByTurnAngleDeg, WayptData& next) { assert(!skipped); assert(next.legCourseValid); bool isRunway = (wpt->type() == "runway"); if (legCourseValid) { if (isRunway) { FGRunway* rwy = static_cast(wpt.get())->runway(); turnExitAngle = next.legCourseTrue - rwy->headingDeg(); } else { turnExitAngle = next.legCourseTrue - legCourseTrue; } } else { // happens for first leg if (isRunway) { legCourseValid = true; FGRunway* rwy = static_cast(wpt.get())->runway(); turnExitAngle = next.legCourseTrue - rwy->headingDeg(); legCourseTrue = rwy->headingDeg(); flyOver = true; // fall through } else { legCourseValid = true; legCourseTrue = next.legCourseTrue; turnExitAngle = 0.0; turnExitPos = pos; flyOver = true; return; } } SG_NORMALIZE_RANGE(turnExitAngle, -180.0, 180.0); turnRadius = radiusM; if (!flyOver && fabs(turnExitAngle) > maxFlyByTurnAngleDeg) { // flyBy logic blows up for sharp turns - due to the tan() term // heading towards infinity. By converting to flyOver we do something // closer to what was requested. This matches logic in the RNAV // Leg controller flyOver = true; } if (flyOver) { if (isRunway) { FGRunway* rwy = static_cast(wpt.get())->runway(); turnExitCenter = turnCenterOverflight(rwy->end(), rwy->headingDeg(), turnExitAngle, turnRadius); } else { turnEntryPos = pos; turnExitCenter = turnCenterOverflight(pos, legCourseTrue, turnExitAngle, turnRadius); } turnExitPos = pointOnExitTurnFromHeading(next.legCourseTrue); if (!next.wpt->flag(WPT_DYNAMIC)) { // distance perpendicular to next leg course, after turning // through turnAngle double xtk = turnRadius * (1 - cos(turnExitAngle * SG_DEGREES_TO_RADIANS)); if (constrainLegCourse || next.isCourseConstrained()) { // next leg course is constrained. We need to swing back onto the // desired course, using a compensation turn // compensation angle to turn back on course double theta = acos((turnRadius - (xtk * 0.5)) / turnRadius) * SG_RADIANS_TO_DEGREES; theta = copysign(theta, turnExitAngle); turnExitAngle += theta; double p = copysign(90, turnExitAngle); const double offsetAngle = legCourseTrue + turnExitAngle - p; SGGeod tc2 = SGGeodesy::direct(turnExitCenter, offsetAngle, turnRadius * 2.0); turnExitPos = SGGeodesy::direct(tc2, next.legCourseTrue + p , turnRadius); overflightCompensationAngle = -theta; // sign of angles will differ, so compute distances seperately turnPathDistanceM = pathDistanceForTurnAngle(turnExitAngle) + pathDistanceForTurnAngle(overflightCompensationAngle); } else { // next leg course can be adjusted. increase the turn angle // and modify the next leg's course accordingly. // hypotenuse of triangle, opposite edge has length turnRadius double distAlongPath = std::min(1.0, sin(fabs(turnExitAngle) * SG_DEGREES_TO_RADIANS)) * turnRadius; double nextLegDistance = SGGeodesy::distanceM(pos, next.pos) - distAlongPath; double increaseAngle = atan2(xtk, nextLegDistance) * SG_RADIANS_TO_DEGREES; increaseAngle = copysign(increaseAngle, turnExitAngle); turnExitAngle += increaseAngle; turnExitPos = pointOnExitTurnFromHeading(legCourseTrue + turnExitAngle); // modify next leg course next.legCourseTrue = SGGeodesy::courseDeg(turnExitPos, next.pos); turnPathDistanceM = pathDistanceForTurnAngle(turnExitAngle); } // of next leg isn't course constrained } else { // next point is dynamic // no compensation needed turnPathDistanceM = pathDistanceForTurnAngle(turnExitAngle); } } else { hasEntry = true; turnEntryCenter = turnCenterFlyBy(pos, legCourseTrue, turnExitAngle, turnRadius); turnExitAngle = turnExitAngle * 0.5; turnEntryAngle = turnExitAngle; turnExitCenter = turnEntryCenter; // important that these match turnEntryPos = pointOnEntryTurnFromHeading(legCourseTrue); turnExitPos = pointOnExitTurnFromHeading(next.legCourseTrue); turnPathDistanceM = pathDistanceForTurnAngle(turnEntryAngle); } } double turnDistanceM() const { return turnPathDistanceM; } void turnEntryPath(SGGeodVec& path) const { if (!hasEntry || fabs(turnEntryAngle) < 0.5 ) { path.push_back(pos); return; } int steps = std::max(SGMiscd::roundToInt(fabs(turnEntryAngle) / 3.0), 1); double stepIncrement = turnEntryAngle / steps; double h = legCourseTrue; for (int s=0; stype() == "runway") { FGRunway* rwy = static_cast(wpt.get())->runway(); h = rwy->headingDeg(); } for (int s=0; s basicTurnDistance) { assert(flyOver); assert(overflightCompensationAngle != 0.0); double p = copysign(90, turnExitAngle); double distanceAlongCompensationTurn = distanceM - basicTurnDistance; double theta = (distanceAlongCompensationTurn / turnRadius) * SG_RADIANS_TO_DEGREES; // compute the compensation turn center - twice the turn radius // from turnCenter SGGeod tc2 = SGGeodesy::direct(turnExitCenter, legCourseTrue + turnExitAngle - p, turnRadius * 2.0); theta = copysign(theta, overflightCompensationAngle); return SGGeodesy::direct(tc2, legCourseTrue + turnExitAngle + theta + p, turnRadius); } double theta = (distanceM / turnRadius) * SG_RADIANS_TO_DEGREES; theta = copysign(theta, turnExitAngle); double inboundCourse = legCourseTrue + (flyOver ? 0.0 : turnExitAngle); return pointOnExitTurnFromHeading(inboundCourse + theta); } SGGeod pointAlongEntryPath(double distanceM) const { assert(hasEntry); double theta = (distanceM / turnRadius) * SG_RADIANS_TO_DEGREES; theta = copysign(theta, turnEntryAngle); return pointOnEntryTurnFromHeading(legCourseTrue + theta); } WayptRef wpt; bool hasEntry, posValid, legCourseValid, skipped; SGGeod pos, turnEntryPos, turnExitPos, turnEntryCenter, turnExitCenter; double turnEntryAngle, turnExitAngle, turnRadius, legCourseTrue; double pathDistanceM; double turnPathDistanceM; // for flyBy, this is half the distance; for flyOver it's the complete distance double overflightCompensationAngle; bool flyOver; }; bool isDescentWaypoint(const WayptRef& wpt) { return (wpt->flag(WPT_APPROACH) && !wpt->flag(WPT_MISS)) || wpt->flag(WPT_ARRIVAL); } class RoutePath::RoutePathPrivate { public: WayptDataVec waypoints; AircraftPerformance perf; bool constrainLegCourses; double maxFlyByTurnAngleDeg = 90.0; void computeDynamicPosition(int index) { auto previous(previousValidWaypoint(index)); if ((previous == waypoints.end()) || !previous->posValid) { SG_LOG(SG_NAVAID, SG_WARN, "couldn't compute position for dynamic waypoint: no preceeding valid waypoint"); return; } WayptRef wpt = waypoints[index].wpt; const std::string& ty(wpt->type()); if (ty == "hdgToAlt") { HeadingToAltitude* h = (HeadingToAltitude*)wpt.get(); double altFt = computeVNAVAltitudeFt(index - 1); double distanceM = perf.distanceNmBetween(altFt, h->altitudeFt()) * SG_NM_TO_METER; double hdg = h->headingDegMagnetic() + magVarFor(previous->pos); waypoints[index].pos = SGGeodesy::direct(previous->turnExitPos, hdg, distanceM); waypoints[index].posValid = true; } else if (ty == "radialIntercept") { // start from previous.turnExit RadialIntercept* i = (RadialIntercept*)wpt.get(); SGGeoc prevGc = SGGeoc::fromGeod(previous->turnExitPos); SGGeoc navid = SGGeoc::fromGeod(wpt->position()); SGGeoc rGc; double magVar = magVarFor(previous->pos); double radial = i->radialDegMagnetic() + magVar; double track = i->courseDegMagnetic() + magVar; bool ok = geocRadialIntersection(prevGc, track, navid, radial, rGc); if (!ok) { // try pulling backward along the radial in case we're too close. // suggests bad procedure construction if this is happening! SGGeoc navidAdjusted = SGGeodesy::advanceDegM(navid, radial, -10 * SG_NM_TO_METER); // try again ok = geocRadialIntersection(prevGc, track, navidAdjusted, radial, rGc); if (!ok) { SG_LOG(SG_NAVAID, SG_WARN, "couldn't compute interception for radial:" << previous->turnExitPos << " / " << track << "/" << wpt->position() << "/" << radial); waypoints[index].pos = wpt->position(); // horrible fallback } else { waypoints[index].pos = SGGeod::fromGeoc(rGc); } } else { waypoints[index].pos = SGGeod::fromGeoc(rGc); } waypoints[index].posValid = true; } else if (ty == "dmeIntercept") { DMEIntercept* di = (DMEIntercept*)wpt.get(); SGGeoc prevGc = SGGeoc::fromGeod(previous->turnExitPos); SGGeoc navid = SGGeoc::fromGeod(wpt->position()); double distRad = di->dmeDistanceNm() * SG_NM_TO_RAD; SGGeoc rGc; double crs = di->courseDegMagnetic() + magVarFor(wpt->position()); SGGeoc bPt = SGGeodesy::advanceDegM(prevGc, crs, 1e5); double dNm = pointsKnownDistanceFromGC(prevGc, bPt, navid, distRad); if (dNm < 0.0) { SG_LOG(SG_NAVAID, SG_WARN, "dmeIntercept failed"); waypoints[index].pos = wpt->position(); // horrible fallback } else { waypoints[index].pos = SGGeodesy::direct(previous->turnExitPos, crs, dNm * SG_NM_TO_METER); } waypoints[index].posValid = true; } else if (ty == "vectors") { waypoints[index].legCourseTrue = SGGeodesy::courseDeg(previous->turnExitPos, waypoints[index].pos); waypoints[index].legCourseValid = true; // no turn data } } double computeVNAVAltitudeFt(int index) { WayptRef w = waypoints[index].wpt; if ((w->flag(WPT_APPROACH) && !w->flag(WPT_MISS)) || w->flag(WPT_ARRIVAL)) { // descent int next = findNextKnownAltitude(index); if (next < 0) { return 0.0; } double fixedAlt = altitudeForIndex(next); double distanceM = distanceBetweenIndices(index, next); return perf.computePreviousAltitude(distanceM, fixedAlt); } else { // climb int prev = findPreceedingKnownAltitude(index); if (prev < 0) { return 0.0; } double fixedAlt = altitudeForIndex(prev); double distanceM = distanceBetweenIndices(prev, index); return perf.computeNextAltitude(distanceM, fixedAlt); } } int findPreceedingKnownAltitude(int index) const { const WayptData& w(waypoints[index]); if (w.wpt->altitudeRestriction() == RESTRICT_AT) { return index; } // principal base case is runways. const std::string& ty(w.wpt->type()); if (ty == "runway") { return index; // runway always has a known elevation } if (index == 0) { SG_LOG(SG_NAVAID, SG_WARN, "findPreceedingKnownAltitude: no preceding altitude value found"); return -1; } // recurse earlier in the route return findPreceedingKnownAltitude(index - 1); } int findNextKnownAltitude(unsigned int index) const { if (index >= waypoints.size()) { SG_LOG(SG_NAVAID, SG_WARN, "findNextKnownAltitude: no next altitude value found"); return -1; } const WayptData& w(waypoints[index]); if (w.wpt->altitudeRestriction() == RESTRICT_AT) { return index; } // principal base case is runways. const std::string& ty(w.wpt->type()); if (ty == "runway") { return index; // runway always has a known elevation } if (index == waypoints.size() - 1) { SG_LOG(SG_NAVAID, SG_WARN, "findNextKnownAltitude: no next altitude value found"); return -1; } return findNextKnownAltitude(index + 1); } double altitudeForIndex(int index) const { const WayptData& w(waypoints[index]); if (w.wpt->altitudeRestriction() != RESTRICT_NONE) { return w.wpt->altitudeFt(); } const std::string& ty(w.wpt->type()); if (ty == "runway") { FGRunway* rwy = static_cast(w.wpt.get())->runway(); return rwy->threshold().getElevationFt(); } SG_LOG(SG_NAVAID, SG_WARN, "altitudeForIndex: waypoint has no explicit altitude"); return 0.0; } double distanceBetweenIndices(int from, int to) const { double total = 0.0; for (int i=from+1; i<= to; ++i) { total += waypoints[i].pathDistanceM; } return total; } WayptDataVec::iterator previousValidWaypoint(unsigned int index) { do { if (index == 0) { return waypoints.end(); } --index; } while (waypoints.at(index).skipped || (waypoints.at(index).wpt->type() == "discontinuity")); return waypoints.begin() + index; } WayptDataVec::iterator previousValidWaypoint(WayptDataVec::iterator it) { return previousValidWaypoint(std::distance(waypoints.begin(), it)); } WayptDataVec::iterator nextValidWaypoint(int index) { return nextValidWaypoint(waypoints.begin() + index); } WayptDataVec::iterator nextValidWaypoint(WayptDataVec::iterator it) { if (it == waypoints.end()) { return it; } ++it; while ((it != waypoints.end()) && (it->skipped || (it->wpt->type() == "discontinuity"))) { ++it; } return it; } }; // of RoutePathPrivate class RoutePath::RoutePath(const flightgear::FlightPlan* fp) : d(new RoutePathPrivate) { for (int l=0; lnumLegs(); ++l) { WayptRef wpt = fp->legAtIndex(l)->waypoint(); if (!wpt) { SG_LOG(SG_NAVAID, SG_DEV_ALERT, "Waypoint " << l << " of " << fp->numLegs() << "is NULL"); break; } d->waypoints.push_back(WayptData(wpt)); } d->constrainLegCourses = fp->followLegTrackToFixes(); commonInit(); } RoutePath::RoutePath(const RoutePath& other) { d.reset(new RoutePathPrivate(*other.d)); } RoutePath& RoutePath::operator=(const RoutePath& other) { d.reset(new RoutePathPrivate(*other.d)); return *this; } RoutePath::~RoutePath() { } void RoutePath::commonInit() { for (auto& w : d->waypoints) { w.initPass0(); } for (unsigned int i=1; iwaypoints.size(); ++i) { WayptData* nextPtr = ((i + 1) < d->waypoints.size()) ? &d->waypoints[i+1] : nullptr; auto prev = d->previousValidWaypoint(i); WayptData* prevPtr = (prev == d->waypoints.end()) ? nullptr : &(*prev); d->waypoints[i].initPass1(prevPtr, nextPtr); } for (unsigned int i=0; iwaypoints.size(); ++i) { if (d->waypoints[i].skipped) { continue; } double alt = 0.0; // FIXME double radiusM = d->perf.turnRadiusMForAltitude(alt); if (i > 0) { auto prevIt = d->previousValidWaypoint(i); WayptData* prevPtr = (prevIt == d->waypoints.end()) ? nullptr : &(*prevIt); d->waypoints[i].computeLegCourse(prevPtr, radiusM); d->computeDynamicPosition(i); } auto nextIt = d->nextValidWaypoint(i); if (nextIt != d->waypoints.end()) { nextIt->computeLegCourse(&(d->waypoints[i]), radiusM); if (nextIt->legCourseValid) { d->waypoints[i].computeTurn(radiusM, d->constrainLegCourses, d->maxFlyByTurnAngleDeg, *nextIt); } else { // next waypoint has indeterminate course. Let's create a sharp turn // this can happen when the following point is ATC vectors, for example. d->waypoints[i].turnEntryPos = d->waypoints[i].pos; d->waypoints[i].turnExitPos = d->waypoints[i].pos; } } else { // final waypt, fix up some data d->waypoints[i].turnExitPos = d->waypoints[i].pos; d->waypoints[i].turnEntryPos = d->waypoints[i].pos; } // now turn is computed, can resolve distances d->waypoints[i].pathDistanceM = computeDistanceForIndex(i); } } SGGeodVec RoutePath::pathForIndex(int index) const { if ((index < 0) || (index >= static_cast(d->waypoints.size()))) { return {}; } const WayptData& w(d->waypoints[index]); const std::string& ty(w.wpt->type()); SGGeodVec r; if (d->waypoints[index].skipped) { return {}; } // don't show any path if (w.wpt->flag(WPT_HIDDEN)) { return {}; } // also hide the path if the prev WP is hidden auto prevIt = d->previousValidWaypoint(index); if (prevIt != d->waypoints.end()) { if (prevIt->wpt->flag(WPT_HIDDEN)) { return {}; } } if (ty == "vectors") { // ideally we'd show a stippled line to connect the route? return {}; } if (ty == "discontinuity") { return {}; // no points for a discontinuity of course } if (ty == "via") { return pathForVia(static_cast(d->waypoints[index].wpt.get()), index); } if (prevIt != d->waypoints.end()) { prevIt->turnExitPath(r); SGGeod from = prevIt->turnExitPos, to = w.turnEntryPos; // compute rounding offset, we want to round towards the direction of travel // which depends on the east/west sign of the longitude change double lonDelta = to.getLongitudeDeg() - from.getLongitudeDeg(); if (fabs(lonDelta) > 0.5) { interpolateGreatCircle(from, to, r); } } // of have previous waypoint w.turnEntryPath(r); // hold is the normal leg and then the hold waypoints as well if (ty== "hold") { const auto h = static_cast(d->waypoints[index].wpt.get()); const auto holdPath = pathForHold(h); r.insert(r.end(), holdPath.begin(), holdPath.end()); } if (ty == "runway") { // runways get an extra point, at the end. this is particularly // important so missed approach segments draw correctly FGRunway* rwy = static_cast(w.wpt.get())->runway(); r.push_back(rwy->end()); } return r; } void RoutePath::interpolateGreatCircle(const SGGeod& aFrom, const SGGeod& aTo, SGGeodVec& r) const { SGGeoc gcFrom = SGGeoc::fromGeod(aFrom), gcTo = SGGeoc::fromGeod(aTo); double lonDelta = gcTo.getLongitudeRad() - gcFrom.getLongitudeRad(); if (fabs(lonDelta) < 1e-3) { return; } lonDelta = SGMiscd::normalizeAngle(lonDelta); int steps = static_cast(fabs(lonDelta) * SG_RADIANS_TO_DEGREES * 2); double lonStep = (lonDelta / steps); double lon = gcFrom.getLongitudeRad() + lonStep; for (int s=0; s < (steps - 1); ++s) { lon = SGMiscd::normalizeAngle(lon); double lat = latitudeForGCLongitude(gcFrom, gcTo, lon); r.push_back(SGGeod::fromGeoc(SGGeoc::fromRadM(lon, lat, SGGeodesy::EQURAD))); //SG_LOG(SG_GENERAL, SG_INFO, "lon:" << lon * SG_RADIANS_TO_DEGREES << " gives lat " << lat * SG_RADIANS_TO_DEGREES); lon += lonStep; } } SGGeod RoutePath::positionForIndex(int index) const { if ((index < 0) || (index >= static_cast(d->waypoints.size()))) { return {}; } return d->waypoints[index].pos; } SGGeodVec RoutePath::pathForVia(Via* via, int index) const { // previous waypoint must be valid for a VIA auto prevIt = d->previousValidWaypoint(index); if (prevIt == d->waypoints.end()) { return SGGeodVec(); } WayptVec enrouteWaypoints = via->expandToWaypoints(prevIt->wpt); SGGeodVec r; WayptVec::const_iterator it; SGGeod legStart = prevIt->wpt->position(); for (it = enrouteWaypoints.begin(); it != enrouteWaypoints.end(); ++it) { // interpolate directly into the result vector interpolateGreatCircle(legStart, (*it)->position(), r); legStart = (*it)->position(); } return r; } SGGeodVec RoutePath::pathForHold(Hold* hold) const { int turnSteps = 16; double hdg = hold->inboundRadial(); double turnDelta = 180.0 / turnSteps; double altFt = 0.0; // FIXME double gsKts = d->perf.groundSpeedForAltitudeKnots(altFt); SGGeodVec r; double az2; double stepTime = turnDelta / 3.0; // 3.0 is degrees/sec for standard rate turn double stepDist = gsKts * SG_KT_TO_MPS * stepTime; double legDist = hold->isDistance() ? hold->timeOrDistance() : gsKts * (hold->timeOrDistance() / 3600.0); legDist *= SG_NM_TO_METER; if (hold->isLeftHanded()) { turnDelta = -turnDelta; } SGGeod pos = hold->position(); r.push_back(pos); // turn+leg sides are a mirror for (int j=0; j < 2; ++j) { // turn for (int i=0;i= static_cast(d->waypoints.size()))) { return 0.0; } auto it = d->waypoints.begin() + index; if ((index == 0) || it->skipped) { // first waypoint, distance is 0 return 0.0; } const auto ty = it->wpt->type(); if (ty == "via") { return distanceForVia(static_cast(it->wpt.get()), index); } if (ty == "discontinuity") { return 0.0; } auto prevIt = d->previousValidWaypoint(index); if (prevIt == d->waypoints.end()) { return 0.0; } double dist = SGGeodesy::distanceM(prevIt->turnExitPos, it->turnEntryPos); dist += prevIt->turnDistanceM(); if (!it->flyOver) { // add entry distance dist += it->turnDistanceM(); } return dist; } double RoutePath::distanceForVia(Via* via, int index) const { auto prevIt = d->previousValidWaypoint(index); if (prevIt == d->waypoints.end()) { return 0.0; } WayptVec enrouteWaypoints = via->expandToWaypoints(prevIt->wpt); double dist = 0.0; SGGeod legStart = prevIt->wpt->position(); for (auto wp : enrouteWaypoints) { dist += SGGeodesy::distanceM(legStart, wp->position()); legStart = wp->position(); } return dist; } double RoutePath::trackForIndex(int index) const { const auto sz = static_cast(d->waypoints.size()); if ((index < 0) || (index >= sz)) { return 0.0; } if (d->waypoints[index].skipped) return trackForIndex(index - 1); const WayptData& wd(d->waypoints[index]); if (!wd.legCourseValid) return 0.0; return wd.legCourseTrue; } double RoutePath::distanceForIndex(int index) const { const auto sz = static_cast(d->waypoints.size()); if ((index < 0) || (index >= sz)) { return 0.0; } return d->waypoints[index].pathDistanceM; } double RoutePath::distanceBetweenIndices(int from, int to) const { if (from < 0) { from = 0; } const auto sz = static_cast(d->waypoints.size()); if (to >= sz) { to = sz - 1; } return d->distanceBetweenIndices(from, to); } SGGeod RoutePath::positionForDistanceFrom(int index, double distanceM) const { int sz = (int) d->waypoints.size(); if (index < 0) { index = sz - 1; // map negative values to end of the route } if ((index < 0) || (index >= sz)) { throw sg_range_exception("waypt index out of range", "RoutePath::positionForDistanceFrom"); } // find the actual leg we're within if (distanceM < 0.0) { // scan backwards while ((index > 0) && (distanceM < 0.0)) { // we are looking at index n, say 4, but with a negative distance. // we want to look at index n-1 (so, 3), and see if this makes // distance positive. We need to offset by distance from 3 -> 4, // which is waypoint 4's path distance. // note pathDistanceM is 0 for skipped waypoints, so this works out distanceM += d->waypoints[index].pathDistanceM; --index; } if (distanceM < 0.0) { // still negative, return route start return d->waypoints[0].pos; } } else { // scan forwards int nextIndex = index + 1; while ((nextIndex < sz) && (d->waypoints[nextIndex].pathDistanceM < distanceM)) { distanceM -= d->waypoints[nextIndex].pathDistanceM; index = nextIndex++; } } auto nextIt = d->nextValidWaypoint(index); if (nextIt == d->waypoints.end()) { // past route end, just return final position return d->waypoints[sz - 1].pos; } // this is important so we start from a valid WP if we're // working either side of a DISCON auto curIt = d->previousValidWaypoint(nextIt); if (curIt == d->waypoints.end()) { SG_LOG(SG_NAVAID, SG_WARN, "Couldn't find valid preceeding waypoint " << index); return nextIt->pos; } const WayptData& wpt = *curIt; const WayptData& next = *nextIt; if (next.wpt->type() == "via") { return positionAlongVia(static_cast(next.wpt.get()), index, distanceM); } if (wpt.turnPathDistanceM > distanceM) { // on the exit path of current wpt return wpt.pointAlongExitPath(distanceM); } else { distanceM -= wpt.turnPathDistanceM; } double corePathDistance = next.pathDistanceM - next.turnPathDistanceM; if (next.hasEntry && (distanceM > corePathDistance)) { // on the entry path of next waypoint return next.pointAlongEntryPath(distanceM - corePathDistance); } // linear between turn exit and turn entry points return SGGeodesy::direct(wpt.turnExitPos, next.legCourseTrue, distanceM); } SGGeod RoutePath::positionAlongVia(Via* via, int previousIndex, double distanceM) const { SG_LOG(SG_NAVAID, SG_ALERT, "RoutePath::positionAlongVia not implemented"); return SGGeod(); }