diff --git a/Include/Constants.h b/Include/Constants.h index 3b1f2a82..b857f4d8 100644 --- a/Include/Constants.h +++ b/Include/Constants.h @@ -29,7 +29,7 @@ // // MARK: Version Information (CHANGE VERSION HERE) // -constexpr float VERSION_NR = 1.24f; +constexpr float VERSION_NR = 1.50f; constexpr bool VERSION_BETA = false; extern float verXPlaneOrg; // version on X-Plane.org extern int verDateXPlaneOrg; // and its date @@ -71,7 +71,7 @@ constexpr double FLIGHT_LOOP_INTVL = -5.0; // call ourselves every 5 frames constexpr double AC_MAINT_INTVL = 2.0; // seconds (calling a/c maintenance periodically) constexpr double TIME_REQU_POS = 0.5; // seconds before reaching current 'to' position we request calculation of next position constexpr double SIMILAR_TS_INTVL = 3; // seconds: Less than that difference and position-timestamps are considered "similar" -> positions are merged rather than added additionally -constexpr double SIMILAR_POS_DIST = 3; // [m] if distance between positions less than this then favor heading from flight data over vector between positions +constexpr double SIMILAR_POS_DIST = 10; // [m] if distance between positions less than this then favor heading from flight data over vector between positions constexpr double FD_GND_AGL = 50; // [ft] consider pos 'ON GRND' if this close to YProbe constexpr double PROBE_HEIGHT_LIM[] = {5000,1000,500,-999999}; // if height AGL is more than ... feet constexpr double PROBE_DELAY[] = { 10, 1,0.5, 0.2}; // delay next Y-probe ... seconds. @@ -79,6 +79,10 @@ constexpr double AC_HIDE_LAT = -70.645077; // Neumayer-Station III constexpr double AC_HIDE_LON = -8.264134; constexpr double AC_HIDE_ALT = 50; constexpr double MAX_HOVER_AGL = 2000; // [ft] max hovering altitude for hover-along-the-runway detection +constexpr double KEEP_ABOVE_MAX_ALT = 18000.0 * M_per_FT;///< [m] Maximum altitude to which the "keep above 2.5¡ glidescope" algorithm is applied (highest airports are below 15,000ft + 3,000 for approach) +constexpr double KEEP_ABOVE_MAX_AGL = 3000.0 * M_per_FT;///< [m] Maximum height above ground to which the "keep above 2.5¡ glidescope" algorithm is applied (highest airports are below 15,000ft + 3,000 for approach) +constexpr double KEEP_ABOVE_RATIO = 0.043495397807572; ///< = tan(2.5¡), slope ratio for keeping a plane above the approach to a runway +constexpr double BEZIER_MIN_HEAD_DIFF = 2.0; ///< [¡] turns of less than this will not be modeled with Bezier curves //MARK: Flight Model constexpr double MDL_ALT_MIN = -1500; // [ft] minimum allowed altitude @@ -86,8 +90,7 @@ constexpr double MDL_ALT_MAX = 60000; // [ft] maximum allowed altitude constexpr double MDL_CLOSE_TO_GND = 0.5; // feet height considered "on ground" constexpr double MDL_MAX_TURN = 90; // max turn in flight at a position constexpr double MDL_MAX_TURN_GND = 120; // max turn on the ground -constexpr double MDL_SAME_TRACK_DIFF = 3.0; // [¡] max degree difference considered "same track" -constexpr double MDL_TO_LOOK_AHEAD = 35.0; // [s] to look ahead for take off prediction +constexpr double MDL_TO_LOOK_AHEAD = 60.0; // [s] to look ahead for take off prediction constexpr float MDL_EXT_CAMERA_PITCH = -5; // initial pitch constexpr float MDL_EXT_STEP_MOVE = 0.5f; // [m] to move with one command constexpr float MDL_EXT_FAST_MOVE = 5.0f; // ...a 'fast' command @@ -110,12 +113,25 @@ constexpr int COLOR_BLUE = 0x00F0F0; // light blue //MARK: Airports, Runways, Taxiways constexpr double ART_EDGE_ANGLE_TOLERANCE=30.0; ///< [¡] tolerance of searched heading to edge's angle to be considered a fit +constexpr double ART_EDGE_ANGLE_TOLERANCE_EXT=80.0; ///< [¡] extended (second prio) tolerance of searched heading to edge's angle to be considered a fit +constexpr double ART_EDGE_ANGLE_EXT_DIST=5.0; ///< [m] Second prio angle tolerance wins, if such a node is this much closer than an first priority angle match constexpr double ART_RWY_TD_POINT_F = 0.10; ///< [-] Touch-down point is this much into actual runway (so we don't touch down at its actual beginning) -constexpr double ART_RWY_MAX_HEAD_DIFF = 10.0; ///< [¡] maximum heading difference between flight and runway +constexpr double ART_RWY_MAX_HEAD_DIFF = 15.0; ///< [¡] maximum heading difference between flight and runway +constexpr double ART_RWY_MAX_DIST = 20.0 * M_per_NM; ///< [m] maximum distance to a runway when searching for one constexpr double ART_RWY_MAX_VSI_F = 2.0; ///< [-] descend rate: maximum allowed factor applied to VSI_FINAL constexpr double ART_RWY_ALIGN_DIST = 500.0; ///< [m] distance before touch down to be fully aligned with rwy constexpr double ART_APPR_SPEED_F = 0.8; ///< [-] ratio of FLAPS_DOWN_SPEED to use as max approach speed constexpr double ART_FINAL_SPEED_F = 0.7; ///< [-] ratio of FLAPS_DOWN_SPEED to use as max final speed +constexpr double ART_TAXI_SPEED_F = 0.8; ///< [-] ratio of MAX_TAXI_SPEED to use as taxi speed +constexpr double APT_MAX_TAXI_SEGM_TURN = 15.0; ///< [¡] Maximum turn angle (compared to original edge's angle) for combining edges +constexpr double APT_MAX_SIMILAR_NODE_DIST_M = 2.0; ///< [m] Max distance for two taxi nodes to be considered "similar", so that only one of them is kept +constexpr double APT_STARTUP_VIA_DIST = 50.0; ///< [m] distance of StartupLoc::viaLoc from startup location +constexpr double APT_STARTUP_MOVE_BACK = 10.0; ///< [m] move back startup location so that it sits about in plane's center instead of at its head +constexpr double APT_JOIN_MAX_DIST_M = 15.0; ///< [m] Max distance for an open node to be joined with another edge +constexpr double APT_JOIN_ANGLE_TOLERANCE=15.0; ///< [¡] tolerance of angle for an open node to be joined with another edge +constexpr double APT_JOIN_ANGLE_TOLERANCE_EXT=45.0; ///< [¡] extended (second prio) tolerance of angle for an open node to be joined with another edge +constexpr double APT_MAX_PATH_TURN=100.0; ///< [¡] Maximum turn allowed during shortest path calculation +constexpr double APT_RECT_ANGLE_TOLERANCE=10.0; ///< [¡] Tolerance when trying to devide for rectangular angle //MARK: Version Information extern char LT_VERSION[]; // like "1.0" @@ -139,6 +155,8 @@ constexpr int LT_NEW_VER_CHECK_TIME = 48; // [h] between two checks of a new #define MSG_DISABLED "Disabled" #define MSG_STARTUP "LiveTraffic %s starting up..." #define MSG_WELCOME "LiveTraffic %s successfully loaded!" +#define MSG_NOT_MODERN_DRIVER "LiveTraffic %s will NOT work under Vulkan/Metal!" +#define MSG_NOT_MODERN_DRIVER2 "Until v2.0 is availble deactivate Vulkan/Metal in XP's Graphic setting!" #define MSG_REINIT "LiveTraffic is re-initializing itself" #define MSG_DISABLE_MYSELF "LiveTraffic disables itself due to unhandable exceptions" #define MSG_LT_NEW_VER_AVAIL "The new version %01.2f of LiveTraffic is available at X-Plane.com!" @@ -146,7 +164,7 @@ constexpr int LT_NEW_VER_CHECK_TIME = 48; // [h] between two checks of a new #define MSG_READING_HIST_FD "Reading historic flight data..." #define MSG_NUM_AC_INIT "Initially created %d aircraft" #define MSG_NUM_AC_ZERO "No more aircraft displayed" -#define MSG_BUF_FILL_COUNTDOWN "Filling buffer: seeing %d aircraft, displaying %d, still %d seconds to buffer" +#define MSG_BUF_FILL_COUNTDOWN "Filling buffer: seeing %d aircraft, displaying %d, still %ds to buffer" #define MSG_HIST_WITH_SYS_TIME "When using historic data you cannot run X-Plane with 'always track system time',\ninstead, choose the historic date in X-Plane's date/time settings." #define MSG_ADSBEX_LIMITE "%ld / %ld requests left" #define INFO_AC_ADDED "Added aircraft %s, operator '%s', a/c model '%s', flight model [%s], bearing %.0f, distance %.1fnm, from channel %s" @@ -158,7 +176,7 @@ constexpr int LT_NEW_VER_CHECK_TIME = 48; // [h] between two checks of a new #define INFO_AC_HIDDEN_AUTO "A/c %s automatically hidden" #define INFO_AC_SHOWN "A/c %s visible" #define INFO_AC_SHOWN_AUTO "A/c %s automatically visible" -#define MSG_TOO_MANY_AC "Reached limit of %d aircraft, will create new ones only after removing outdated ones." +#define MSG_TOO_MANY_AC "Reached limit of %d aircraft, will render nearest aircraft only." #define MSG_CSL_PACKAGE_LOADED "Successfully loaded CSL package %s" #define MSG_MDL_FORCED "Settings > Debug: Model matching forced to '%s'/'%s'/'%s'" #define MSG_MDL_NOT_FORCED "Settings > Debug: Model matching no longer forced" @@ -196,6 +214,7 @@ constexpr int LT_NEW_VER_CHECK_TIME = 48; // [h] between two checks of a new #define MENU_NEWVER "New Version %01.2f available!" #ifdef DEBUG #define MENU_RELOAD_PLUGINS "Reload all Plugins (Caution!)" +#define MENU_REMOVE_ALL_BUT "Remove all but selected a/c" #endif //MARK: Help URLs @@ -272,7 +291,7 @@ constexpr int SERR_LEN = 100; // size of buffer for IO error t #define ERR_MALLOC "Could not (re)allocate %ld bytes of memory" #define ERR_ASSERT "ASSERT FAILED: %s" #define ERR_AC_NO_POS "No positional data available when creating aircraft %s" -#define ERR_AC_CALC_PPOS "Could calculate position when creating aircraft %s" +#define ERR_AC_CALC_PPOS "Could not calculate position when creating aircraft %s" #define ERR_Y_PROBE "Y Probe returned %d at %s" #define ERR_POS_UNNORMAL "A/c %s reached invalid pos: %s" #define ERR_IGNORE_POS "A/c %s: Ignoring data leading to sharp turn or invalid speed: %s" @@ -327,8 +346,10 @@ constexpr int ERR_CFG_FILE_MAXWARN = 5; // maximum number of warnings while #define DBG_FILTER_AC_REMOVED "DEBUG Filtering for a/c REMOVED" #define DBG_MERGED_POS "DEBUG MERGED POS %s into updated TS %.1f" #define DBG_POS_DATA "DEBUG POS DATA: %s" +#define DBG_KEEP_ABOVE "DEBUG POS LIFTED TO 2.5deg GLIDESCOPE from %.0fft: %s" #define DBG_NO_MORE_POS_DATA "DEBUG NO MORE LIVE POS DATA: %s" #define DBG_SKIP_NEW_POS "DEBUG SKIPPED NEW POS: %s" +#define DBG_ADDED_NEW_POS "DEBUG ADDED NEW POS: %s" #define DBG_INVENTED_STOP_POS "DEBUG INVENTED STOP POS: %s" #define DBG_INVENTED_TD_POS "DEBUG INVENTED TOUCH-DOWN POS: %s" #define DBG_INVENTED_TO_POS "DEBUG INVENTED TAKE-OFF POS: %s" diff --git a/Include/CoordCalc.h b/Include/CoordCalc.h index 80a6270b..ec2f6b0b 100644 --- a/Include/CoordCalc.h +++ b/Include/CoordCalc.h @@ -45,12 +45,19 @@ inline T pyth2 (T a, T b) { return sqr(a) + sqr(b); } //MARK: Degree/Radian conversion // (as per stackoverflow post, adapted) // -inline double deg2rad (const double deg) -{ return (deg * PI / 180); } -inline double rad2deg (const double rad) +/// Converts degree [-180..+360] to radians [-Ï€..+Ï€] +constexpr inline double deg2rad (const double deg) +{ return ((deg <= 180.0 ? deg : deg-360.0) * PI / 180.0); } + +/// Converts radians [-Ï€...+Ï€] to degree [-180..180] +constexpr inline double rad2deg (const double rad) { return (rad * 180 / PI); } +/// Converts radians [-Ï€...+2Ï€] to degree [0..360] +constexpr inline double rad2deg360 (const double rad) +{ return ((rad >= 0.0 ? rad : rad+PI+PI) * 180.0 / PI); } + // angle flown, given speed and vsi (both in m/s) inline double vsi2deg (const double speed, const double vsi) { return rad2deg(std::atan2(vsi,speed)); } @@ -62,6 +69,28 @@ inline double vsi2deg (const double speed, const double vsi) struct positionTy; struct vectorTy; +/// A simple two-dimensional point +struct ptTy { + double x, y; + ptTy () : x(NAN), y(NAN) {} + ptTy (double _x, double _y) : x(_x), y(_y) {} + ptTy operator + (const ptTy& _o) const { return ptTy ( x+_o.x, y+_o.y); } ///< scalar sum + ptTy operator - (const ptTy& _o) const { return ptTy ( x-_o.x, y-_o.y); } ///< scalar difference + bool operator== (const ptTy& _o) const; ///< equality based on dequal() (ie. 'nearly' equal) + bool operator!= (const ptTy& _o) const { return !operator==(_o); } ///< unequality bases on `not equal` + bool isValid() const { return !std::isnan(x) && !std::isnan(y); } ///< valid if both `x` and `y` are not `NAN` + void clear() { x = y = NAN; } ///< set both `x` and `y` to `NAN` + ptTy mirrorAt (const ptTy& _o) const ///< return a point of `this` mirrored at `_o` + { return ptTy (2*_o.x - x, 2*_o.y - y); } + + std::string dbgTxt () const; ///< returns a string "y, x" for the point/position +}; +inline ptTy operator * (double d, ptTy pt) { return ptTy ( d * pt.x, d * pt.y); } ///< scalar multiplication +inline ptTy operator / (ptTy pt, double d) { return ptTy ( pt.x / d, pt.y / d); } ///< scalar division + +/// Vector of points +typedef std::vector vecPtTyT; + /// angle between two locations given in plain lat/lon double CoordAngle (double lat1, double lon1, double lat2, double lon2); /// distance between two locations given in plain lat/lon [meter] @@ -83,13 +112,23 @@ double YProbe_at_m (const positionTy& posAt, XPLMProbeRef& probeRef); // MARK: Estimated Functions on coordinates // -/// @details Length of a degree latitude +/// @brief Length of one degree latitude /// @see https://en.wikipedia.org/wiki/Geographic_coordinate_system#Length_of_a_degree constexpr double LAT_DEG_IN_MTR = 111132.95; -/// @details Length of a degree longitude + +/// @brief Length of a degree longitude /// @see https://en.wikipedia.org/wiki/Geographic_coordinate_system#Length_of_a_degree inline double LonDegInMtr (double lat) { return LAT_DEG_IN_MTR * std::cos(deg2rad(lat)); } +/// Convert vertical distance into degree latitude +constexpr inline double Dist2Lat (double dist_m) { return dist_m / LAT_DEG_IN_MTR; } +/// Convert vertical distance into degree longitude +inline double Dist2Lon (double dist_m, double lat) { return dist_m / LonDegInMtr(lat); } +/// Convert degree latitude into vertical distance +constexpr inline double Lat2Dist (double latDiff) { return latDiff * LAT_DEG_IN_MTR; } +/// Convert degree longitude into vertical distance +inline double Lon2Dist (double lonDiff, double lat) { return lonDiff * LonDegInMtr(lat); } + /// @brief An _estimated_ **square** of the distance between 2 points given by lat/lon /// @details Makes use simple formulas to convert lat/lon differences into meters /// So this is not exact but quick and good enough for many purposes. @@ -123,12 +162,16 @@ struct distToLineTy { double len2 = NAN; ///< square of length of line between ln_x/y1 and ln_x/y2 double leg1_len2 = NAN; ///< square length of leg from point 1 to base (base is point on the line with shortest distance to point) double leg2_len2 = NAN; ///< square length of leg from point 2 to base (base is point on the line with shortest distance to point) + /// Is the base outside the endpoints of the line? bool IsBaseOutsideLine () const { return leg1_len2 > len2 || leg2_len2 > len2; } /// How much is the base outside the (nearer) endpoint? (squared) double DistSqrOfBaseBeyondLine () const { return std::max(leg1_len2,leg2_len2) - len2; } + /// Resulting distance, considering also distance of base outside line as distance + double DistSqrPlusOuts () const + { return dist2 + (IsBaseOutsideLine() ? DistSqrOfBaseBeyondLine () : 0.0); } }; /// @brief Square of distance between a location and a line defined by two points. @@ -150,7 +193,7 @@ void DistPointToLineSqr (double pt_x, double pt_y, double ln_x2, double ln_y2, distToLineTy& outResults); -/// @brief Based on results from DistPointToLineSqr() computes locaton of base point on line +/// @brief Based on results from DistPointToLineSqr() computes locaton of base point (projection) on line /// @param ln_x1 Line: First endpoint's x coordinate (same as passed in to DistPointToLineSqr()) /// @param ln_y1 Line: First endpoint's y coordinate (same as passed in to DistPointToLineSqr()) /// @param ln_x2 Line: Second endpoint's x coordinate (same as passed in to DistPointToLineSqr()) @@ -163,6 +206,100 @@ void DistResultToBaseLoc (double ln_x1, double ln_y1, const distToLineTy& res, double &x, double &y); +/// @brief Intersection point of two lines through given points +/// @see https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line +ptTy CoordIntersect (const ptTy& a, const ptTy& b, const ptTy& c, const ptTy& d, + double* pT = nullptr, + double* pU = nullptr); + +/// @brief Calculate a point on a quadratic Bezier curve +/// @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves +/// @param t Range [0..1] defines which point on the curve to be returned, 0 = p0, 1 = p2 +/// @param p0 Start point of curve, reached with t=0.0 +/// @param p1 Control point of curve, usually not actually reached at any value of t +/// @param p2 End point of curve, reached with t=1.0 +/// @param[out] pAngle If defined, receives the angle of the curve at `t` in degrees +ptTy Bezier (double t, const ptTy& p0, const ptTy& p1, const ptTy& p2, + double* pAngle = nullptr); + +/// @brief Calculate a point on a cubic Bezier curve +/// @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves +/// @param t Range [0..1] defines which point on the curve to be returned, 0 = p0, 1 = p3 +/// @param p0 Start point of curve, reached with t=0.0 +/// @param p1 1st control point of curve, usually not actually reached at any value of t +/// @param p2 2nd control point of curve, usually not actually reached at any value of t +/// @param p3 End point of curve, reached with t=1.0 +/// @param[out] pAngle If defined, receives the angle of the curve at `t` in degrees +ptTy Bezier (double t, const ptTy& p0, const ptTy& p1, const ptTy& p2, const ptTy& p3, + double* pAngle = nullptr); + +// +// MARK: Global enums +// + +/// Flight phase +enum flightPhaseE : unsigned char { + FPH_UNKNOWN = 0, ///< used for initializations + FPH_TAXI = 10, ///< Taxiing + FPH_TAKE_OFF = 20, ///< Group of status for take-off: + FPH_TO_ROLL, ///< Take-off roll + FPH_ROTATE, ///< Rotating + FPH_LIFT_OFF, ///< Lift-off, until "gear-up" height + FPH_INITIAL_CLIMB, ///< Initial climb, until "flaps-up" height + FPH_CLIMB = 30, ///< Regular climbout + FPH_CRUISE = 40, ///< Cruising, no altitude change + FPH_DESCEND = 50, ///< Descend, more then 100ft/min descend + FPH_APPROACH = 60, ///< Approach, below "flaps-down" height + FPH_FINAL, ///< Final, below "gear-down" height + FPH_LANDING = 70, ///< Group of status for landing: + FPH_FLARE, ///< Flare, when reaching "flare " height + FPH_TOUCH_DOWN, ///< The one cycle when plane touches down, don't rely on catching it...it's really one cycle only + FPH_ROLL_OUT, ///< Roll-out after touch-down until reaching taxi speed or stopping + FPH_STOPPED_ON_RWY ///< Stopped on runway because ran out of tracking data, plane will disappear soon +}; + +/// Is this a flight phase requiring a runway? +inline bool isRwyPhase (flightPhaseE fph) +{ return fph == FPH_TAKE_OFF || fph == FPH_TO_ROLL || fph == FPH_ROTATE || + fph == FPH_TOUCH_DOWN || fph == FPH_ROLL_OUT; } + +/// Ground status +enum onGrndE : unsigned char { + GND_UNKNOWN=0, ///< ground status yet unknown + GND_OFF, ///< off the ground, airborne + GND_ON ///< on the ground +}; + +/// Coordinates are in which kind of coordinate system? +enum coordUnitE : unsigned char { + UNIT_WORLD=0, ///< world coordinates (latitude, longitude, altitude) + UNIT_LOCAL ///< local GL coordinates (x, y, z) +}; + +/// Angles are in degree or radians? +enum angleUnitE : unsigned char { + UNIT_DEG=0, ///< angles are in degree + UNIT_RAD ///< angles are in radians +}; + +/// Position is on taxiway, runway, startup location? +enum specialPosE : unsigned char { + SPOS_NONE=0, ///< no special position + SPOS_STARTUP, ///< at startup location (gate, ramp, tie-down...) + SPOS_TAXI, ///< snapped to taxiway + SPOS_RWY, ///< snapped to runway +}; + +/// Return a 3 char-string for the special position enums +inline const char* SpecialPosE2String (specialPosE sp) +{ + return + sp == SPOS_STARTUP ? "SUP" : + sp == SPOS_TAXI ? "TXI" : + sp == SPOS_RWY ? "RWY" : " "; +} + + // //MARK: Data Structures // @@ -177,6 +314,9 @@ struct vectorTy { vectorTy () : angle(NAN), dist(NAN), vsi(NAN), speed(NAN) {} vectorTy ( double dAngle, double dDist, double dVsi=NAN, double dSpeed=NAN ) : angle(dAngle), dist(dDist), vsi(dVsi), speed(dSpeed) {} + + /// Valid vector, ie. at least angle and distance defined? + bool isValid () const { return !std::isnan(angle) && !std::isnan(dist); } // standard string for any output purposes operator std::string() const; @@ -186,38 +326,51 @@ struct vectorTy { inline double vsi_ft () const { return vsi / Ms_per_FTm; } }; +constexpr size_t EDGE_UNKNOWN = ULONG_MAX; ///< position's taxiway edge is unknown, not even tried to find one +constexpr size_t EDGE_UNAVAIL = EDGE_UNKNOWN-1; ///< tried finding a taxiway, but was unsuccessful + // a position: latitude (Z), longitude (X), altitude (Y), timestamp struct positionTy { enum positionTyE { LAT=0, LON, ALT, TS, HEADING, PITCH, ROLL }; std::valarray v; - int mergeCount; // for posList use only: when merging positions this counts how many flight data objects made up this position - - enum onGrndE { GND_UNKNOWN=0, GND_OFF, GND_ON } onGrnd; - enum coordUnitE { UNIT_WORLD, UNIT_LOCAL } unitCoord; - enum angleUnitE { UNIT_DEG, UNIT_RAD } unitAngle; + int mergeCount = 1; /// for posList use only: when merging positions this counts how many flight data objects made up this position + + /// collection of defining flags + struct posFlagsTy { + flightPhaseE flightPhase : 7; ///< start of some special flight phase? + bool bHeadFixed : 1; ///< heading fixed, not to be recalculated? + onGrndE onGrnd : 2; ///< on ground or not or not known? + coordUnitE unitCoord : 1; ///< world or local coordinates? + angleUnitE unitAngle : 1; ///< heading in degree or radians? + specialPosE specialPos : 2; ///< position is somehow special` + bool bCutCorner : 1; ///< is this an (inserted) position, that can be cut short? (-> use quadratic Bezier instead of cubic) + } f; - // start of some special flight phase like rotate, take off, touch down? - // (can't use LTAircraft::FlightPhase due to cyclic header inclusion) - int flightPhase = 0; + /// The taxiway network's edge this pos is on, index into Apt::vecTaxiEdges + size_t edgeIdx = EDGE_UNKNOWN; public: - positionTy () : v{NAN,NAN,NAN,NAN,NAN,NAN,NAN}, mergeCount(1), - onGrnd(GND_UNKNOWN), unitCoord(UNIT_WORLD), unitAngle(UNIT_DEG) {} + positionTy () : v{NAN,NAN,NAN,NAN,NAN,NAN,NAN} + { *(uint16_t*)&f = 0; } positionTy (double dLat, double dLon, double dAlt_m=NAN, double dTS=NAN, double dHead=NAN, double dPitch=NAN, double dRoll=NAN, onGrndE grnd=GND_UNKNOWN, coordUnitE uCoord=UNIT_WORLD, angleUnitE uAngle=UNIT_DEG, - int fPhase = 0) : - v{dLat, dLon, dAlt_m, dTS, dHead, dPitch, dRoll}, mergeCount(1), - onGrnd(grnd), unitCoord(uCoord), unitAngle(uAngle), flightPhase(fPhase) {} + flightPhaseE fPhase = FPH_UNKNOWN) : + v{dLat, dLon, dAlt_m, dTS, dHead, dPitch, dRoll} + { *(uint16_t*)&f = 0; f.onGrnd=grnd; f.unitCoord=uCoord; f.unitAngle=uAngle; f.flightPhase=fPhase; } positionTy(const XPMPPlanePosition_t& x) : positionTy (x.lat, x.lon, x.elevation * M_per_FT, NAN, x.heading, x.pitch, x.roll) {} positionTy ( const XPLMProbeInfo_t& probe ) : - positionTy ( probe.locationZ, probe.locationX, probe.locationY ) { unitCoord=UNIT_LOCAL; } + positionTy ( probe.locationZ, probe.locationX, probe.locationY ) { f.unitCoord=UNIT_LOCAL; } + positionTy ( const ptTy& _pt) : + positionTy ( _pt.y, _pt.x ) {} // merge with the given position positionTy& operator |= (const positionTy& pos); + // typecase to ptTy + operator ptTy() const { return ptTy(lon(),lat()); } // typecast to what XPMP API needs operator XPMPPlanePosition_t() const; // standard string for any output purposes @@ -242,6 +395,13 @@ struct positionTy { bool isNormal (bool bAllowNanAltIfGnd = false) const; // is fully valid? (isNormal + heading, pitch, roll)? bool isFullyValid() const; + /// Has a valid edge in the taxiway network of some airport? + bool HasTaxiEdge () const { return edgeIdx < EDGE_UNAVAIL; } + /// Has position been post-processed by some optimization (like snap to taxiway)? + bool IsPostProcessed () const { return + f.bHeadFixed || f.bCutCorner || f.specialPos != SPOS_NONE || + f.flightPhase != FPH_UNKNOWN || edgeIdx != EDGE_UNKNOWN; + } // rad/deg conversion (only affects lat and lon) positionTy deg2rad() const; @@ -259,7 +419,7 @@ struct positionTy { inline double pitch() const { return v[PITCH]; } inline double roll() const { return v[ROLL]; } - inline bool IsOnGnd() const { return onGrnd == GND_ON; } + inline bool IsOnGnd() const { return f.onGrnd == GND_ON; } inline double& lat() { return v[LAT]; } inline double& lon() { return v[LON]; } diff --git a/Include/DataRefs.h b/Include/DataRefs.h index a859af85..3ac35583 100644 --- a/Include/DataRefs.h +++ b/Include/DataRefs.h @@ -126,6 +126,7 @@ enum dataRefsXP { DR_LON_REF, // sim/flightmodel/position/lon_ref float n degrees The longitude of the point 0,0,0 in OpenGL coordinates. DR_VIEW_EXTERNAL, DR_VIEW_TYPE, + DR_MODERN_DRIVER, // sim/graphics/view/using_modern_driver: boolean: Vulkan/Metal in use? DR_WEATHER_BARO_SEA, // XP's weather DR_WEATHER_USE_REAL, DR_PLANE_LAT, // user's plane @@ -484,7 +485,7 @@ class DataRefs int maxFullNumAc = 50; // how many of these to draw in full (as opposed to 'lights only')? int fullDistance = 3; // nm: Farther away a/c is drawn 'lights only' int fdStdDistance = 15; // nm: miles to look for a/c around myself - int fdSnapTaxiDist = 25; ///< [m]: Snapping to taxi routes in a max distance of this many meter (0 -> off) + int fdSnapTaxiDist = 15; ///< [m]: Snapping to taxi routes in a max distance of this many meter (0 -> off) int fdRefreshIntvl = 20; // how often to fetch new flight data int fdBufPeriod = 90; // seconds to buffer before simulating aircraft int acOutdatedIntvl = 50; // a/c considered outdated if latest flight data more older than this compare to 'now' @@ -547,6 +548,7 @@ class DataRefs bool DidLocalRefPointChange (); ///< Did the reference point to the local coordinate system change since last call to this function? inline bool IsViewExternal() const { return XPLMGetDatai(adrXP[DR_VIEW_EXTERNAL]) != 0; } inline XPViewTypes GetViewType () const { return (XPViewTypes)XPLMGetDatai(adrXP[DR_VIEW_TYPE]); } + inline bool UsingModernDriver () const { return adrXP[DR_MODERN_DRIVER] ? XPLMGetDatai(adrXP[DR_MODERN_DRIVER]) != 0 : false; } inline bool IsVREnabled() const { return #ifdef DEBUG bSimVREntered ? true : // simulate some aspects of VR @@ -632,6 +634,7 @@ class DataRefs inline int GetLabelColor() const { return labelColor; } void GetLabelColor (float outColor[4]) const; inline int GetMaxNumAc() const { return maxNumAc; } + void SetMaxNumAc(int n) { maxNumAc = n; } inline int GetMaxFullNumAc() const { return maxFullNumAc; } inline int GetFullDistance_nm() const { return fullDistance; } inline int GetFdStdDistance_nm() const { return fdStdDistance; } diff --git a/Include/LTADSBEx.h b/Include/LTADSBEx.h index 286d3622..4e9c9cdb 100644 --- a/Include/LTADSBEx.h +++ b/Include/LTADSBEx.h @@ -126,9 +126,9 @@ class ADSBExchangeConnection : public LTOnlineChannel, LTFlightDataChannel virtual LTChannelType GetChType() const { return CHT_TRACKING_DATA; } virtual const char* ChName() const { return ADSBEX_NAME; } virtual bool FetchAllData(const positionTy& pos) { return LTOnlineChannel::FetchAllData(pos); } - // shall data of this channel be subject to LTFlightData::DataSmoothing? - virtual bool DoDataSmoothing (double& gndRange, double& airbRange) const - { gndRange = ADSBEX_SMOOTH_GROUND; airbRange = ADSBEX_SMOOTH_AIRBORNE; return true; } +// // shall data of this channel be subject to LTFlightData::DataSmoothing? +// virtual bool DoDataSmoothing (double& gndRange, double& airbRange) const +// { gndRange = ADSBEX_SMOOTH_GROUND; airbRange = ADSBEX_SMOOTH_AIRBORNE; return true; } protected: // need to add/cleanup API key diff --git a/Include/LTAircraft.h b/Include/LTAircraft.h index 150d8b76..6147ea82 100644 --- a/Include/LTAircraft.h +++ b/Include/LTAircraft.h @@ -115,7 +115,7 @@ struct AccelParam // get current value double m_s() const { return currSpeed_m_s; } double kt() const { return currSpeed_kt; } - bool isZero() const { return currSpeed_m_s <= 0; } + bool isZero() const { return currSpeed_m_s <= 0.01; } // start an acceleration now void StartAccel(double startSpeed, double targetSpeed, double accel, @@ -136,6 +136,101 @@ struct AccelParam inline double getTargetDeltaDist() const { return targetDeltaDist; } }; +/// @brief Handles a quadratic Bezier curve based on flight data positions +/// @details Only using quadratic curves because in higher-level Bezier curves the parameter `t` +/// does no longer correspond well to distance and planes would appear slowing down +/// at beginning and end. +/// @details The constructors take positions from flight data, +/// the necessary end and control points of a Bezier Curve +/// are computed from that input. +/// @see https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Constructing_B%C3%A9zier_curves +struct BezierCurve +{ +protected: + positionTy start; ///< start point of the actual Bezier curve + positionTy end; ///< end point of the actual Bezier curve + ptTy ptCtrl; ///< Control point of the curve + double startF = NAN; ///< at which factor f (from LTAircraft::CalcPPos()) does Bezier execution begin? + double endF = NAN; ///< at which factor f (from LTAircraft::CalcPPos()) does Bezier execution end? + double fCalAdd = NAN; ///< summand for `f` calibration + double fCalDiv = NAN; ///< divisor for `f` calibration + double myF = NAN; ///< last used `f` after calibration + double mySecondMaxF = NAN; ///< in case of a cut-corner curve: maximum f value on second half +public: + BezierCurve () {} ///< Standard constructor does nothing + + /// @brief Define a quadratic Bezier Curve based on the given flight data positions + /// @param _nowF Current factor f as earliest possible starting factor + /// @param _from From position on the leg towards _mid + /// @param _mid Mid position, current leg's end and next leg's starting point, the turning point, used as Bezier control point, ie. will not be reached + /// @param _to End position of next leg + /// @param _angleFromMid Angle from `_from` to `_mid` to avoid recalculation of already known data + /// @param _angleMidTo Angle from `_mid` to `_to` to avoid recalculation of already known data + /// @param _fullTurnTime Seconds allowed for a 360° turn (taken from LTAircraft::FlightModel) + void Define (double _nowF, + const positionTy& _from, + double _angleFromMid, + const positionTy& _mid, + double _angleMidTo, + const positionTy& _to, + double _fullTurnTime); + + /// @brief Define a quadratic Bezier Curve based on the given flight data positions + /// @param _initF The initial `f` factor when starting this Bezier for internal calibration of seemless calculations + /// @param _from Start position of current leg + /// @param _to End position of current leg + /// @param _vec Vector from `_from` to `_to` to avoid recalculation of already known data + /// @details The control point is computed as the point where the lines through _from and _to, + /// having angle as defined by `.heading()`, meet. This can and cannot work out. + /// @return Has a valid Bezier curve been defined? + bool Define (double _initF, + const positionTy& _from, + const positionTy& _to, + const vectorTy& _vec); + + /// Convert the geographic coordinates to meters, with `start` being the origin (0|0) point + /// This is needed for accurate angle calculations + void ConvertToMeter (); + /// Convert the given geographic coordinates to meters + void ConvertToMeter (ptTy& pt) const; + + /// Convert the given position back to geographic coordinates + void ConvertToGeographic (ptTy& pt) const; + + /// Calibrate the Bezier so that `f = [_inifF.._maxF]` maps to `myF = [_myInit.._myMax]` + void Calibrate (double _initF, double _maxF, + double _myInit, double _myMax); + /// Re-Calibrate for the second half of a cut-corner curve + void ReCalibrate2ndHalf () { Calibrate(0.0, mySecondMaxF, 0.5, 1.0); } + + /// Clear the definition, so that BezierCurve::isDefined() will return `false` + void Clear (); + /// Is a curve defined? + bool isDefined () const { return ptCtrl.isValid(); } + /// Is Bezier active for given timestamp? + bool isActive (double _f) const + { return isDefined() && startF <= _f && _f <= endF; } + /// Is or will Bezier be active for given factor f? + bool isActiveFuture (double _f) const + { return isDefined() && _f <= endF; } + /// is defined and the given timestamp between start's and end's timestamp? + bool isTsInbetween (double _ts) const + { return isDefined() && start.ts() <= _ts && _ts <= end.ts(); } + /// is defined and the given timestamp before end's timestamp? + bool isTsBeforeEnd (double _ts) const + { return isDefined() && _ts <= end.ts(); } + + /// Return the position as per given timestamp, if the timestamp is between `start` and `end` + /// @param[in,out] pos Current position, to be overwritten with new position + /// @param ts Timestamp for the position we look for, only used for validation if Bezier is active + /// @param f Factor in range [0..1]: This controls the returned value (might be controled by acceleration!) + /// @return if the position was adjusted + bool GetPos (positionTy& pos, double ts, double f); + + /// Debug text output + std::string dbgTxt() const; +}; + // //MARK: LTAircraft // Represents an aircraft as displayed in XP by use of the @@ -197,27 +292,7 @@ class LTAircraft : XPCAircraft }; public: - /// @brief Flight phase - enum FlightPhase { - FPH_UNKNOWN = 0, ///< used for initializations - FPH_TAXI = 10, ///< Taxiing - FPH_TAKE_OFF = 20, ///< Group of status for take-off: - FPH_TO_ROLL, ///< Take-off roll - FPH_ROTATE, ///< Rotating - FPH_LIFT_OFF, ///< Lift-off, until "gear-up" height - FPH_INITIAL_CLIMB, ///< Initial climb, until "flaps-up" height - FPH_CLIMB = 30, ///< Regular climbout - FPH_CRUISE = 40, ///< Cruising, no altitude change - FPH_DESCEND = 50, ///< Descend, more then 100ft/min descend - FPH_APPROACH = 60, ///< Approach, below "flaps-down" height - FPH_FINAL, ///< Final, below "gear-down" height - FPH_LANDING = 70, ///< Group of status for landing: - FPH_FLARE, ///< Flare, when reaching "flare " height - FPH_TOUCH_DOWN, ///< The one cycle when plane touches down, don't rely on catching it...it's really one cycle only - FPH_ROLL_OUT, ///< Roll-out after touch-down until reaching taxi speed or stopping - FPH_STOPPED_ON_RWY ///< Stopped on runway because ran out of tracking data, plane will disappear soon - }; - static std::string FlightPhase2String (FlightPhase phase); + static std::string FlightPhase2String (flightPhaseE phase); public: // reference to the defining flight data @@ -246,17 +321,17 @@ class LTAircraft : XPCAircraft double tsLastCalcRequested; // dynamic parameters of the plane - FlightPhase phase; // current flight phase + flightPhaseE phase; // current flight phase double rotateTs; // when to rotate? double vsi; // vertical speed (ft/m) bool bOnGrnd; // are we touching ground? bool bArtificalPos; // running on artifical positions for roll-out? - bool bNeedNextVec; // in need of next vector after to-pos? + bool bNeedSpeed = false; ///< need speed calculation? + bool bNeedCCBezier = false; ///< need Bezier calculation due to cut-corner case? AccelParam speed; // current speed [m/s] and acceleration control + BezierCurve turn; ///< position, heading, roll while flying a turn MovingParam gear; MovingParam flaps; - MovingParam heading; // used when turning - MovingParam roll; MovingParam pitch; MovingParam reversers; ///< reverser open ratio MovingParam spoilers; ///< spoiler extension ratio @@ -264,9 +339,8 @@ class LTAircraft : XPCAircraft MovingParam gearDeflection; ///< main gear deflection in meters during touch-down // Y-Probe - XPLMProbeRef probeRef; double probeNextTs; // timestamp of NEXT probe - double terrainAlt; // in feet + double terrainAlt_m; ///< terrain altitude in meters // bearing/dist from viewpoint to a/c vectorTy vecView; // degrees/meters @@ -306,9 +380,10 @@ class LTAircraft : XPCAircraft // have no more viable positions left, in need of more? bool OutOfPositions() const; // current a/c configuration - inline FlightPhase GetFlightPhase() const { return phase; } + inline flightPhaseE GetFlightPhase() const { return phase; } std::string GetFlightPhaseString() const { return FlightPhase2String(phase); } inline bool IsOnGrnd() const { return bOnGrnd; } + bool IsOnRwy() const; ///< is the aircraft on a rwy (on ground and at least on pos on rwy) inline double GetHeading() const { return ppos.heading(); } inline double GetTrack() const { return vec.angle; } inline double GetFlapsPos() const { return flaps.is(); } @@ -322,10 +397,10 @@ class LTAircraft : XPCAircraft inline double GetRoll() const { return ppos.roll(); } inline double GetAlt_ft() const { return ppos.alt_ft(); } inline double GetAlt_m() const { return ppos.alt_m(); } - inline double GetTerrainAlt_ft() const { return terrainAlt; } // ft - inline double GetTerrainAlt_m() const { return terrainAlt * M_per_FT; } // m - inline double GetPHeight_ft() const { return ppos.alt_ft() - terrainAlt; } - inline double GetPHeight_m() const { return GetPHeight_ft() * M_per_FT; } + inline double GetTerrainAlt_ft() const { return terrainAlt_m / M_per_FT; } ///< terrain alt converted to ft + inline double GetTerrainAlt_m() const { return terrainAlt_m; } ///< terrain alt in meter + inline double GetPHeight_m() const { return ppos.alt_m() - terrainAlt_m; } ///< height above ground in meter + inline double GetPHeight_ft() const { return GetPHeight_m() / M_per_FT; } ///< height above ground converted to ft inline vectorTy GetVec() const { return vec; } inline vectorTy GetVecView() const { return vecView; } std::string GetLightsStr() const; @@ -353,6 +428,8 @@ class LTAircraft : XPCAircraft bool CalcPPos (); // determine other parameters like gear, flap, roll etc. based on flight model assumptions void CalcFlightModel (const positionTy& from, const positionTy& to); + /// determine roll, based on a previous and a current heading + void CalcRoll (double _prevTs, double _prevHeading); bool YProbe (); // determines if now visible bool CalcVisible (); diff --git a/Include/LTApt.h b/Include/LTApt.h index 0824ea95..415b8af5 100644 --- a/Include/LTApt.h +++ b/Include/LTApt.h @@ -30,23 +30,43 @@ bool LTAptEnable (); /// Update the airport data with airports around current camera position void LTAptRefresh (); -/// @brief Update local coordinate system's values due to ref point change -/// @param bForce `true` Recalculate all values, `false` calculate only missing values -void LTAptLocalCoordsUpdate (bool bForce); - /// @brief Return the best possible runway to auto-land at +/// @param _mdl flight model definitions to use for calculations +/// @param _from Start pos/heading for seach +/// @param _speed_m_s Speed during approach +/// @param _logTxt (optional) Shall decision be logged? If yes, with which id text? +/// @return Position of matching runway touch-down point, incl. timestamp and heading (of runway); positionTy() if nothing found +/// @note Call from separate thread, like from CalcNextPos +positionTy LTAptFindRwy (const LTAircraft::FlightModel& _mdl, + const positionTy& _from, + double _speed_m_s, + const std::string& _logTxt = ""); + +/// @brief Return the best possible runway to auto-land at based on current pos/heading/speed of `_ac` /// @param _ac Aircraft in search for a landing spot. It's last go-to position and VSI as well as its model are of importance /// @return Position of matching runway touch-down point, incl. timestamp and heading (of runway) /// @note Call from separate thread, like from CalcNextPos -positionTy LTAptFindRwy (const LTAircraft& _ac); +inline positionTy LTAptFindRwy (const LTAircraft& _ac, bool bDoLogging = false) +{ + return LTAptFindRwy (_ac.mdl, _ac.GetToPos(), _ac.GetSpeed_m_s(), + bDoLogging ? std::string(_ac) : ""); +} /// @brief Snaps the passed-in position to the nearest rwy or taxiway if appropriate -/// @param pos Reference to the position, which might get changed. -/// @param bLogging Do logging via LOG_MSG? +/// @param fd Flight data object to be analyzed +/// @param[in,out] posIter Iterator into LTFlightData::posDeque, points to position to analyze, might change due to inserted taxi positions +/// @param bInsertTaxiTurns Shall additional taxi turning points be added into LTFlightData::posDeque? /// @return Changed the position? -bool LTAptSnap (positionTy& pos, bool bLogging); +bool LTAptSnap (LTFlightData& fd, dequePositionTy::iterator& posIter, + bool bInsertTaxiTurns); /// Cleanup void LTAptDisable (); +#ifdef DEBUG +/// @brief Dumps the entire taxi network into a CSV file readable by GPS Visualizer +/// @see https://www.gpsvisualizer.com/ +void LTAptDump (const std::string& _aptId); +#endif + #endif /* LTApt_h */ diff --git a/Include/LTFlightData.h b/Include/LTFlightData.h index 5193108e..34f89ca7 100644 --- a/Include/LTFlightData.h +++ b/Include/LTFlightData.h @@ -49,6 +49,7 @@ enum transpTy { class LTAircraft; struct LTFlightDataList; +class Apt; class LTFlightData { @@ -222,6 +223,7 @@ class LTFlightData dequeFDDynDataTy dynDataDeque; double rotateTS; double youngestTS; + positionTy posRwy; ///< determined rwy (likely) to land on // STATIC DATA (protected, access will be mutex-controlled for thread-safety) FDStaticData statData; @@ -333,8 +335,11 @@ class LTFlightData bool TryDeriveGrndStatus (positionTy& pos); // determine terrain alt at pos double YProbe_at_m (const positionTy& pos); - // returns vector at timestamp (which has speed, direction and the like) - tryResult TryGetVec (double ts, vectorTy& vec) const; + /// returns next position in posDeque with timestamp after ts + /// @param ts Need a position with a timestamp larger than this + /// @param[out] pos Receives the position if found + /// @return Indicates if the call was successful + tryResult TryGetNextPos (double ts, positionTy& pos) const; // stringify all position information - mainly for debugging purposes std::string Positions2String () const; @@ -363,6 +368,8 @@ class LTFlightData // access/create/destroy aircraft bool AircraftMaintenance ( double simTime ); // returns: delete me? + bool DetermineAcModel (); ///< try interpreting model text or check for ground vehicle, last resort: default a/c type + bool AcSlotAvailable (double simTime); ///< checks if there is a slot available to create this a/c, tries to remove the farest a/c if too many a/c rendered bool CreateAircraft ( double simTime ); void DestroyAircraft (); LTAircraft* GetAircraft () const { return pAc; } @@ -370,7 +377,13 @@ class LTFlightData // actions on all flight data / treating mapFd as lists static void UpdateAllModels (); static const LTFlightData* FindFocusAc (const double bearing); +#ifdef DEBUG + static void RemoveAllAcButSelected (); +#endif friend LTFlightDataList; + + // LTApt inserts positions during the "snap-to-taxiway" precedure + friend Apt; }; // global map of flight data, keyed by transpIcao diff --git a/Include/LTOpenSky.h b/Include/LTOpenSky.h index 888c1380..70eba81a 100644 --- a/Include/LTOpenSky.h +++ b/Include/LTOpenSky.h @@ -64,9 +64,9 @@ class OpenSkyConnection : public LTOnlineChannel, LTFlightDataChannel virtual LTChannelType GetChType() const { return CHT_TRACKING_DATA; } virtual const char* ChName() const { return OPSKY_NAME; } virtual bool FetchAllData(const positionTy& pos) { return LTOnlineChannel::FetchAllData(pos); } - // shall data of this channel be subject to LTFlightData::DataSmoothing? - virtual bool DoDataSmoothing (double& gndRange, double& airbRange) const - { gndRange = OPSKY_SMOOTH_GROUND; airbRange = OPSKY_SMOOTH_AIRBORNE; return true; } +// // shall data of this channel be subject to LTFlightData::DataSmoothing? +// virtual bool DoDataSmoothing (double& gndRange, double& airbRange) const +// { gndRange = OPSKY_SMOOTH_GROUND; airbRange = OPSKY_SMOOTH_AIRBORNE; return true; } }; //MARK: OpenSky Master Data Constats diff --git a/Include/LTRealTraffic.h b/Include/LTRealTraffic.h index b9d24dd3..769be24f 100644 --- a/Include/LTRealTraffic.h +++ b/Include/LTRealTraffic.h @@ -39,6 +39,7 @@ constexpr size_t RT_NET_BUF_SIZE = 512; constexpr double RT_SMOOTH_AIRBORNE = 65.0; // smooth 65s of airborne data constexpr double RT_SMOOTH_GROUND = 35.0; // smooth 35s of ground data +constexpr double RT_VSI_AIRBORNE = 80.0; ///< if VSI is more than this then we assume "airborne" #define MSG_RT_STATUS "RealTraffic network status changed to: %s" #define MSG_RT_WEATHER_IS "RealTraffic weather: %s reports %ld hPa and '%s'" @@ -158,9 +159,9 @@ class RealTrafficConnection : public LTOnlineChannel, LTFlightDataChannel virtual void Close (); // SetValid also sets internal status virtual void SetValid (bool _valid, bool bMsg = true); - // shall data of this channel be subject to LTFlightData::DataSmoothing? - virtual bool DoDataSmoothing (double& gndRange, double& airbRange) const - { gndRange = RT_SMOOTH_GROUND; airbRange = RT_SMOOTH_AIRBORNE; return true; } +// // shall data of this channel be subject to LTFlightData::DataSmoothing? +// virtual bool DoDataSmoothing (double& gndRange, double& airbRange) const +// { gndRange = RT_SMOOTH_GROUND; airbRange = RT_SMOOTH_AIRBORNE; return true; } // shall data of this channel be subject to hovering flight detection? virtual bool DoHoverDetection () const { return true; } diff --git a/Include/LiveTraffic.h b/Include/LiveTraffic.h index 639f98a2..bb7b3256 100644 --- a/Include/LiveTraffic.h +++ b/Include/LiveTraffic.h @@ -61,8 +61,10 @@ #endif // C++ +#include #include #include +#include #include #include #include @@ -138,7 +140,6 @@ void LTMainStop (); void MenuUpdateAllItemStatus(); void HandleNewVersionAvail (); -void HandleRefPointChanged (); ///< Handles that the local coordinate's reference point has changed #ifdef DEBUG void LTErrorCB (const char* msg); @@ -230,6 +231,23 @@ bool begins_with(const TContainer& input, const TContainer& match) && std::equal(match.cbegin(), match.cend(), input.cbegin()); } +/// Clamps `v` between `lo` and `hi`: `lo` if `v` < `lo`, `hi` if `hi` < `v`, otherwise `v` +/// @see C++17, https://en.cppreference.com/w/cpp/algorithm/clamp +template +constexpr const T& clamp( const T& v, const T& lo, const T& hi ) +{ + assert( !(hi < lo) ); + return (v < lo) ? lo : (hi < v) ? hi : v; +} + +template +constexpr bool between( const T& v, const T& lo, const T& hi ) +{ + assert( !(hi < lo) ); + return (lo <= v) && (v <= hi); +} + + // comparing 2 doubles for near-equality bool dequal ( const double d1, const double d2 ); diff --git a/Include/TextIO.h b/Include/TextIO.h index 01afec65..2187f687 100644 --- a/Include/TextIO.h +++ b/Include/TextIO.h @@ -94,6 +94,9 @@ throw LTErrorFD(fdref,__FILE__, __LINE__, __func__, lvl, __VA_ARGS__); // fTimeToDisplay == 0 -> no limit XPLMWindowID CreateMsgWindow(float fTimeToDisplay, logLevelTy lvl, const char* szMsg, ...); +/// Show the special text "Seeing aircraft...showing..." +XPLMWindowID CreateMsgWindow(float fTimeToDisplay, int numSee, int numShow, int bufTime); + // Destroys the windows (if still active) void DestroyWindow(); diff --git a/Lib/xplanemp/xplanemp.framework/Versions/A/Resources/Info.plist b/Lib/xplanemp/xplanemp.framework/Versions/A/Resources/Info.plist index b6bd14a9..57fd0e94 100644 --- a/Lib/xplanemp/xplanemp.framework/Versions/A/Resources/Info.plist +++ b/Lib/xplanemp/xplanemp.framework/Versions/A/Resources/Info.plist @@ -3,7 +3,7 @@ BuildMachineOSBuild - 19C57 + 19D76 CFBundleDevelopmentRegion en CFBundleExecutable @@ -27,17 +27,17 @@ DTCompiler com.apple.compilers.llvm.clang.1_0 DTPlatformBuild - 11C29 + 11E146 DTPlatformVersion GM DTSDKBuild - 19B90 + 19E258 DTSDKName macosx10.15 DTXcode - 1130 + 1140 DTXcodeBuild - 11C29 + 11E146 LSMinimumSystemVersion 10.9 NSHumanReadableCopyright diff --git a/Lib/xplanemp/xplanemp.framework/Versions/A/xplanemp b/Lib/xplanemp/xplanemp.framework/Versions/A/xplanemp index 752021be..9465550e 100644 Binary files a/Lib/xplanemp/xplanemp.framework/Versions/A/xplanemp and b/Lib/xplanemp/xplanemp.framework/Versions/A/xplanemp differ diff --git a/Lib/xplanemp/xplanemp.lib b/Lib/xplanemp/xplanemp.lib index 2f034dd4..53a9776e 100644 Binary files a/Lib/xplanemp/xplanemp.lib and b/Lib/xplanemp/xplanemp.lib differ diff --git a/Lib/xplanemp/xplanemp.pdb b/Lib/xplanemp/xplanemp.pdb new file mode 100644 index 00000000..16af7069 Binary files /dev/null and b/Lib/xplanemp/xplanemp.pdb differ diff --git a/LiveTraffic.aps b/LiveTraffic.aps index cd6e11fc..06675356 100644 Binary files a/LiveTraffic.aps and b/LiveTraffic.aps differ diff --git a/LiveTraffic.rc b/LiveTraffic.rc index 21fda765..30151c94 100644 Binary files a/LiveTraffic.rc and b/LiveTraffic.rc differ diff --git a/LiveTraffic.xcodeproj/project.pbxproj b/LiveTraffic.xcodeproj/project.pbxproj index a14c01dd..3046da82 100755 --- a/LiveTraffic.xcodeproj/project.pbxproj +++ b/LiveTraffic.xcodeproj/project.pbxproj @@ -163,6 +163,7 @@ 25A0960B2203B0C500658AA8 /* XPStandardWidgets.pas */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.pascal; path = XPStandardWidgets.pas; sourceTree = ""; }; 25A0960C2203B0C500658AA8 /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; 25A0960D2203B0C500658AA8 /* license.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = license.txt; sourceTree = ""; }; + 25A70295241D911200D2E7EE /* Apt.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Apt.md; sourceTree = ""; }; 25AACA2D217A82FF0035DC20 /* Acceleration.psd */ = {isa = PBXFileReference; lastKnownFileType = file; path = Acceleration.psd; sourceTree = ""; }; 25AACA2F217A99ED0035DC20 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; wrapsLines = 1; }; 25AACA30217B2FCA0035DC20 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -244,6 +245,7 @@ 254EA46D20834D5E008A312F /* docs */ = { isa = PBXGroup; children = ( + 25A70295241D911200D2E7EE /* Apt.md */, 25AACA2D217A82FF0035DC20 /* Acceleration.psd */, 25D65429210C630800E65D77 /* BADA_Aircraft_Performance_Summary_Tables.pdf */, 25D6542A210C630800E65D77 /* Calc.xlsx */, @@ -387,7 +389,6 @@ 25A095BD2203B01300658AA8 /* Include */ = { isa = PBXGroup; children = ( - 25624BDB23B0150300B899E1 /* LTApt.h */, 25A095C62203B01300658AA8 /* ACInfoWnd.h */, 25A095C52203B01300658AA8 /* Constants.h */, 25A095C02203B01300658AA8 /* CoordCalc.h */, @@ -395,6 +396,7 @@ 25A095C82203B01300658AA8 /* LiveTraffic.h */, 2573631D22233CCA005210C5 /* LTADSBEx.h */, 25A095C92203B01300658AA8 /* LTAircraft.h */, + 25624BDB23B0150300B899E1 /* LTApt.h */, 25A095C22203B01300658AA8 /* LTChannel.h */, 25A095BF2203B01300658AA8 /* LTFlightData.h */, 25FEB7BA224D7C8C002A051F /* LTForeFlight.h */, @@ -721,6 +723,7 @@ CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; @@ -804,6 +807,7 @@ CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; diff --git a/LiveTraffic.xcodeproj/project.xcworkspace/xcuserdata/birger.xcuserdatad/xcdebugger/Expressions.xcexplist b/LiveTraffic.xcodeproj/project.xcworkspace/xcuserdata/birger.xcuserdatad/xcdebugger/Expressions.xcexplist index 9cea3720..a9b89ac0 100644 --- a/LiveTraffic.xcodeproj/project.xcworkspace/xcuserdata/birger.xcuserdatad/xcdebugger/Expressions.xcexplist +++ b/LiveTraffic.xcodeproj/project.xcworkspace/xcuserdata/birger.xcuserdatad/xcdebugger/Expressions.xcexplist @@ -3,33 +3,92 @@ version = "1.0"> + contextName = "LTFlightData::AddNewPos(positionTy&):LTFlightData.cpp"> + + + + + + + contextName = "str_tokenize(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, bool):LTMain.cpp"> + + + value = "gear"> + + + contextName = "ADSBExchangeConnection::ProcessFetchedData(std::__1::map<LTFlightData::FDKeyTy, LTFlightData, std::__1::less<LTFlightData::FDKeyTy>, std::__1::allocator<std::__1::pair<LTFlightData::FDKeyTy const, LTFlightData> > >&):LTADSBEx.cpp"> + + + + + value = "errno"> + contextName = "Apt::FindEdgesForHeading(double, double, std::__1::vector<unsigned long, std::__1::allocator<unsigned long> >&, TaxiEdge::edgeTy) const:LTApt.cpp"> + value = "vecTaxiEdges[222]"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -42,32 +101,62 @@ + contextName = "ADSBExchangeHistorical::ProcessFetchedData(std::__1::map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, LTFlightData, std::__1::less<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, LTFlightData> > >&):LTChannel.cpp"> + + + + + contextName = "TaxiEdge::GetRwyEP_A(Apt const&) const:LTApt.cpp"> + contextName = "fm_processModelLine(char const*, int, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&, LTAircraft::FlightModel&):LTAircraft.cpp"> + + + + + contextName = "LTAircraft::CalcSpeed(positionTy const&, vectorTy const&):LTAircraft.cpp"> + + + value = "pAc"> + + + + + contextName = "LTFlightData::DataSmoothing(bool&):LTFlightData.cpp"> + value = "posStringBefore"> + contextName = "DataRefs::GetSimTime() const:DataRefs.cpp"> + + + + + + + + @@ -78,465 +167,586 @@ + contextName = "ADSBExchangeConnection::InitCurl():LTADSBEx.cpp"> + contextName = "LTACMasterdataChannel::ClearMasterDataRequests():LTChannel.cpp"> + value = "listAcStatUpdate"> - - + contextName = "Apt::SnapToTaxiway(LTFlightData&, std::__1::__deque_iterator<positionTy, positionTy*, positionTy&, positionTy**, long, 85l>&):LTApt.cpp"> + value = "prevA"> + value = "prevB"> + contextName = "LTFlightDataCreateNewAc():LTFlightData.cpp"> + contextName = "LTMainInit():LTMain.cpp"> + contextName = "obj_schedule_one_aircraft(CSLPlane_t*, double, double, double, double, double, double, int, xpmp_LightStatus, XPLMPlaneDrawState_t*):XPMPMultiplayerObj8.cpp"> + contextName = "OpenSkyAcMasterdata::ProcessFetchedData(std::__1::map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, LTFlightData, std::__1::less<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, LTFlightData> > >&):LTFlightData.cpp"> + + + + + contextName = "LTFlightDataRemoveOutdatedAc():LTFlightData.cpp"> + contextName = "XPMPDefaultPlaneRenderer(int):XPMPPlaneRenderer.cpp"> + value = "cull"> + + + + + contextName = "TaxiEdge::SetEndNode(unsigned long, double, double):LTApt.cpp"> + contextName = "TFMainWindowWidget::MessageCloseButtonPushed():TFWidgets.cpp"> + + + value = "(LTAircraft::FlightModel*)&pAc->mdl"> + contextName = "LTAircraft::CalcFlightModel():LTAircraft.cpp"> + + + + + + + contextName = "positionTy::rad2deg():CoordCalc.cpp"> + contextName = "LTAptSnap(LTFlightData&, std::__1::__deque_iterator<positionTy, positionTy*, positionTy&, positionTy**, long, 85l>&):LTApt.cpp"> + + + + + value = "tfc"> + contextName = "ADSBExchangeHistorical::FetchAllData(positionTy const&):LTChannel.cpp"> + + + + + value = "errno"> + contextName = "DataRefs::LTGetDebugAcFilter(void*, void*, int, int):DataRefs.cpp"> - - + value = "(char*)outValue"> + contextName = "DataRefs::GetSimTime():DataRefs.cpp"> + value = "tStartThisYear"> + value = "tStartPrevYear"> + + + + + + + value = "dataRefs"> + contextName = "LTFlightData::FDStaticData::operator|=(LTFlightData::FDStaticData const&):LTFlightData.cpp"> + contextName = "OpenSkyAcMasterdata::ProcessFetchedData(std::__1::map<LTFlightData::FDKeyTy, LTFlightData, std::__1::less<LTFlightData::FDKeyTy>, std::__1::allocator<std::__1::pair<LTFlightData::FDKeyTy const, LTFlightData> > >&):LTOpenSky.cpp"> + contextName = "LTFlightData::AppendNewPos():LTFlightData.cpp"> + contextName = "DataRefs::GetLocalDateDays():DataRefs.h"> + + + + + + + + + + + + + value = "me.netData+me.netDataPos-10"> + contextName = "Apt::FindClosestEdge(positionTy const&, positionTy&, int, double, TaxiEdge const*) const:LTApt.cpp"> + value = "to"> + value = "from"> + contextName = "LTOnlineChannel::FetchAllData(positionTy const&):LTFlightData.cpp"> + contextName = "std::__1::valarray<double>::valarray(std::__1::valarray<double> const&):valarray"> + contextName = "Doc8643::ReadDoc8643File():DataRefs.cpp"> + contextName = "GLOBAL"> + value = "currCycle.simTime"> + value = "dataRefs"> + + + + + value = "from"> + value = "ppos.v[6]"> + value = "to"> + + + + - - - - - - - - - - + contextName = "LTAircraft::CalcRoll(double, double):LTAircraft.cpp"> + value = "ppos.v[6]"> + contextName = "ptTy::mirrorAt(ptTy const&) const:CoordCalc.h"> + + + + + contextName = "LTFlightData::DataCleansing(bool&):LTFlightData.cpp"> + value = "pos.v[2]"> + + + + + contextName = "AccelParam::AccelParam(double, double, double, double):LTAircraft.cpp"> + contextName = "positionTy::deltaPos(vectorTy const&) const:CoordCalc.cpp"> + contextName = "Apt::SnapToTaxiway(LTFlightData&, std::__1::__deque_iterator<positionTy, positionTy*, positionTy&, positionTy**, long, 128l>&, bool):LTApt.cpp"> + + + + + contextName = "Jasmine::next(jsonToken&):LTFlightData.cpp"> + contextName = "positionListFindBefore(std::__1::list<positionTy, std::__1::allocator<positionTy> > const&, double):CoordCalc.cpp"> + contextName = "MovingParam::get():LTAircraft.cpp"> + contextName = "LTChannel::DecErrCnt():LTChannel.cpp"> + contextName = "Apt::SplitEdge(unsigned long, unsigned long):LTApt.cpp"> + contextName = "Apt::GetEdgeBetweenNodes(unsigned long, unsigned long):LTApt.cpp"> + contextName = "LTAircraft::LTAircraft(char const*, char const*, char const*, char const*):LTAircraft.cpp"> + value = "(XPMPPlanePtr)mPlane"> + + + contextName = "DataRefs::LTGetInt(void*):DataRefs.cpp"> + value = "mapFd"> + value = "sizeof(bool)"> + + + + + + - - + contextName = "LTFlightData::AircraftMaintenance(double):LTFlightData.cpp"> + value = "posDeque"> + contextName = "LTAircraft::GetPlanePosition(XPMPPlanePosition_t*):LTAircraft.cpp"> + value = "ppos"> + + + + - - - - + contextName = "UDPReceiver::recv(unsigned long):Network.cpp"> + value = "buf"> + contextName = "Apt::GetSimilarTaxiTmpPos(double, double, bool):LTApt.cpp"> + contextName = "DataRefs::LoadConfigFile()::$_0::operator()(DataRefs::dataRefDefinitionT const&) const:DataRefs.cpp"> - - - - + value = "sDataRef"> - - - - + contextName = "LTSettingsUI::SaveCSLPath(int):SettingsUI.cpp"> + contextName = "ReadOneAptFile(std::__1::basic_ifstream<char, std::__1::char_traits<char> >&, boundingBoxTy const&):LTApt.cpp"> + contextName = "BezierCurve::Define(double, positionTy const&, positionTy const&, vectorTy const&):LTAircraft.cpp"> + value = "end"> + contextName = "LTAircraft::YProbe():LTAircraft.cpp"> + value = "terrainAlt"> + + + + + contextName = "DataRefs::SaveCSLPath(int, DataRefs::CSLPathCfgTy):DataRefs.cpp"> + value = "vCSLPaths"> + contextName = "CreateMsgWindow(float, char const*, ...):TextIO.cpp"> + + + + + value = "dataRefs.bChannel"> + contextName = "TaxiNode::operator!() const:LTApt.cpp"> + contextName = "Apt::AddRwyEnds(double, double, double, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, double, double, double, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&):LTApt.cpp"> + + + + + + + + + value = "statData"> + contextName = "CSL_DrawObject(XPMPPlane_t*, float, double, double, double, double, double, double, int, int, xpmp_LightStatus, XPLMPlaneDrawState_t*):XPMPMultiplayerCSL.cpp"> - - - - - - - + contextName = "Apt::AddTaxiTmpPath(TaxiTmpPath&&):LTApt.cpp"> + + + + value = "widgetIds[0]"> + contextName = "LTFlightData::Positions2String() const:LTFlightData.cpp"> + value = "dynData.rcvr"> + contextName = "Apt::FindEdgesForHeading(double, double, std::__1::list<TaxiEdge const*, std::__1::allocator<TaxiEdge const*> >&, TaxiEdge::nodeTy) const:LTApt.cpp"> + contextName = "ADSBExchangeConnection::ProcessFetchedData(std::__1::map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, LTFlightData, std::__1::less<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, LTFlightData> > >&):LTFlightData.cpp"> + contextName = "UDPReceiver::UDPReceiver(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int, unsigned long, unsigned int):Network.cpp"> + + + + - - + value = "s_cur_plane"> + contextName = "RealTrafficConnection::ProcessRecvedTrafficData(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >):LTRealTraffic.cpp"> + + + + + value = "_from.v[2]"> + + + + + + + value = "sv[3]"> + value = "sv"> + contextName = "CreateMsgWindow(float, logLevelTy, char const*, ...):TextIO.cpp"> + contextName = "XPMPRenderMultiplayerPlanes(int, int, void*):XPMPMultiplayer.cpp"> + + + + + contextName = "Apt::ShortestPath(unsigned long, bool, unsigned long, double):LTApt.cpp"> + contextName = "LTAircraft::FlightModel::FindFlightModel(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >):LTAircraft.cpp"> + contextName = "ReadOneTaxiLine(std::__1::basic_ifstream<char, std::__1::char_traits<char> >&, Apt&):LTApt.cpp"> + contextName = "LTFlightData::UpdateData(LTFlightData::FDStaticData const&):LTFlightData.cpp"> + value = "statData"> + contextName = "LTAptFindRwy(LTAircraft const&):LTApt.cpp"> + + + + + value = "listTaxiEdges"> + contextName = "LTFlightData::TryDeriveGrndStatus(double, positionTy::onGrndE&):LTFlightData.cpp"> + contextName = "ADSBExchangeHistorical::FetchAllFlightData(positionTy const&):LTFlightData.cpp"> + value = "i"> + + + + + + + + + contextName = "LTAircraft::ChangeModel(LTFlightData::FDStaticData const&):LTAircraft.cpp"> + value = "labelInternal"> @@ -558,215 +768,224 @@ + contextName = "ThreadIndexApt():LTApt.cpp"> + contextName = "BezierCurve::GetPos(positionTy&, double, double, double, double):LTAircraft.cpp"> - - - - + value = "pos.v.__begin_[4] - angle"> + value = "ts - pos.v.__begin_[3]"> + value = "pos.v.__begin_[6]"> + contextName = "LTWidget::DispatchMessages(int, void*, long, long):SettingsUI.cpp"> + + + + + + - - + value = "widgetIds[0]"> + value = "(XPMouseState_t*)inParam1"> + contextName = "LTAircraft::LTAircraft(LTFlightData&):LTAircraft.cpp"> + contextName = "LTFlightData::TryDeriveGrndStatus(double, positionTy::onGrndE&, bool):LTFlightData.cpp"> - - - - + contextName = "LTFlightData::CalcNextPosMain():LTFlightData.cpp"> - - - - + contextName = "TaxiEdge::TaxiEdge(TaxiEdge::nodeTy, unsigned long, unsigned long, double, double):LTApt.cpp"> + contextName = "DistResultToBaseLoc(double, double, double, double, distToLineTy const&, double&, double&):CoordCalc.cpp"> + contextName = "Apt::FindClosestEdge(positionTy const&, positionTy&, int, double, double, unsigned long&, std::__1::vector<unsigned long, std::__1::allocator<unsigned long> > const&) const:LTApt.cpp"> + contextName = "BezierCurve::Define(positionTy const&, double, positionTy const&, double, positionTy const&, double):LTAircraft.cpp"> + value = "start"> + contextName = "Apt::FindClosestEdge(positionTy const&, positionTy&, int, double) const:LTApt.cpp"> + contextName = "LTCapDateTime::SetCaption():SettingsUI.cpp"> - - - - + contextName = "XPMP2::CSLModelByName(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&):CSLModels.cpp"> + contextName = "LTFlightData::UpdateStaticLabel():LTFlightData.cpp"> + contextName = "CoordPlusVector(positionTy const&, vectorTy const&):CoordCalc.cpp"> + contextName = "DataRefs::DataRefs(int):DataRefs.cpp"> + value = "tStartThisYear"> + + + + + + + value = "pos"> + contextName = "positionTy::operator|=(positionTy const&):CoordCalc.cpp"> + contextName = "LTAircraft::~LTAircraft():LTAircraft.cpp"> + contextName = "Apt::AddApt(Apt&&):LTApt.cpp"> + contextName = "Apt::JoinPathEnds():LTApt.cpp"> + + + + + + + contextName = "LTFlightData::UpdateData(LTFlightData::FDDynamicData const&, positionTy const*):LTFlightData.cpp"> + contextName = "RealTrafficConnection::ProcessRecvedWeatherData(char const*):LTRealTraffic.cpp"> + value = "(weather+12)"> + contextName = "LTFlightData::IsPosOK(positionTy const&, positionTy const&) const:LTFlightData.cpp"> + + + + + contextName = "LTAircraft::CalcFlightModel(positionTy const&, positionTy const&):LTAircraft.cpp"> + value = "flaps"> + value = "pitch"> + value = "gearDeflection"> + + + contextName = "BezierCurve::GetPos(positionTy&, double, double, double, double) const:LTAircraft.cpp"> + contextName = "OpenSkyConnection::ProcessFetchedData(std::__1::map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, LTFlightData, std::__1::less<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, LTFlightData> > >&):LTChannel.cpp"> + + + + + contextName = "TaxiEdge::ReplaceNode(unsigned long, unsigned long):LTApt.cpp"> + contextName = "OpenSkyAcMasterdata::GetURL(positionTy const&):LTChannel.cpp"> + value = "currKey"> + contextName = "::XPluginStart(char *, char *, char *):LiveTraffic.cpp"> + contextName = "FetchVersionCB(char*, unsigned long, unsigned long, void*):LTVersion.cpp"> + value = "ptr+2700"> + contextName = "AccelParam::StartSpeedControl(double, double, double, double, double):LTAircraft.cpp"> + value = "_startTime"> + value = "_targetTime"> + + + contextName = "FlightradarConnection::ProcessFetchedData(std::__1::map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, LTFlightData, std::__1::less<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, LTFlightData> > >&):LTChannel.cpp"> + contextName = "AccelParam::getRatio(double) const:LTAircraft.cpp"> - - - - + contextName = "DataRefs::LTSetSimDateTime(void*, int):DataRefs.cpp"> + contextName = "boundingBoxTy::boundingBoxTy(positionTy const&, double, double):CoordCalc.cpp"> + contextName = "DataRefs::GetZuluTimeDiff():DataRefs.cpp"> + value = "l"> - - diff --git a/Resources/Doc8643.txt b/Resources/Doc8643.txt index a03645c8..0438ac39 100644 --- a/Resources/Doc8643.txt +++ b/Resources/Doc8643.txt @@ -726,9 +726,13 @@ AIRBUS HELICOPTERS-HARBIN EC-175 EC75 H2T M AIRBUS HELICOPTERS-HARBIN H-175 EC75 H2T M AIRBUS HELICOPTERS-HARBIN Z-15 EC75 H2T M AIRBUS HELICOPTERS-KAWASAKI BK-117C-2 EC45 H2T L +AIRBUS HELICOPTERS-KAWASAKI BK-117D EC45 H2T L +AIRBUS HELICOPTERS-KAWASAKI BK-117D Jupiter EC45 H2T L AIRBUS HELICOPTERS-KAWASAKI EC-145 EC45 H2T L AIRBUS HELICOPTERS-KAWASAKI EC-645 EC45 H2T L AIRBUS HELICOPTERS-KAWASAKI H-145 EC45 H2T L +AIRBUS HELICOPTERS-KAWASAKI H-145 Jupiter EC45 H2T L +AIRBUS HELICOPTERS-KAWASAKI Jupiter EC45 H2T L AIRBUS HELICOPTERS-KAWASAKI Lakota EC45 H2T L AIRBUS HELICOPTERS-KAWASAKI UH-72 Lakota EC45 H2T L AIRBUS HELICOPTERS AS-332C Super Puma AS32 H2T M @@ -900,6 +904,7 @@ AIRLONY Skylane ALSL L1P L AIRMASTER Avalon 680 AVLN A1T L AIRMAX M-22 SeaMax SMAX A1P L AIRMAX SeaMax SMAX A1P L +AIRNET One ONE L1P L AIRO 1 JFOX L1P L AIRO 5 UF10 L1P L AIRPLANE FACTORY Sling 2 SLG2 L1P L @@ -1122,6 +1127,8 @@ AOI Tucano TUCA L1T L APPLEBAY Zia ZIA L1P L APPLEGATE & WEYANT Dart GC DRTG L1P L AQUILA A-210 A210 L1P L +AQUILA A-211 A210 L1P L +AQUILA A-212 A210 L1P L AQUILA AT-01 A210 L1P L ARADO Ar-79 AR79 L1P L ARC ATLANTIQUE RF-47 RF47 L1P L @@ -2310,6 +2317,7 @@ BUSHBY Midget Mustang MIMU L1P L BUSHBY Mustang 2 MUS2 L1P L BUTTERFLY Golden Butterfly GOBU G1P L BUTTERFLY Super Sky Cycle SSC G1P L +BYE AEROSPACE eFlyer 2 EF2 L1E L C2P One ONE L1P L CAARP CAP-10 CP10 L1P L CAARP CAP-20 CP20 L1P L @@ -3259,10 +3267,13 @@ DAC RangeR RNGR L1P L DAEWOO KT-1 Woong-Bee KT1 L1T L DAEWOO KTX-1 Woong-Bee KT1 L1T L DAEWOO Woong-Bee KT1 L1T L +DAHER Kodiak KODI L1T L DAHER TBM-700N (TBM-910) TBM9 L1T L DAHER TBM-700N (TBM-930) TBM9 L1T L +DAHER TBM-700N (TBM-940) TBM9 L1T L DAHER TBM-910 TBM9 L1T L DAHER TBM-930 TBM9 L1T L +DAHER TBM-940 TBM9 L1T L DALLACH D-4 Fascination DAL4 L1P L DALLACH D-5 Evolution DAL5 L1P L DALLACH Evolution DAL5 L1P L @@ -3785,6 +3796,7 @@ EMBRAER AT-29 E314 L1T L EMBRAER Bandeirante E110 L2T L EMBRAER Bandeirulha E110 L2T L EMBRAER Brasilia E120 L2T M +EMBRAER C-390 Millennium E390 L2J M EMBRAER C-95 Bandeirante E110 L2T L EMBRAER C-99 E145 L2J M EMBRAER Carioca P28B L1P L @@ -3831,6 +3843,7 @@ EMBRAER EMB-312H Super Tucano E314 L1T L EMBRAER EMB-314 ALX E314 L1T L EMBRAER EMB-314 Super Tucano E314 L1T L EMBRAER EMB-326 Xavante M326 L1J L +EMBRAER EMB-390 E390 L2J M EMBRAER EMB-500 Phenom 100 E50P L2J L EMBRAER EMB-505 Phenom 300 E55P L2J M EMBRAER EMB-545 Legacy 450 E545 L2J M @@ -3867,7 +3880,7 @@ EMBRAER ERJ-190-300 E290 L2J M EMBRAER ERJ-190-400 E295 L2J M EMBRAER ERJ-190-500 E275 L2J M EMBRAER Ipanema IPAN L1P L -EMBRAER KC-390 KC39 L2J M +EMBRAER KC-390 E390 L2J M EMBRAER Lance 2 P32T L1P L EMBRAER Legacy (EMB-135BJ) E35L L2J M EMBRAER Legacy 450 E545 L2J M @@ -3875,6 +3888,7 @@ EMBRAER Legacy 500 E550 L2J M EMBRAER Legacy 600 E35L L2J M EMBRAER Legacy 650 E35L L2J M EMBRAER Lineage 1000 E190 L2J M +EMBRAER Millennium E390 L2J M EMBRAER Minuano PA32 L1P L EMBRAER Navajo PA31 L2P L EMBRAER P-95 Bandeirulha E110 L2T L @@ -3943,7 +3957,6 @@ ENSTROM TH-28 EN48 H1T L EPERVIER (1) Epervier EPER L1P L EPERVIER (2) X-1 EPX1 L1P L EPIC AIRCRAFT E-1000 EPIC L1T L -EPIC AIRCRAFT Epic Dynasty EPIC L1T L EPIC AIRCRAFT Epic Elite ELIT L2J L EPIC AIRCRAFT Epic Escape ESCA L1T L EPIC AIRCRAFT Epic LT EPIC L1T L @@ -4092,6 +4105,7 @@ EXTRA EA-330 E300 L1P L EXTRA EA-350 E300 L1P L EXTRA EA-400 E400 L1P L EXTRA EA-500 E500 L1T L +EXTRA NG EXNG L1P L F+W EMMEN Alouette 3 ALO3 H1T L F+W EMMEN DH-100 Vampire VAMP L1J L F+W EMMEN DH-112 Venom VNOM L1J M @@ -5993,11 +6007,15 @@ LMAASA AT-63 Pampa IA63 L1J L LMAASA IA-63 Pampa IA63 L1J L LMAASA Pampa IA63 L1J L LOAD RANGER 2000 LR2T H1T L +LOCKHEED MARTIN 382C Hercules C130 L4T M +LOCKHEED MARTIN 382J Super Hercules C30J L4T M +LOCKHEED MARTIN 382U Super Hercules C30J L4T M +LOCKHEED MARTIN 382V Super Hercules C30J L4T M LOCKHEED MARTIN C-130H Hercules C130 L4T M -LOCKHEED MARTIN C-130J Hercules C30J L4T M -LOCKHEED MARTIN CC-130J Hercules C30J L4T M +LOCKHEED MARTIN C-130J Super Hercules C30J L4T M +LOCKHEED MARTIN CC-130J Super Hercules C30J L4T M LOCKHEED MARTIN Desert Falcon F16 L1J M -LOCKHEED MARTIN EC-130J Hercules C30J L4T M +LOCKHEED MARTIN EC-130J Super Hercules C30J L4T M LOCKHEED MARTIN F-16 Sufa F16 L1J M LOCKHEED MARTIN F-16 Desert Falcon F16 L1J M LOCKHEED MARTIN F-16 Fighting Falcon F16 L1J M @@ -6006,26 +6024,28 @@ LOCKHEED MARTIN F-35A Lightning 2 F35 L1J M LOCKHEED MARTIN F-35B Lightning 2 VF35 L1J M LOCKHEED MARTIN F-35C Lightning 2 F35 L1J M LOCKHEED MARTIN Fighting Falcon F16 L1J M -LOCKHEED MARTIN HC-130J Hercules C30J L4T M -LOCKHEED MARTIN Hercules (AE-2100 engines) C30J L4T M -LOCKHEED MARTIN Hercules (T-56 engines) C130 L4T M -LOCKHEED MARTIN KC-130J Hercules C30J L4T M -LOCKHEED MARTIN L-100 Hercules C130 L4T M +LOCKHEED MARTIN HC-130J Super Hercules C30J L4T M +LOCKHEED MARTIN Hercules C130 L4T M +LOCKHEED MARTIN KC-130J Super Hercules C30J L4T M LOCKHEED MARTIN L-285 Orion P3 L4T M -LOCKHEED MARTIN L-382 Hercules C130 L4T M LOCKHEED MARTIN L-645 Raptor F22 L2J M LOCKHEED MARTIN LC-130 Hercules C130 L4T M +LOCKHEED MARTIN LM-100J Super Hercules C30J L4T M LOCKHEED MARTIN Lightning 2 (F-35A/C) F35 L1J M LOCKHEED MARTIN Lightning 2 (F-35B) VF35 L1J M -LOCKHEED MARTIN MC-130J Hercules C30J L4T M +LOCKHEED MARTIN MC-130J Super Hercules C30J L4T M LOCKHEED MARTIN Orion P3 L4T M LOCKHEED MARTIN P-3 Orion P3 L4T M LOCKHEED MARTIN Raptor F22 L2J M LOCKHEED MARTIN Sufa F16 L1J M -LOCKHEED MARTIN WC-130J Hercules C30J L4T M +LOCKHEED MARTIN Super Hercules C30J L4T M +LOCKHEED MARTIN WC-130J Super Hercules C30J L4T M LOCKHEED MARTIN X-55 X55 L2J M LOCKHEED-AZCARATE LASA-60 Santa Maria LA60 L1P L LOCKHEED-AZCARATE Santa Maria LA60 L1P L +LOCKHEED 182 Hercules C130 L4T M +LOCKHEED 282 Hercules C130 L4T M +LOCKHEED 382 Hercules C130 L4T M LOCKHEED AC-130 Spectre C130 L4T M LOCKHEED AP-3 Orion P3 L4T M LOCKHEED AT-33 T33 L1J M @@ -6033,7 +6053,6 @@ LOCKHEED Arcturus P3 L4T M LOCKHEED Aurora P3 L4T M LOCKHEED Aya C130 L4T M LOCKHEED B-34 Lexington L37 L2P M -LOCKHEED Blackbird SR71 L2J M LOCKHEED C-121 Constellation CONI L4P M LOCKHEED C-130 Karnaf C130 L4T M LOCKHEED C-130A Hercules C130 L4T M @@ -6063,7 +6082,6 @@ LOCKHEED EC-130H Hercules C130 L4T M LOCKHEED EC-130Q Hercules C130 L4T M LOCKHEED EP-3 Orion P3 L4T M LOCKHEED ER-2 U2 L1J M -LOCKHEED ES-3 Viking S3 L2J M LOCKHEED Electra (L-10) L10 L2P L LOCKHEED Electra (L-188) L188 L4T M LOCKHEED Electra Junior L12 L2P L @@ -6104,16 +6122,13 @@ LOCKHEED L-14 Hudson L14 L2P M LOCKHEED L-14 Super Electra L14 L2P M LOCKHEED L-15 Harpoon L37 L2P M LOCKHEED L-18 Lodestar L18 L2P M -LOCKHEED L-182 Hercules C130 L4T M LOCKHEED L-185 Orion P3 L4T M LOCKHEED L-188 Electra L188 L4T M LOCKHEED L-222 Lightning P38 L2P M LOCKHEED L-237 Ventura L37 L2P M -LOCKHEED L-282 Hercules C130 L4T M LOCKHEED L-285 Orion P3 L4T M LOCKHEED L-300 Starlifter C141 L4J H LOCKHEED L-322 Lightning P38 L2P M -LOCKHEED L-382 Hercules C130 L4T M LOCKHEED L-394 Viking S3 L2J M LOCKHEED L-422 Lightning P38 L2P M LOCKHEED L-426 Neptune P2 L2P M @@ -6152,7 +6167,6 @@ LOCKHEED RP-3 Orion P3 L4T M LOCKHEED RT-33 T33 L1J M LOCKHEED S-3 Viking S3 L2J M LOCKHEED SP-2 Neptune P2 L2P M -LOCKHEED SR-71 Blackbird SR71 L2J M LOCKHEED Sapeer C130 L4T M LOCKHEED Shooting Star T33 L1J M LOCKHEED Spectre C130 L4T M @@ -6171,7 +6185,6 @@ LOCKHEED Tp84 Hercules C130 L4T M LOCKHEED TriStar L101 L3J H LOCKHEED U-2 U2 L1J M LOCKHEED UP-3 Orion P3 L4T M -LOCKHEED US-3 Viking S3 L2J M LOCKHEED VC-121 Constellation CONI L4P M LOCKHEED VC-130 Hercules C130 L4T M LOCKHEED VP-3 Orion P3 L4T M @@ -6208,11 +6221,7 @@ LOT Ecureuil AS50 H1T L LOVING-WAYNE Love LOVE L1P L LOVING-WAYNE WR-1 Love LOVE L1P L LSA AMERICA Allegro ALGR L1P L -LTV A-7 Corsair 2 A7 L1J M -LTV Corsair 2 A7 L1J M -LTV EA-7 Corsair 2 A7 L1J M LTV F-8 Crusader F8 L1J M -LTV TA-7 Corsair 2 A7 L1J M LUCAS L-5 LUL5 L1P L LUCAS L-6 LUL6 L1P L LUCAS L-7 LUL7 L1P L @@ -6663,6 +6672,7 @@ MONNETT Sonerai SRAI L1P L MONOCOUPE 110 Special M110 L1P L MONOCOUPE 90 MC90 L1P L MONOCOUPE Special M110 L1P L +MONTAER MC-01 MC01 L1P L MONTAGNE Mountain Goat MOGO L1P L MONTANA Coyote MCOY L1P L MOONEY 201 M20P L1P L @@ -7220,6 +7230,7 @@ OMNI 404 Turbo Titan C04T L2T L OMNI Turbo Titan C04T L2T L ON MARK Marketeer B26 L2P M ON MARK Marksman B26 L2P M +ONE PRO One ONE L1P L OPTICA OA-7 Optica OPCA L1P L OPTICA Optica OPCA L1P L OPTION AIR Acapella ACPL L1P L @@ -8654,6 +8665,7 @@ SCINTEX Emeraude CP30 L1P L SCINTEX ML-250 Rubis RUBI L1P L SCINTEX Rubis RUBI L1P L SCINTEX Super Emeraude CP13 L1P L +SCODA Super Petrel PETR A1P L SCOTTISH AVIATION Bulldog BDOG L1P L SCOTTISH AVIATION Fpl61 BDOG L1P L SCOTTISH AVIATION Jetstream T.Mk.1 JS20 L2T L @@ -9817,11 +9829,8 @@ VOLPAR Turboliner B18T L2T L VOUGHT-SIKORSKY Corsair CORS L1P M VOUGHT-SIKORSKY F4U Corsair CORS L1P M VOUGHT-SIKORSKY V-166 Corsair CORS L1P M -VOUGHT A-7 Corsair 2 A7 L1J M -VOUGHT Corsair 2 A7 L1J M VOUGHT Panther 800 AS65 H2T L VOUGHT SA-366 Panther 800 AS65 H2T L -VOUGHT TA-7 Corsair 2 A7 L1J M VSR SR-1 Snoshoo SNOS L1P L VSR Snoshoo SNOS L1P L VSTOL Pairadigm PDIG L2P L @@ -10044,8 +10053,10 @@ XIAN FBC-1 Flying Leopard JH7 L2J M XIAN Flying Leopard JH7 L2J M XIAN H-6 TU16 L2J M XIAN JH-7 JH7 L2J M +XIAN Kunpeng Y20 L4J H XIAN MA-60 MA60 L2T M XIAN MA-60H MA6H L2T M +XIAN Y-20 Kunpeng Y20 L4J H XIAN Y-7-100 AN24 L2T M XIAN Y-7-200 AN24 L2T M XIAN Y-7E AN24 L2T M @@ -10090,8 +10101,8 @@ YALO Bocian M-2000 SZ9M L1P L YALO GM-01 Gniady GM01 L1P L YALO Gniady GM01 L1P L YALO SZD-9bis Bocian M-2000 SZ9M L1P L -YEOMAN YEOMAN Cropmaster YA1 L1P L -YEOMAN YEOMAN YA-1 Cropmaster YA1 L1P L +YEOMAN Cropmaster YA1 L1P L +YEOMAN YA-1 Cropmaster YA1 L1P L ZENAIR Acro-Z CH15 L1P L ZENAIR Acro-Zenith CH15 L1P L ZENAIR CH-100 Mono-Z CH10 L1P L diff --git a/Src/ACInfoWnd.cpp b/Src/ACInfoWnd.cpp index feeee449..680be03e 100644 --- a/Src/ACInfoWnd.cpp +++ b/Src/ACInfoWnd.cpp @@ -768,7 +768,7 @@ void ACIWnd::UpdateDynValues() valRoll.SetDescriptor(pAc->GetRoll()); valAlt.SetDescriptor(round(pos.alt_ft())); if (pos.IsOnGnd()) - valAGL.SetDescriptor(positionTy::GrndE2String(positionTy::GND_ON)); + valAGL.SetDescriptor("On Grnd"); else valAGL.SetDescriptor(pAc->GetPHeight_ft()); valSpeed.SetDescriptor(pAc->GetSpeed_kt()); diff --git a/Src/CoordCalc.cpp b/Src/CoordCalc.cpp index 1d9bb271..c2f9a24c 100644 --- a/Src/CoordCalc.cpp +++ b/Src/CoordCalc.cpp @@ -29,8 +29,26 @@ #include #include - // -//MARK: Coordinate Calc +// +// MARK: ptTy +// + +bool ptTy::operator== (const ptTy& _o) const +{ + return dequal(x, _o.x) && dequal(y, _o.y); +} + + +std::string ptTy::dbgTxt () const +{ + char buf[100]; + snprintf(buf, sizeof(buf), "%7.5f, %7.5f", y, x); + return std::string(buf); +} + + +// +// MARK: Coordinate Calc // (as per stackoverflow post, adapted) // double CoordAngle (double lat1, double lon1, double lat2, double lon2) @@ -46,8 +64,7 @@ double CoordAngle (double lat1, double lon1, double lat2, double lon2) (sin(lat1) * cos(lat2) * cos(longitudeDifference)); const double y = sin(longitudeDifference) * cos(lat2); - const double degree = rad2deg(atan2(y, x)); - return (degree >= 0)? degree : (degree + 360); + return rad2deg360(atan2(y, x)); } double CoordDistance (double lat1, double lon1, double lat2, double lon2) @@ -165,6 +182,73 @@ void DistResultToBaseLoc (double ln_x1, double ln_y1, } +// Intersection point of two lines through given points +/// @details `1` = `a`...`4` = `d` +ptTy CoordIntersect (const ptTy& a, const ptTy& b, const ptTy& c, const ptTy& d, + double* pT, double* pU) +{ + const double divisor = + (a.x - b.x)*(c.y - d.y) - (a.y - b.y)*(c.x - d.x); + + // Are we to calculate t and u, too? + if (pT) + *pT = ((a.x - c.x)*(c.y - d.y) - (a.y - c.y)*(c.x - d.x))/divisor; + if (pU) + *pU = ((a.x - b.x)*(a.y - c.y) - (a.y - b.y)*(a.x - c.x))/divisor; + + // return the intersection point + const double f1 = (a.x*b.y - a.y*b.x); + const double f2 = (c.x*d.y - c.y*d.x); + return (f1 * (c - d) - f2 * (a - b)) / divisor; +} + + +// Calculate a point on a quadratic Bezier curve +ptTy Bezier (double t, const ptTy& p0, const ptTy& p1, const ptTy& p2, + double* pAngle) +{ + const double oneMt = 1-t; + + // Angle wanted? + if (pAngle) { + // B'(t) = 2(1-t)(p1-p0)+2t(p2-p1) + ptTy dtB = 2 * oneMt * (p1-p0) + 2 * t * (p2-p1); + *pAngle = rad2deg360(std::atan2(dtB.x, dtB.y)); + } + + // We calculate the value directly, ie. without De-Casteljau + // B(t) = (1-t)^2 p0 + 2(1+t)t p1 + t^2 p2 + return + (oneMt*oneMt) * p0 + // (1-t)^2 p0 + + (2 * oneMt * t) * p1 + // 2(1+t)t p1 + + (t*t) * p2; // t^2 p2 +} + +// Calculate a point on a cubic Bezier curve +ptTy Bezier (double t, const ptTy& p0, const ptTy& p1, const ptTy& p2, const ptTy& p3, + double* pAngle) +{ + const double oneMt = 1-t; + const double oneMt2 = oneMt * oneMt; + const double t2 = t * t; + + // Angle wanted? + if (pAngle) { + // B'(t) = 3(1-t)^2 (p1-p0) + 6(1-t)t(p2-p1) + 3t^2(p3-p2) + ptTy dtB = 3*oneMt2*(p1-p0) + 6*(oneMt)*t*(p2-p1) + 3*t2*(p3-p2); + *pAngle = rad2deg360(std::atan2(dtB.x, dtB.y)); + } + + // We calculate the value directly, ie. without De-Casteljau + // B(t) = (1-t)^3 p0 + 3(1-t)^2t p1 + 3(1-t)t^2 p2 + t^3 p3 + return + (oneMt2 * oneMt) * p0 + // (1-t)^3 p0 + + (3 * oneMt2 * t) * p1 + // 3(1-t)^2t p1 + + (3 * oneMt * t2) * p2 + // 3(1-t)t^2 p2 + + (t2 * t) * p3; // t^3 p3 +} + + // returns terrain altitude at given position // returns NaN in case of failure double YProbe_at_m (const positionTy& posAt, XPLMProbeRef& probeRef) @@ -221,12 +305,17 @@ vectorTy::operator std::string() const // "merges" with the given position, i.e. creates kind of an "average" position positionTy& positionTy::operator |= (const positionTy& pos) { - LOG_ASSERT(unitCoord == pos.unitCoord && unitAngle == pos.unitAngle); + LOG_ASSERT(f.unitCoord == pos.f.unitCoord && f.unitAngle == pos.f.unitAngle); // heading needs special treatment // (also removes nan value if one of the headings is nan) const double h = HeadingAvg(heading(), pos.heading(), mergeCount, pos.mergeCount); // take into account how many other objects made up the current pos! ("* count") + // Special handling for possible NAN values: If NAN on either side then the other wins + double _alt = std::isnan(alt_m()) ? pos.alt_m() : alt_m(); + double _ptc = std::isnan(pitch()) ? pos.pitch() : pitch(); + double _rol = std::isnan(roll()) ? pos.roll() : roll(); + // previous implementation: v = (v * mergeCount + pos.v) / (mergeCount+1); for (double &d : v) d *= mergeCount; // (v * mergeCount (VS doesn't compile v.apply with lambda function) v += pos.v; // + pos.v) @@ -234,17 +323,28 @@ positionTy& positionTy::operator |= (const positionTy& pos) heading() = h; + // Handle NAN cases + if (std::isnan(alt_m())) alt_m() = _alt; + if (std::isnan(pitch())) pitch() = _ptc; + if (std::isnan(roll())) roll() = _rol; + mergeCount++; // one more position object making up this position // any special flight phase? shall survive // (if both pos have special flight phases then ours survives) - if (!flightPhase) - flightPhase = pos.flightPhase; + if (!f.flightPhase) + f.flightPhase = pos.f.flightPhase; // ground status: if different, then the new one is likely off ground, // but we have it determined soon - if (onGrnd != pos.onGrnd) - onGrnd = GND_UNKNOWN; // IsOnGnd() will return false for this! + if (f.onGrnd != pos.f.onGrnd) + f.onGrnd = GND_UNKNOWN; // IsOnGnd() will return false for this! + + // Special Pos and other location flags need to be re-evaluated + f.bHeadFixed = false; + f.specialPos = SPOS_NONE; + f.bCutCorner = false; + edgeIdx = EDGE_UNKNOWN; return normalize(); } @@ -270,29 +370,35 @@ positionTy::operator XPMPPlanePosition_t() const const char* positionTy::GrndE2String (onGrndE grnd) { switch (grnd) { - case GND_OFF: return "GND_OFF "; - case GND_ON: return "GND_ON "; + case GND_OFF: return "GND_OFF"; + case GND_ON: return "GND_ON"; default: return "GND_UNKNOWN"; } } std::string positionTy::dbgTxt () const { - char buf[100]; - snprintf(buf, sizeof(buf), "(%7.4f, %7.4f) %5.0ff %s {h %3.0f, p %3.0f, r %3.0f} [%.1f] %s", + char buf[120]; + snprintf(buf, sizeof(buf), "%.1f: (%7.5f, %7.5f) %7.1fft %8.8s %3.3s %2.2s %-13.13s %4.*zu {h %3.0f%c, p %3.0f, r %3.0f}", + ts(), lat(), lon(), alt_ft(), - GrndE2String(onGrnd), - heading(), pitch(), roll(), - ts(), - flightPhase ? LTAircraft::FlightPhase2String(LTAircraft::FlightPhase(flightPhase)).c_str() : ""); + GrndE2String(f.onGrnd), + SpecialPosE2String(f.specialPos), + f.bCutCorner ? "CT" : " ", + f.flightPhase ? (LTAircraft::FlightPhase2String(f.flightPhase)).c_str() : "", + HasTaxiEdge() ? 1 : 0, + HasTaxiEdge() ? edgeIdx : 0, + heading(), + (f.bHeadFixed ? '*' : ' '), + pitch(), roll()); return std::string(buf); } positionTy::operator std::string () const { char buf[100]; - snprintf(buf, sizeof(buf), "%7.4f %c / %7.4f %c", + snprintf(buf, sizeof(buf), "%6.4f %c / %6.4f %c", std::abs(lat()), lat() < 0 ? 'S' : 'N', std::abs(lon()), lon() < 0 ? 'W' : 'E'); return std::string(buf); @@ -301,7 +407,7 @@ positionTy::operator std::string () const // normalizes to -90/+90 lat, -180/+180 lon, 360° heading, return *this positionTy& positionTy::normalize() { - LOG_ASSERT(unitAngle==UNIT_DEG && unitCoord==UNIT_WORLD); + LOG_ASSERT(f.unitAngle==UNIT_DEG && f.unitCoord==UNIT_WORLD); // latitude: works for -180 <= lat <= 180 LOG_ASSERT (lat() <= 180); @@ -326,7 +432,7 @@ positionTy& positionTy::normalize() // is a good valid position? bool positionTy::isNormal (bool bAllowNanAltIfGnd) const { - LOG_ASSERT(unitAngle==UNIT_DEG && unitCoord==UNIT_WORLD); + LOG_ASSERT(f.unitAngle==UNIT_DEG && f.unitCoord==UNIT_WORLD); return // should be actual numbers ( !std::isnan(lat()) && !std::isnan(lon()) && !std::isnan(ts())) && @@ -353,20 +459,20 @@ bool positionTy::isFullyValid() const positionTy positionTy::deg2rad() const { positionTy ret(*this); // copy position - if (unitAngle == UNIT_DEG) { // if DEG convert to RAD + if (f.unitAngle == UNIT_DEG) { // if DEG convert to RAD ret.lat() = ::deg2rad(lat()); ret.lon() = ::deg2rad(lon()); - ret.unitAngle = UNIT_RAD; + ret.f.unitAngle = UNIT_RAD; } return ret; } positionTy& positionTy::deg2rad() { - if (unitAngle == UNIT_DEG) { // if DEG convert to RAD + if (f.unitAngle == UNIT_DEG) { // if DEG convert to RAD lat() = ::deg2rad(lat()); lon() = ::deg2rad(lon()); - unitAngle = UNIT_RAD; + f.unitAngle = UNIT_RAD; } return *this; } @@ -374,20 +480,20 @@ positionTy& positionTy::deg2rad() positionTy positionTy::rad2deg() const { positionTy ret(*this); // copy position - if (unitAngle == UNIT_RAD) { // if DEG convert to RAD + if (f.unitAngle == UNIT_RAD) { // if DEG convert to RAD ret.lat() = ::rad2deg(lat()); ret.lon() = ::rad2deg(lon()); - ret.unitAngle = UNIT_DEG; + ret.f.unitAngle = UNIT_DEG; } return ret; } positionTy& positionTy::rad2deg() { - if (unitAngle == UNIT_RAD) { // if DEG convert to RAD + if (f.unitAngle == UNIT_RAD) { // if DEG convert to RAD lat() = ::rad2deg(lat()); lon() = ::rad2deg(lon()); - unitAngle = UNIT_DEG; + f.unitAngle = UNIT_DEG; } return *this; } @@ -403,20 +509,20 @@ positionTy& positionTy::operator += (const vectorTy& vec ) // convert between World and Local OpenGL coordinates positionTy& positionTy::LocalToWorld() { - if ( unitCoord == UNIT_LOCAL ) { + if (f.unitCoord == UNIT_LOCAL) { XPLMLocalToWorld(X(), Y(), Z(), &lat(), &lon(), &alt_m()); - unitCoord = UNIT_WORLD; + f.unitCoord = UNIT_WORLD; } return *this; } positionTy& positionTy::WorldToLocal() { - if ( unitCoord == UNIT_WORLD ) { + if (f.unitCoord == UNIT_WORLD) { XPLMWorldToLocal(lat(), lon(), alt_m(), &X(), &Y(), &Z()); - unitCoord = UNIT_LOCAL; + f.unitCoord = UNIT_LOCAL; } return *this; } diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index 4319a336..4e76d7d7 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -286,6 +286,7 @@ const char* DATA_REFS_XP[] = { "sim/flightmodel/position/lon_ref", // float n degrees The longitude of the point 0,0,0 in OpenGL coordinates" "sim/graphics/view/view_is_external", "sim/graphics/view/view_type", + "sim/graphics/view/using_modern_driver", // boolean: Vulkan/Metal in use? (since XP11.50) "sim/weather/barometer_sealevel_inhg", // float y 29.92 +- .... The barometric pressure at sea level. "sim/weather/use_real_weather_bool", // int y 0,1 Whether a real weather file is in use." "sim/flightmodel/position/latitude", @@ -633,7 +634,8 @@ bool DataRefs::Init () // for XP10 compatibility we accept if we don't find a few, // all else stays an error if (i != DR_VR_ENABLED && - i != DR_PILOTS_HEAD_ROLL) { + i != DR_PILOTS_HEAD_ROLL && + i != DR_MODERN_DRIVER) { LOG_MSG(logFATAL,ERR_DATAREF_FIND,DATA_REFS_XP[i]); return false; } @@ -791,7 +793,7 @@ positionTy DataRefs::GetUsersPlanePos(double& trueAirspeed_m, double& track ) co XPLMGetDataf(adrXP[DR_PLANE_HEADING]), XPLMGetDataf(adrXP[DR_PLANE_PITCH]), XPLMGetDataf(adrXP[DR_PLANE_ROLL]), - XPLMGetDatai(adrXP[DR_PLANE_ONGRND]) ? positionTy::GND_ON : positionTy::GND_OFF + XPLMGetDatai(adrXP[DR_PLANE_ONGRND]) ? GND_ON : GND_OFF ); // make invalid pos invalid @@ -1308,6 +1310,9 @@ void DataRefs::LTSetCfgValue (void* p, int val) bool DataRefs::SetCfgValue (void* p, int val) { + // If fdSnapTaxiDist changes we might want to enable/disable airport reading + int oldFdSnapTaxiDist = fdSnapTaxiDist; + // we don't exactly know which parameter p points to... // ...we just set it, validate all of them, and reset in case validation fails int oldVal = *reinterpret_cast(p); @@ -1315,13 +1320,18 @@ bool DataRefs::SetCfgValue (void* p, int val) // any configuration value invalid? if (labelColor < 0 || labelColor > 0xFFFFFF || +#ifdef DEBUG + maxNumAc < 1 || maxNumAc > 100 || +#else maxNumAc < 5 || maxNumAc > 100 || +#endif maxFullNumAc < 5 || maxFullNumAc > 100 || fullDistance < 1 || fullDistance > 100 || fdStdDistance < 5 || fdStdDistance > 100 || fdRefreshIntvl < 10 || fdRefreshIntvl > 5*60 || fdBufPeriod < fdRefreshIntvl || fdBufPeriod > 5*60 || acOutdatedIntvl < 2*fdRefreshIntvl || acOutdatedIntvl > 5*60 || + fdSnapTaxiDist < 0 || fdSnapTaxiDist > 50 || netwTimeout < 15 || hideBelowAGL < 0 || hideBelowAGL > MDL_ALT_MAX || rtListenPort < 1024 || rtListenPort > 65535 || @@ -1335,6 +1345,17 @@ bool DataRefs::SetCfgValue (void* p, int val) return false; } + // Special handling for fdSnapTaxiDist: + if (oldFdSnapTaxiDist != fdSnapTaxiDist) // snap taxi dist did change + { + // switched from on to off? + if (oldFdSnapTaxiDist > 0 && fdSnapTaxiDist == 0) + LTAptDisable(); + // switched from off to on? + else if (oldFdSnapTaxiDist == 0 && fdSnapTaxiDist > 0) + LTAptEnable(); + } + // success return true; } diff --git a/Src/LTADSBEx.cpp b/Src/LTADSBEx.cpp index 9386b289..e0df5836 100644 --- a/Src/LTADSBEx.cpp +++ b/Src/LTADSBEx.cpp @@ -183,7 +183,7 @@ bool ADSBExchangeConnection::ProcessFetchedData (mapLTFlightDataTy& fdMap) alt_ft * M_per_FT, posTime, dyn.heading); - pos.onGrnd = dyn.gnd ? positionTy::GND_ON : positionTy::GND_OFF; + pos.f.onGrnd = dyn.gnd ? GND_ON : GND_OFF; // position is rather important, we check for validity if ( pos.isNormal(true) ) { @@ -848,7 +848,7 @@ bool ADSBExchangeHistorical::ProcessFetchedData (mapLTFlightDataTy& fdMap) { // we need a good take on the ground status of mainPos // for later landing detection - mainPos.onGrnd = dyn.gnd ? positionTy::GND_ON : positionTy::GND_OFF; + mainPos.f.onGrnd = dyn.gnd ? GND_ON : GND_OFF; // FIXME: Called from outside main thread, // can produce wrong terrain alt (2 cases here) @@ -864,7 +864,7 @@ bool ADSBExchangeHistorical::ProcessFetchedData (mapLTFlightDataTy& fdMap) // short-cut: if we are in the air then skip adding trails // they might be good on the ground...in the air the // positions can be too inaccurate causing jumps in speed, vsi, heading etc... - if (mainPos.onGrnd == positionTy::GND_OFF) + if (mainPos.f.onGrnd == GND_OFF) pCosList = NULL; // found trails and there are at least 2 quadrupels, i.e. really a "trail" not just a single pos? @@ -955,11 +955,11 @@ bool ADSBExchangeHistorical::ProcessFetchedData (mapLTFlightDataTy& fdMap) for(positionTy& posIter: trails) { // calc altitude based on vsiBef, as soon as we touch ground it will be normalized to terrain altitude posIter.alt_m() = refPos.alt_m() + vsiBef * (posIter.ts()-refPos.ts()); - posIter.onGrnd = positionTy::GND_UNKNOWN; + posIter.f.onGrnd = GND_UNKNOWN; fd.TryDeriveGrndStatus(posIter); // only add pos if not off ground // (see above: airborne we don't want too many positions) - if (posIter.onGrnd != positionTy::GND_OFF) + if (posIter.f.onGrnd != GND_OFF) fd.AddNewPos(posIter); } @@ -985,7 +985,7 @@ bool ADSBExchangeHistorical::ProcessFetchedData (mapLTFlightDataTy& fdMap) if ( tsDiff > 0 ) { for(positionTy& posIter: trails) { posIter.alt_m() = refPos.alt_m() + altDiff * (posIter.ts()-refPos.ts()) / tsDiff; - posIter.onGrnd = positionTy::GND_UNKNOWN; + posIter.f.onGrnd = GND_UNKNOWN; fd.AddNewPos(posIter); } bAddedTrails = true; diff --git a/Src/LTAircraft.cpp b/Src/LTAircraft.cpp index 083c692a..165d4aeb 100644 --- a/Src/LTAircraft.cpp +++ b/Src/LTAircraft.cpp @@ -42,8 +42,10 @@ struct cycleInfo { cycleInfo prevCycle = { -1, -1, -1, 0 }; cycleInfo currCycle = { -1, -1, -1, 0 }; -/// Position of user's plane, updated irregularly but often enough -positionTy posUsersPlane; +#ifdef DEBUG +/// Is the selected aircraft currently being calculated by a callback to LTAircraft::GetPlanePosition? +static bool gSelAcCalc = false; +#endif // cycle the cycle...that is move the old current values to previous // and fetch new current values @@ -134,7 +136,11 @@ val(_min) void MovingParam::SetVal(double _val) { - LOG_ASSERT(defMin <= _val && _val <= defMax); + if (!(defMin <= _val && _val <= defMax)) { + LOG_MSG(logFATAL, "min=%.1f _val=%.1f max=%.1f duration=%.1f", + defMin, _val, defMax, defDuration); + LOG_ASSERT(defMin <= _val && _val <= defMax); + } val = _val; // just set the target value, no moving valFrom = valTo = valDist = timeFrom = timeTo = NAN; } @@ -426,15 +432,15 @@ void AccelParam::StartSpeedControl(double _startSpeed, double _targetSpeed, startTime = _startTime; accelStartTime = std::max(tx, _startTime); targetTime = _targetTime; - if (dataRefs.GetDebugAcPos(pAc->key())) { - LOG_MSG(logDEBUG,"%s: start=%.1f, in %.1fs: accel=%.1f,target=%.1f) for %s", - acceleration >= 0.0 ? "ACCELERATION" : "DECELERATION", - startSpeed, - accelStartTime - startTime, - acceleration, - targetSpeed, - std::string(*pAc).c_str()); - } +// if (dataRefs.GetDebugAcPos(pAc->key())) { +// LOG_MSG(logDEBUG,"%s: start=%.1f, in %.1fs: accel=%.1f,target=%.1f) for %s", +// acceleration >= 0.0 ? "ACCELERATION" : "DECELERATION", +// startSpeed, +// accelStartTime - startTime, +// acceleration, +// targetSpeed, +// std::string(*pAc).c_str()); +// } } // *** Acceleration formula *** @@ -511,6 +517,267 @@ double AccelParam::getRatio (double deltaTS ) const return getDeltaDist(deltaTS) / targetDeltaDist; } +// +// MARK: Bezier Curves +// + +// Define a quadratic Bezier Curve based on the given flight data positions +/// @details The curve will be constructed around the _mid position, which becomes +/// the control point of the quadratic curve. The start and end point are on the +/// current and on the next leg respectively, at most half way down the leg +/// away from the mid point. The start/end position will be even closer if the +/// plane is not to turn much. Based on speed and an approximate distance +/// we allow the plane to turn at the speed defined by +/// LTAircraft::FlightModel::TAXI_TURN_TIME (on the ground) resp. +/// LTAircraft::FlightModel::FLIGHT_TURN_TIME (airborne). +void BezierCurve::Define (double _nowF, + const positionTy& _from, + double _angleFromMid, + const positionTy& _mid, + double _angleMidTo, + const positionTy& _to, + double _fullTurnTime) +{ + // pre-initialization of start and end, will be fine-tuned down the road + start = _from; + end = _mid; + mySecondMaxF = myF = NAN; + ptCtrl.clear(); + + // How much is the turn, how long shall (half) the turn take at most? + const double halfTurnTime = std::abs(HeadingDiff(_angleFromMid, + _angleMidTo)) * _fullTurnTime/360.0 / 2; + // Earliest, we start "now", but no sooner than necessary for turning + double legTime = _mid.ts() - _from.ts(); + const double f = std::max (_nowF, 1 - halfTurnTime / legTime); + // Apply an operation similar to "the factor" in CalcPPos() + // so we can be quite sure we match up with the flying plane: + start.v = (1-f) * _from.v + f * _mid.v; + + // calibrate so that from calculated start point to end of first leg + // means to do the first half of the turn: + Calibrate(f, 1.0, 0.0, 0.5); + + // Now similar for the end position, here we allow at maximum to + // turn until half the leg: + legTime = _to.ts() - _mid.ts(); + const double t = std::min (halfTurnTime, legTime/2); + mySecondMaxF = t / legTime; + end.v = (1-mySecondMaxF) * _mid.v + mySecondMaxF * _to.v;; + + // Store the control point + ptCtrl = _mid; + +#ifdef DEBUG + if (gSelAcCalc) + LOG_MSG(logDEBUG, "Quadratic cut-corner Bezier defined starting at %.1f:\n%s", + start.ts(), dbgTxt().c_str()); +#endif + + // Convert all coordinates to meter + ConvertToMeter(); +} + +// Define a quadratic Bezier curve, which computes a rreasonable control point itself +bool BezierCurve::Define (double _initF, + const positionTy& _from, + const positionTy& _to, + const vectorTy& _vec) +{ + // The control point shall be where the lines through _from/_to, + // angle as per .heading(), meet. They only meet at a reasonable + // point between _from and _to, if the angles point to different sides + // of the direct line _from|_to. + // Then, the curve leaves to one side but then comes back, ie. turning + // the other way. + const double fromHDiff = HeadingDiff(_vec.angle, _from.heading()); + const double toHDiff = HeadingDiff(_vec.angle, _to.heading()); + if (std::signbit(fromHDiff) == std::signbit(toHDiff)) + { + // Same same...a quadrativ curve would look ugly, not created + #ifdef DEBUG + if (gSelAcCalc) + LOG_MSG(logDEBUG, "Bezier NOT defined as angles point to same side! %.1f <- %+.1f << %.1f >> %+.1f -> %.1f", + _from.heading(), fromHDiff, _vec.angle, toHDiff, _to.heading()); + #endif + return false; + } + + // Backup of an active cut-corner curve...in case we bail + BezierCurve backup = *this; + + // Start and end are given directly + start = _from; + end = _to; + Calibrate(_initF, 1.0, 0.0, 1.0); + mySecondMaxF = myF = NAN; + ptCtrl.clear(); + + // Convert all coordinates to meter + ConvertToMeter(); + + // The control point: + // There must be a cleaner approach...but all I have at the moment + // is a function that needs the lines defined by two existing points, + // so I calculate two more points, one pointing away from _from, + // one pointing away from _to: + { + ptTy _from2 = _from.destPos(vectorTy(_from.heading(),100.0)); + ptTy _to2 = _to.destPos(vectorTy( _to.heading(),100.0)); + ConvertToMeter(_from2); + ConvertToMeter(_to2); + ptCtrl = CoordIntersect({0.0,0.0}, _from2, end, _to2); + } + + // Double-check: The control point must be between (and not beyond) + // start(0|0) and end and have a reasonable distance to look good + const double maxDist2 = 0.95 * pyth2(end.lat(), end.lon()); // 95% square distance of end from start (0|0) + const double minDist2 = maxDist2 / 0.95 * 0.05; // 5% square distance of end from start (0|0) + const double ctlDist2 = pyth2(ptCtrl.y, ptCtrl.x); // square distance of control point from start (0|0) + if (!between(ctlDist2, minDist2, maxDist2)) + { + // control point is not in reasonable distance to end points + // Same same...a quadrativ curve would look ugly, not created + *this = backup; + #ifdef DEBUG + if (gSelAcCalc) { + LOG_MSG(logDEBUG, "Bezier NOT defined as control point too close!"); + } + #endif + return false; + } + + +#ifdef DEBUG + if (gSelAcCalc) + LOG_MSG(logDEBUG, "Bezier defined:\n%s", + dbgTxt().c_str()); +#endif + return true; +} + +// Convert the geographic coordinates to meters, with `start` being the origin (0|0) point +/// @details The `start` point serves as origin and is - by definition - (0|0). +/// The content of `start` will not be overwritten, it is necessary for reverse conversion. +/// The other points are overwritten with the distance - in meters - to `start`. +void BezierCurve::ConvertToMeter () +{ + end.lat() = Lat2Dist(end.lat() - start.lat()); + end.lon() = Lon2Dist(end.lon() - start.lon(), start.lat()); + ConvertToMeter(ptCtrl); +} + +/// Convert the given geographic coordinates to meters +void BezierCurve::ConvertToMeter (ptTy& pt) const +{ + if (pt.isValid()) { + pt.y = Lat2Dist(pt.y - start.lat()); + pt.x = Lon2Dist(pt.x - start.lon(), start.lat()); + } +} + +// Convert the given position back to geographic coordinates +void BezierCurve::ConvertToGeographic (ptTy& pt) const +{ + if (pt.isValid()) { + pt.y = start.lat() + Dist2Lat(pt.y); + pt.x = start.lon() + Dist2Lon(pt.x, start.lat()); + } +} + +// Calibrate the Bezier so that f = [inifF..maxF] maps to myF [myInit..myMax] +void BezierCurve::Calibrate (double _initF, double _maxF, + double _myInit, double _myMax) +{ + startF = _initF; + endF = _maxF; + fCalAdd = _myInit - _initF; + fCalDiv = (_maxF - _initF) / (_myMax - _myInit); +} + +// Clear the definition, so that BezierCurve::isDefined() will return `false` +void BezierCurve::Clear () +{ + start.ts() = NAN; + end.ts() = NAN; + ptCtrl.clear(); + startF = NAN; + endF = NAN; + fCalAdd = NAN; + fCalDiv = NAN; + myF = NAN; + mySecondMaxF = NAN; +} + +// Return the position as per given timestamp, if the timestamp is between `start` and `end` +bool BezierCurve::GetPos (positionTy& pos, double ts, double f) +{ + // not defined or not in the time range of this curve? + if (!isActive(f)) + return false; + + // Calibrate f between [0.0..1.0] +#ifdef DEBUG + bool bFirstTime = std::isnan(myF); +#endif + myF = (f + fCalAdd) / fCalDiv; + if (myF < 0.0 || myF > 1.0) + return false; + + // The position to return + double angle = NAN; + ptTy p = Bezier(myF, {0.0,0.0}, ptCtrl, end, &angle); + LOG_ASSERT(p.isValid()); + + // Convert the result back into geographic coordinated + ConvertToGeographic(p); + +#ifdef DEBUG + if (gSelAcCalc) { + if (bFirstTime) { + LOG_MSG(logDEBUG, dbgTxt().c_str()); + LOG_MSG(logDEBUG, "Current pos: %s", pos.dbgTxt().c_str()); + } + if (bFirstTime || + std::abs(pos.heading()-angle) > 1.5) + LOG_MSG(logDEBUG, "ts=%.1f, f=%.4f, myF=%.4f, p={%s}, head=%.1f -> %.1f", + ts, f, myF, p.dbgTxt().c_str(), pos.heading(), angle); + } +#endif + + // Update pos + pos.lat() = p.y; + pos.lon() = p.x; + pos.alt_m() = start.alt_m() * (1-myF) + end.alt_m() * myF; + pos.heading() = angle; + + // update position's timestamp + pos.ts() = ts; + + // We've updated the position + return true; +} + + +// Debug text output +std::string BezierCurve::dbgTxt() const +{ + if (isDefined()) { + char s[250]; + snprintf(s, sizeof(s), "(%.5f %.5f) {%.5f %.5f} (%.5f %.5f) ", + start.lat(), start.lon(), + ptCtrl.y, ptCtrl.y, + end.lat(), end.lon(), + startF, + fCalAdd, fCalDiv, + myF, mySecondMaxF); + return s; + } else { + return ""; + } +} + + // //MARK: LTAircraft::FlightModel // @@ -863,7 +1130,7 @@ const LTAircraft::FlightModel* LTAircraft::FlightModel::GetFlightModel //MARK: LTAircraft::FlightPhase // -std::string LTAircraft::FlightPhase2String (FlightPhase phase) +std::string LTAircraft::FlightPhase2String (flightPhaseE phase) { switch (phase) { case FPH_UNKNOWN: return "Unknown"; @@ -911,17 +1178,15 @@ tsLastCalcRequested(0), phase(FPH_UNKNOWN), rotateTs(NAN), vsi(0.0), -bOnGrnd(false), bArtificalPos(false), bNeedNextVec(false), +bOnGrnd(false), bArtificalPos(false), gear(mdl.GEAR_DURATION), flaps(mdl.FLAPS_DURATION), -heading(mdl.TAXI_TURN_TIME, 360, 0, true), -roll(2*mdl.ROLL_MAX_BANK / mdl.ROLL_RATE, mdl.ROLL_MAX_BANK, -mdl.ROLL_MAX_BANK, false), pitch((mdl.PITCH_MAX-mdl.PITCH_MIN)/mdl.PITCH_RATE, mdl.PITCH_MAX, mdl.PITCH_MIN), reversers(MDL_REVERSERS_TIME), spoilers(MDL_SPOILERS_TIME), tireRpm(MDL_TIRE_SLOW_TIME, MDL_TIRE_MAX_RPM), gearDeflection(MDL_GEAR_DEFL_TIME, mdl.GEAR_DEFLECTION), -probeRef(NULL), probeNextTs(0), terrainAlt(0), +probeNextTs(0), terrainAlt_m(0.0), bValid(true) { // for some calcs we need correct timestamps _before_ first draw already @@ -950,16 +1215,14 @@ bValid(true) // init surfaces surfaces = { - 0, + sizeof(surfaces), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, { 0 }, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, false }; - surfaces.size = sizeof(surfaces); // init moving params where necessary pitch.SetVal(0); - roll.SetVal(0); // calculate our first position, must also succeed if (!CalcPPos()) @@ -991,10 +1254,6 @@ LTAircraft::~LTAircraft() if (IsInCameraView()) ToggleCameraView(); - // Release probe handle - if (probeRef) - XPLMDestroyProbe(probeRef); - // Decrease number of visible aircraft and log a message about that fact dataRefs.DecNumAc(); LOG_MSG(logINFO,INFO_AC_REMOVED,labelInternal.c_str()); @@ -1012,13 +1271,14 @@ void LTAircraft::CalcLabelInternal (const LTFlightData::FDStaticData& statDat) } -// MARK: LTAircraft stringify for debugging output purposes +/// LTAircraft stringify for debugging output purposes LTAircraft::operator std::string() const { char buf[500]; - snprintf(buf,sizeof(buf),"a/c %s ppos:\n%s Y: %.0ff %.0fkn %.0fft/m Phase: %02d %s\nposList:\n", + snprintf(buf,sizeof(buf),"a/c %s\n%s <- turn\n%s Y: %.1fft %.0fkn %.0fft/m Phase: %02d %s\nposList:\n", labelInternal.c_str(), - ppos.dbgTxt().c_str(), terrainAlt, + turn.dbgTxt().c_str(), + ppos.dbgTxt().c_str(), GetTerrainAlt_ft(), GetSpeed_kt(), GetVSI_ft(), phase, FlightPhase2String(phase).c_str()); @@ -1050,11 +1310,11 @@ void LTAircraft::LabelUpdate() // MARK: LTAircraft Calculate present position // -// position heading to (usually posList[1], but ppos if ppos > posList[1]) +// position heading to (usually posList.back(), but ppos if ppos > posList.back()) const positionTy& LTAircraft::GetToPos() const { if ( posList.size() >= 2 ) - return ppos < posList[1] ? posList[1] : ppos; + return ppos < posList.back() ? posList.back() : ppos; else return ppos; } @@ -1068,7 +1328,17 @@ bool LTAircraft::OutOfPositions() const return bArtificalPos || (posList.size() < 2) || - (currCycle.simTime >= std::prev(posList.cend())->ts()); + (currCycle.simTime >= posList.back().ts()); +} + + +// is the aircraft on a rwy (on ground and at least on pos on rwy) +bool LTAircraft::IsOnRwy() const +{ + return IsOnGrnd() && + posList.size() >= 2 && + (posList.front().f.specialPos == SPOS_RWY || + posList[1].f.specialPos == SPOS_RWY); } @@ -1143,8 +1413,14 @@ bool LTAircraft::CalcPPos() // ppos we set posList[0] ('from') to ppos. Should be close anyway in normal // situations. (It's not if the simulation was halted while feeding live // data, then posList got completely outdated and ppos might jump beyond the entire list.) - if ( ppos < posList[1]) - posList[0] = ppos; + if ( ppos < posList[1]) { + // Save some flags needed for later calculations + ppos.f.specialPos = posList.front().f.specialPos; + ppos.f.bCutCorner = posList.front().f.bCutCorner; + ppos.edgeIdx = posList.front().edgeIdx; + // Then overwrite posList[0] + posList.front() = ppos; + } // flag: switched positions bPosSwitch = true; } @@ -1157,6 +1433,8 @@ bool LTAircraft::CalcPPos() positionTy& from = posList[0]; positionTy& to = posList[1]; double duration = to.ts() - from.ts(); + const double prevTs = phase == FPH_UNKNOWN ? currCycle.simTime : ppos.ts(); // Timestamp of previous cylce + const double prevHead = phase == FPH_UNKNOWN ? from.heading() : ppos.heading(); // previous heading (needed for roll calculation) #ifdef DEBUG std::string debFrom ( from.dbgTxt() ); std::string debTo ( to.dbgTxt() ); @@ -1169,6 +1447,8 @@ bool LTAircraft::CalcPPos() // some things only change when we work with new positions compared to last frame if ( bPosSwitch ) { // *** vector we will be flying now from 'from' to 'to': + from.normalize(); + to.normalize(); vec = from.between(to); LOG_ASSERT_FD(fd,!std::isnan(vec.speed) && !std::isnan(vec.vsi)); @@ -1182,36 +1462,10 @@ bool LTAircraft::CalcPPos() // avg of the current vector speed.SetSpeed(vec.speed); - - // point to some reasonable heading - heading.SetVal(ppos.heading() = from.heading()); } // *** ground status starts with that one of 'from' - ppos.onGrnd = from.onGrnd; - - // *** heading: make sure it is less than 360 (well...just normaize the entire positions) - from.normalize(); - to.normalize(); - - // start the turn from the initial heading to the vector heading - heading.defDuration = IsOnGrnd() ? mdl.TAXI_TURN_TIME : mdl.FLIGHT_TURN_TIME; - heading.moveQuickestToBy(NAN, - vec.dist > SIMILAR_POS_DIST ? // if vector long enough: - vec.angle : // turn to vector heading - HeadingAvg(from.heading(),to.heading()), // otherwise only turn to avg between from- and target-heading - NAN, (from.ts()+to.ts())/2, // by half the vector flight time - true); // start immediately - - // *** roll *** - // roll: we should currently have a bank angle, which should be - // returned back to level flight by the time the turn ends - if (!IsOnGrnd() && phase != FPH_FLARE && heading.isProgrammed()) - roll.moveQuickestToBy(NAN, 0.0, - NAN, heading.toTS(), false); - else - // on the ground or no heading change: keep wings level - roll.moveTo(0.0); + ppos.f.onGrnd = from.f.onGrnd; // *** Pitch *** @@ -1229,11 +1483,8 @@ bool LTAircraft::CalcPPos() // nearly no drag, so try calculating the flight path angle and use // it also as pitch if ( vsi > mdl.VSI_STABLE ) { - toPitch = vsi2deg(vec.speed, vec.vsi); - if (toPitch < mdl.PITCH_MIN) - toPitch = mdl.PITCH_MIN; - if (toPitch > mdl.PITCH_MAX) - toPitch = mdl.PITCH_MAX; + toPitch = clamp(vsi2deg(vec.speed, vec.vsi), + mdl.PITCH_MIN, mdl.PITCH_MAX); } // add some degrees in case flaps will be set @@ -1250,89 +1501,68 @@ bool LTAircraft::CalcPPos() // *** speed/acceleration/decelaration - // Only skip this in case of short final, i.e. if currently off ground + // Only skip acceleration control in case of short final, i.e. if currently off ground // but next pos is on the ground, because in that case the // next vector is the vector for roll-out on the ground // with significantly reduced speed due to breaking, that speed // is undesirable at touch-down; for short final we assume constant speed - bNeedNextVec = false; - if (!from.IsOnGnd() && to.IsOnGnd()) { - // short final, i.e. last vector before touch down - - // keep constant speed: - // avg ground speed between the two points [kt] + bNeedSpeed = from.IsOnGnd() || !to.IsOnGnd(); + if (!bNeedSpeed) speed.SetSpeed(vec.speed); - } else { - // will be calculated soon outside "if (bPosSwitch)" - // (need provision for case we don't get the data access lock right now) - bNeedNextVec = true; - } // output debug info on request if (dataRefs.GetDebugAcPos(key())) { LOG_MSG(logDEBUG,DBG_AC_SWITCH_POS,std::string(*this).c_str()); } - } + } // if ( bPosSwitch ) - // *** acceleration / decelartion *** + // Further computations make only sense if 'to' is still in the future + // (there seem to be case when this is not the case, and if only because the user pauses or changes time) + if ((bNeedSpeed || bNeedCCBezier) && to.ts() < currCycle.simTime) { + if (bNeedSpeed) speed.SetSpeed(vec.speed); + bNeedSpeed = bNeedCCBezier = false; + } - // Need next vector for speed determination? - if ( bNeedNextVec ) { - double toSpeed = NAN; - vectorTy nextVec; - - // makes only sense if 'to' is still in the future - // (there seem to be case when this is not the case, and if only because the user pauses or changes time) - if (to.ts() > currCycle.simTime) { - switch ( fd.TryGetVec(to.ts()+1, nextVec) ) { - case LTFlightData::TRY_SUCCESS: - // got the vector! - // Target speed: Weighted average of current and next vector - toSpeed = - (vec.speed * nextVec.dist + nextVec.speed * vec.dist) / - (vec.dist + nextVec.dist); - break; - case LTFlightData::TRY_NO_LOCK: - // try again next frame (bNeedNextVec stays true) - break; - case LTFlightData::TRY_NO_DATA: - // No data...but if we are running artifical roll-out then - // we just decelerate to stop - if (bArtificalPos && phase >= FPH_TOUCH_DOWN) { - toSpeed = 0; - break; - } - // else (not artifical roll-out) fall-through - [[fallthrough]]; - case LTFlightData::TRY_TECH_ERROR: - // no data or errors...well...then we just fly constant speed - bNeedNextVec = false; - break; - } - } else { - // 'to' is no longer future...then we just fly constant speed - bNeedNextVec = false; + // Need next position for speed or Bezier determination? + positionTy nextPos; + vectorTy nextVec; + if (bNeedSpeed || bNeedCCBezier) + { + switch ( fd.TryGetNextPos(to.ts()+1.0, nextPos) ) { + case LTFlightData::TRY_SUCCESS: + // got the next position! Compute vector to it + nextVec = to.between(nextPos); + break; + case LTFlightData::TRY_NO_LOCK: + // try again next frame (bNeedSpeed || bNeedBezier stays true) + break; + case LTFlightData::TRY_NO_DATA: + case LTFlightData::TRY_TECH_ERROR: + // no data or errors...well...then we just fly straight + if (bNeedSpeed) speed.SetSpeed(vec.speed); + bNeedSpeed = bNeedCCBezier = false; + break; } + } + + // *** acceleration / decelartion *** + if (bNeedSpeed && nextVec.isValid()) + { + // Target speed: Weighted average of current and next vector + const double toSpeed = + (vec.speed * nextVec.dist + nextVec.speed * vec.dist) / + (vec.dist + nextVec.dist); - // if we did come up with a target speed then start speed control + // initiate speed control (if speed valid, could be NAN if both distances ae zero) if (!std::isnan(toSpeed)) { - // initiate speed control speed.StartSpeedControl(speed.m_s(), toSpeed, vec.dist, from.ts(), to.ts(), this); - // don't need to calc speed again - bNeedNextVec = false; - } - // else if no next attempt: just fly constant avg speed then - else if (!bNeedNextVec) { - // output debug info on request - if (dataRefs.GetDebugAcPos(key())) { - LOG_MSG(logDEBUG,"CONSTANT SPEED due to no next vector available for %s",std::string(*this).c_str()); - } - speed.SetSpeed(vec.speed); } + // don't need to calc speed again + bNeedSpeed = false; } // *** The Factor *** @@ -1348,6 +1578,18 @@ bool LTAircraft::CalcPPos() f = (currCycle.simTime - from.ts()) / duration; } + // If we just switched positions then f starts over near 0.0. + // If we are at the same time running a Bezier across the -now- from point (cut-corner case), + // then we need to re-calibrate its internal factor f against the new f: + if (bPosSwitch) { + if (turn.isTsInbetween(currCycle.simTime)) + // Re-calibrate the curve so that f calculation continuous seamlessly + turn.ReCalibrate2ndHalf(); + else + // Otherwise clear the turn, just for cleanup + turn.Clear(); + } + // *** Artifical stop *** // limit on-the-ground activities, i.e. slow down to a stop if we don't know better @@ -1371,21 +1613,64 @@ bool LTAircraft::CalcPPos() posList.emplace_back(ppos); positionTy& stopPoint = posList.emplace_back(ppos.destPos(vecStop)); stopPoint.ts() = speed.getTargetTime(); + stopPoint.f.flightPhase = FPH_STOPPED_ON_RWY; bArtificalPos = true; // flag: we are working with an artifical position now + turn.Clear(); // (and certainly not with a Bezier curve) if (dataRefs.GetDebugAcPos(key())) { LOG_MSG(logDEBUG,DBG_INVENTED_STOP_POS,stopPoint.dbgTxt().c_str()); } } - // Now we apply the factor so that with time we move from 'from' to 'to'. - // Note that this calculation also works if we passed 'to' already - // (due to no newer 'to' available): we just keep going the same way. - // Here now valarray comes in handy as we can write the calculation - // with simple vector notation: - ppos.v = from.v * (1-f) + to.v * f; - // (this also computes values for heading, pitch, roll, which is a historic - // relict. We later decided to use MovingParam for those values.) + // Try getting our current position from the Bezier curve + if (!turn.GetPos(ppos, currCycle.simTime, f)) + { + // No Bezier curve currently active: + // Now we apply the factor so that with time we move from 'from' to 'to'. + // Note that this calculation also works if we passed 'to' already + // (due to no newer 'to' available): we just keep going the same way. + // Here now valarray comes in handy as we can write the calculation + // with simple vector notation: + ppos.v = from.v * (1-f) + to.v * f; + // (this also computes standard values for heading, pitch, roll.) + + // Heading: In first half turn towards vec.angle + // (or, if vec is too short, to average between from and to-heading) + const double maxTurnInCycle = (currCycle.simTime-prevTs)*360.0/(IsOnGrnd() ? mdl.TAXI_TURN_TIME : mdl.FLIGHT_TURN_TIME); + if (f < 0.5) { + const double targetHead = vec.dist >= SIMILAR_POS_DIST ? vec.angle : HeadingAvg(from.heading(), to.heading()); + const double headDiff = HeadingDiff(prevHead, targetHead); + if (std::abs(headDiff) > maxTurnInCycle) + ppos.heading() = prevHead + std::copysign(maxTurnInCycle, headDiff); + else + ppos.heading() = targetHead; + + } + // in second half, close to the end, turn to final heading + // (if this will not be taken care of by a cut-corner curve + else { + ppos.heading() = prevHead; // by default: don't change the heading + // If there is no turn upcoming + if (!turn.isActiveFuture(f)) { + const double headDiff = HeadingDiff(prevHead, to.heading()); + const double tToTurn = std::abs(headDiff)/360.0 * (IsOnGrnd() ? mdl.TAXI_TURN_TIME : mdl.FLIGHT_TURN_TIME); + // are we close enough to the end to start the turn? + if (currCycle.simTime + tToTurn + 0.1 >= to.ts()) { + if (std::abs(headDiff) > maxTurnInCycle) + ppos.heading() = prevHead + std::copysign(maxTurnInCycle, headDiff); + else + ppos.heading() = to.heading(); + } + } + } + } + + // calculate timestamp can be a bit off, especially when acceleration is in progress, + // overwrite with current value as of now + ppos.ts() = currCycle.simTime; + // Calculate roll based on heading change + CalcRoll(prevTs, prevHead); + // if we are runnig beyond 'to' we might become invalid (especially too low, too high) // catch that case...likely the a/c is to be removed due to outdated data // soon anyway, we just speed up things a bit here @@ -1398,31 +1683,55 @@ bool LTAircraft::CalcPPos() return false; } - // *** Attitude ***/ - - // half-way through prepare turning to end heading - if ( f > 0.5 && f < 1.0 && !dequal(heading.toVal(), to.heading()) ) { - heading.defDuration = IsOnGrnd() ? mdl.TAXI_TURN_TIME : mdl.FLIGHT_TURN_TIME; - heading.moveQuickestToBy(NAN, to.heading(), // target heading - NAN, to.ts(), // by target timestamp - false); // start as late as possible - // roll: start to roll when turn starts - if (!IsOnGrnd() && phase != FPH_FLARE && heading.isProgrammed()) - roll.moveTo(heading.isIncrease() ? roll.defMax : roll.defMin, - heading.fromTS()); + // *** Bezier Curves *** + // Now only (for the first time) we have a factor f and present position ppos + // For seemless start into a Bezier curve we needed that. + + // We apply Bezier if distance and turn amount are reasonable atfer a position switch + if (bPosSwitch && // just only switched positions? + !to.f.bCutCorner && // to position is not a cut-the-corner position + vec.dist > SIMILAR_POS_DIST && // reasonable leg distance and turn amount? + (std::abs(HeadingDiff(ppos.heading(), to.heading())) >= BEZIER_MIN_HEAD_DIFF || // begin-heading vs. end-heading + std::abs(HeadingDiff(ppos.heading(), vec.angle)) >= BEZIER_MIN_HEAD_DIFF)) // begin-heading vs. track-heading + { + // normal case, not cutting corners + // (this might re-define a cut-corner curve in progress, which is intentional) + turn.Define(f, ppos, to, vec); + } + + // Quadratic Bezier due to cut-corner case (half-way down the leg)? + if (bNeedCCBezier && nextVec.isValid()) + { + turn.Define(f, + from, vec.angle, + to, nextVec.angle, + nextPos, + to.IsOnGnd() ? mdl.TAXI_TURN_TIME : mdl.FLIGHT_TURN_TIME); + bNeedCCBezier = false; + } + // half-way through prepare a quadratic curve to cut the corner...if needed + else if (to.f.bCutCorner && // shall we cut the corner? + !bNeedCCBezier && // flag not already set? + f >= 0.5 && f < 1.0 && // half-way through + !turn.isTsBeforeEnd(currCycle.simTime) && // Bezier not already defined? + vec.dist > SIMILAR_POS_DIST && // reasonable leg distance and turn amount? + std::abs(HeadingDiff(ppos.heading(), to.heading())) >= BEZIER_MIN_HEAD_DIFF) + { + // set the flag to fetch the next leg. All the rest is done above + bNeedCCBezier = true; } + // *** Attitude *** + // if we ran out of positions we might have passed the final to-pos with a bank angle - // return that bank angle to 0 - if (f > 1.0) - roll.moveTo(0.0); + // return that bank angle to 0 and stop any heading change (which would be visible as a spin) + if (f > 1.0) { + ppos.roll() = 0.0; + ppos.heading() = vec.angle; + } - // current heading - ppos.heading() = heading.get(); // current pitch ppos.pitch() = pitch.get(); - // current roll (bank angle) - ppos.roll() = roll.get(); #ifdef DEBUG std::string debPpos ( ppos.dbgTxt() ); @@ -1440,7 +1749,7 @@ bool LTAircraft::CalcPPos() { // safety measure: // on the ground we are...on the ground, not moving vertically - ppos.SetAltFt(terrainAlt); + ppos.alt_m() = terrainAlt_m; vsi = 0; // but tires are rotating tireRpm.SetVal(std::min(TireRpm(GetSpeed_kt()), @@ -1471,7 +1780,7 @@ void LTAircraft::CalcFlightModel (const positionTy& /*from*/, const positionTy& // previous status bool bOnGrndPrev = bOnGrnd; - FlightPhase bFPhPrev = phase; + flightPhaseE bFPhPrev = phase; // present height (AGL in ft) double PHeight = GetPHeight_ft(); @@ -1486,7 +1795,7 @@ void LTAircraft::CalcFlightModel (const positionTy& /*from*/, const positionTy& // else: we could also be airborne, // so assume 'on the ground' if 'very' close to it, otherwise airborne bOnGrnd = PHeight <= MDL_CLOSE_TO_GND; - ppos.onGrnd = bOnGrnd ? positionTy::GND_ON : positionTy::GND_OFF; + ppos.f.onGrnd = bOnGrnd ? GND_ON : GND_OFF; } // Vertical Direction @@ -1513,11 +1822,14 @@ void LTAircraft::CalcFlightModel (const positionTy& /*from*/, const positionTy& phase = FPH_STOPPED_ON_RWY; } - // on the ground with high speed - if ( bOnGrnd && speed.kt() > mdl.MAX_TAXI_SPEED ) { - if ( bFPhPrev <= FPH_LIFT_OFF ) + // on the ground with high speed or on a runway + if ( bOnGrnd && (speed.kt() > mdl.MAX_TAXI_SPEED || IsOnRwy())) + { + if ( bFPhPrev <= FPH_LIFT_OFF ) // before take off phase = FPH_TO_ROLL; - else + else if (speed.isZero()) // stopped on rwy + phase = FPH_STOPPED_ON_RWY; + else // else: rolling out phase = FPH_ROLL_OUT; } @@ -1695,14 +2007,13 @@ void LTAircraft::CalcFlightModel (const positionTy& /*from*/, const positionTy& // flare if (ENTERED(FPH_FLARE)) { pitch.moveTo(mdl.PITCH_FLARE); // flare! - roll.moveTo(0); } // touch-down if (ENTERED(FPH_TOUCH_DOWN)) { gearDeflection.max(); // start main gear deflection spoilers.max(); // start deploying spoilers - ppos.onGrnd = positionTy::GND_ON; + ppos.f.onGrnd = GND_ON; pitch.moveTo(0); } @@ -1758,6 +2069,8 @@ void LTAircraft::CalcFlightModel (const positionTy& /*from*/, const positionTy& // *** Log *** + ppos.f.flightPhase = phase; + // if requested log a phase change if ( bFPhPrev != phase && dataRefs.GetDebugAcPos(key()) ) LOG_MSG(logDEBUG,DBG_AC_FLIGHT_PHASE, @@ -1767,6 +2080,37 @@ void LTAircraft::CalcFlightModel (const positionTy& /*from*/, const positionTy& } +// determine roll, based on a previous and a current heading +/// @details We assume that max bank angle (`mdl.ROLL_MAX_BANK`) is applied for a 1 minute curve +/// (ie. for a 360° turn in _half_ the `mdl.FLIGHT_TURN_TIME`). +/// If we are turning more slowly then we apply less bank angle. +void LTAircraft::CalcRoll (double _prevTs, double _prevHeading) +{ + // On the ground we should actually better be levelled... + if (IsOnGrnd()) { + ppos.roll() = 0.0; + return; + } + + // For the roll we assume that max bank angle is applied for a 1 minute curve + // (ie. for a 360° turn in _half_ the fullTurnTime). + // If we are turning more slowly then we apply less bank angle. + const double partOfCircle = HeadingDiff(_prevHeading, ppos.heading()) / 360.0; + const double diffTime = ppos.ts() - _prevTs; + const double timeFullCircle = diffTime / partOfCircle; // at current turn rate (if small then we turn _very_ fast!) + const double newRoll = (std::isnan(timeFullCircle) ? ppos.roll() : + std::abs(timeFullCircle) < mdl.FLIGHT_TURN_TIME/2 ? std::copysign(mdl.ROLL_MAX_BANK,timeFullCircle) : + mdl.ROLL_MAX_BANK * mdl.FLIGHT_TURN_TIME/2 / timeFullCircle); + // safeguard against to harsh roll rates: + if (std::abs(ppos.roll()-newRoll) > diffTime * mdl.ROLL_RATE) { + if (newRoll < ppos.roll()) ppos.roll() -= diffTime * mdl.ROLL_RATE; + else ppos.roll() += diffTime * mdl.ROLL_RATE; + } + else + ppos.roll() = newRoll; +} + + // determines terrain altitude via XPLM's Y Probe bool LTAircraft::YProbe () { @@ -1778,7 +2122,7 @@ bool LTAircraft::YProbe () return true; // This is terrain altitude right beneath us in [ft] - terrainAlt = YProbe_at_m(ppos, probeRef) / M_per_FT; + terrainAlt_m = fd.YProbe_at_m(ppos); if (currCycle.simTime >= probeNextTs) { @@ -1786,7 +2130,7 @@ bool LTAircraft::YProbe () static_assert(sizeof(PROBE_HEIGHT_LIM) == sizeof(PROBE_DELAY)); for ( size_t i=0; i < sizeof(PROBE_HEIGHT_LIM)/sizeof(PROBE_HEIGHT_LIM[0]); i++) { - if ( ppos.alt_ft() - terrainAlt >= PROBE_HEIGHT_LIM[i] ) { + if ( GetPHeight_ft() >= PROBE_HEIGHT_LIM[i] ) { probeNextTs = currCycle.simTime + PROBE_DELAY[i]; break; } @@ -2252,7 +2596,7 @@ XPMPPlaneCallbackResult LTAircraft::GetPlanePosition(XPMPPlanePosition_t* outPos NextCycle(cycle); #ifdef DEBUG - fd.bIsSelected = bIsSelected = (key() == dataRefs.GetSelectedAcKey()); + gSelAcCalc = fd.bIsSelected = bIsSelected = (key() == dataRefs.GetSelectedAcKey()); #endif // libxplanemp provides us with the multiplayer index, i.e. the plane's diff --git a/Src/LTApt.cpp b/Src/LTApt.cpp index 44ecd545..95cb778b 100644 --- a/Src/LTApt.cpp +++ b/Src/LTApt.cpp @@ -3,6 +3,11 @@ /// @details Scans `apt.dat` file for airport, runway, and taxiway information.\n /// Finds potential runway for an auto-land flight.\n /// Finds center lines on runways and taxiways to snap positions to. +/// @details Definitions:\n +/// Node: A position, where edges connect, relates to a 111-116 line in `apt.dat`\n +/// Edge: The connection of two nodes, relates to two consecutive 111-116 lines in `apt.dat`\n +/// Path: A set of connecting edges, relates to a gorup of 111-116 lines in `apt.dat`, headed by a 120 line\n +/// @see More information on reading from `apt.dat` is on [a separate page](@ref apt_dat). /// @author Birger Hoppe /// @copyright (c) 2020 Birger Hoppe /// @copyright Permission is hereby granted, free of charge, to any person obtaining a @@ -39,11 +44,6 @@ #define WARN_APTDAT_FAILED "Could not open ANY apt.dat file. No runway/taxiway info available to guide ground traffic." #define WARN_APTDAT_READ_FAIL "Could not completely read '%s'. Some runway/taxiway info will be missing to guide ground traffic: %s" -/// Minimum length of one segment in a taxi way (shorter ones are grouped together) -constexpr double APT_MIN_TAXI_SEGM_LEN_M = 10.0; -/// Square of Minimum length of one segment in a taxi way (shorter ones are grouped together) -constexpr double APT_MIN_TAXI_SEGM_LEN_M2 = (APT_MIN_TAXI_SEGM_LEN_M * APT_MIN_TAXI_SEGM_LEN_M); - /// This flag stops the file reading thread volatile bool bStopThread = false; @@ -53,10 +53,38 @@ class Apt; // MARK: Airports, Runways and Taxiways // -// TODO: Runways are just node/edges, identify by nodeTy -// List of edges to be sorted by (angle). -// It allows faster search for -// matching edges as we can limit the search range by angle. +/// Vector of indexes into another vector (e.g. indexes into the vector of edges, sorted by angle) +typedef std::vector vecIdxTy; + +/// @brief A position as read from apt.dat, stored temporarily, before turned into a TaxiNode +struct TaxiTmpPos : public ptTy { + size_t refCnt = 0; ///< number of usages in paths + + /// Constructor + TaxiTmpPos (double _lat, double _lon) : ptTy (_lon,_lat), refCnt(1) {} + + // access to latitude/longitude + double& lat() { return y; } /// latitude (stored in `y`) + double lat() const { return y; } /// latitude (stored in `y`) + double& lon() { return x; } /// longitude (stored in `y`) + double lon() const { return x; } /// longitude (stored in `y`) + + /// Increase ref count + size_t inc() { return ++refCnt; } + /// Is a joint in the sense that 2 or more _paths_ (not egdes) connect? + bool isJoint() const { return refCnt >= 2; } +}; + +/// A map sorted by lat. That makes it a little easier to find "nearby" positions +typedef std::map mapTaxiTmpPosTy; + +/// @brief Temporarily stores a path definition as read from apt.dat before post-processing +struct TaxiTmpPath { + std::list listPos; ///< list of positions making up the path +}; + +/// The list of paths read from apt.dat before post-processing +typedef std::list listTaxiTmpPathTy; /// @brief A node of a taxi way /// @details Depending on scenery and search range we might need to read and store @@ -66,35 +94,39 @@ class TaxiNode { public: double lat; ///< latitude double lon; ///< longitude - double x = NAN; ///< local coordinates, east axis - double z = NAN; ///< local coordinates, south axis + vecIdxTy vecEdges; ///< vector of edges connecting to this node, stored as indexes into Apt::vecTaxiEdges + // attributes needed by Dijkstra's shortest path algorithm + double pathLen = HUGE_VAL; ///< current best known path length to this node + size_t prevIdx = ULONG_MAX; ///< previous node on shortest path + bool bVisited = false; ///< has node been fully analyzed public: /// Default constructor leaves all empty TaxiNode () : lat(NAN), lon(NAN) {} /// Typical constructor requires a location TaxiNode (double _lat, double _lon) : lat(_lat), lon(_lon) {} - /// Destructor does not actually do anything, but is recommended in a good virtual class definition - virtual ~TaxiNode () {} + + /// Initialize Dijkstra attribues + void InitDijkstraAttr () + { pathLen = HUGE_VAL; prevIdx = ULONG_MAX; bVisited = false; } /// Is node valid in terms of geographic coordinates? bool HasGeoCoords () const { return !std::isnan(lat) && !std::isnan(lon); } - /// Is node valid in terms of local coordinates? - bool HasLocalCoords () const { return !std::isnan(x) && !std::isnan(z); } - /// @brief Update local coordinates - /// @param bForce `False` only calculate x/z if not yet known, `true` recalculate no matter what - /// @param _alt_m Default altitude to use if member TaxiNode::alt_m is not filled - virtual void LocalCoordsUpdate (bool bForce, double _alt_m) - { - double y; - if (bForce || std::isnan(x)) - XPLMWorldToLocal(lat, lon, - _alt_m, - &x, &y, &z); - } + /// Compares to given lat/long + bool CompEqualLatLon (double _lat, double _lon) const + { return dequal(lat, _lat) && dequal(lon, _lon); } + /// Comparison function for equality based on lat/lon static bool CompEqualLatLon (const TaxiNode& a, const TaxiNode& b) - { return dequal(a.lat, b.lat) && dequal(a.lon, b.lon); } + { return a.CompEqualLatLon(b.lat, b.lon); } + + /// Equality is based solely on geographic position + bool operator== (const TaxiNode& o) const + { return CompEqualLatLon(o.lat, o.lon); } + + /// Proximity is based on a given max distance + bool IsCloseTo (const TaxiNode& o, double _maxDist) + { return DistLatLonSqr(lat, lon, o.lat, o.lon) <= (_maxDist*_maxDist); } }; /// Vector of taxi nodes @@ -105,30 +137,15 @@ class RwyEndPt : public TaxiNode { public: std::string id; ///< rwy identifier, like "23" or "05R" double alt_m = NAN; ///< ground altitude in meter - double y = NAN; ///< local coordinates, vertical (up) axis + double heading = NAN; ///< rwy heading + public: /// Default constructor leaves all empty RwyEndPt () : TaxiNode () {} /// Typical constructor fills id and location - RwyEndPt (const std::string& _id, double _lat, double _lon) : - TaxiNode(_lat, _lon), id(_id) {} - /// Destructor does not actually do anything, but is recommended in a good virtual class definition - virtual ~RwyEndPt () {} - - /// @brief Update local coordinates, make use of stored altitude if available - /// @param bForce `False` only calculate x/z if not yet known, `true` recalculate no matter what - /// @param _alt_m Default altitude to use if member TaxiNode::alt_m is not filled - virtual void LocalCoordsUpdate (bool bForce, double _alt_m) - { - if (bForce || std::isnan(x)) - XPLMWorldToLocal(lat, lon, - std::isnan(alt_m) ? _alt_m : alt_m, - &x, &y, &z); - // We only keep the y value if it related to _our_ altitude - if (std::isnan(alt_m)) - y = NAN; - } - + RwyEndPt (const std::string& _id, double _lat, double _lon, double _heading) : + TaxiNode(_lat, _lon), id(_id), heading(_heading) {} + /// Compute altitude if not yet known void ComputeAlt (XPLMProbeRef& yProbe) { @@ -140,82 +157,152 @@ class RwyEndPt : public TaxiNode { /// Vector of runway endpoints typedef std::vector vecRwyEndPtTy; +/// Startup location (row code 1300) +class StartupLoc : public TaxiNode { +public: + std::string id; ///< all text after the coordinates, mostly for internal id purposes + double heading = NAN; ///< heading the plane stands at this location + ptTy viaPos; ///< position via which to leave startup location, + +public: + /// Default constructor leaves all empty + StartupLoc () : TaxiNode () {} + /// Typical constructor fills id and location + StartupLoc (const std::string& _id, double _lat, double _lon, double _heading) : + TaxiNode(_lat, _lon), id(_id), heading(_heading) {} +}; + +/// Vector of startup locations +typedef std::vector vecStartupLocTy; + /// @brief An edge in the taxi / rwy network, connected two nodes /// @details TaxiEdge can only store _indexes_ into the vector of nodes, -/// which is Apt::vecTaxiNodes. tt cannot directly store pointers or references, +/// which is Apt::vecTaxiNodes. It cannot directly store pointers or references, /// as the memory location might change when the vector reorganizes due to /// additions.\n -/// This also means that some functions otherwise better suites here are now +/// This also means that some functions otherwise better suited here are now /// moved to Apt as only Apt has access to all vectors. class TaxiEdge { public: /// Taxiway or runway? - enum nodeTy { + enum edgeTy { UNKNOWN_WAY = 0, ///< edge is of undefined type RUN_WAY = 1, ///< edge is for runway TAXI_WAY, ///< edge is for taxiway + REMOVED_WAY, ///< edge has been removed during post-processing }; protected: - nodeTy type; ///< type of node (runway, taxiway) - size_t a = 0; ///< from node (index into vecTaxiNodes) - size_t b = 0; ///< to node (index into vecTaxiNodes) + edgeTy type = UNKNOWN_WAY; ///< type of node (runway, taxiway) + size_t a = UINT_MAX; ///< from node (index into vecTaxiNodes) + size_t b = UINT_MAX; ///< to node (index into vecTaxiNodes) public: - double angle; /// angle/heading from a to b - double dist_m; /// distance in meters between a and b + double angle; ///< angle/heading from a to b + double dist_m; ///< distance in meters between a and b public: /// Constructor - TaxiEdge (nodeTy _t, size_t _a, size_t _b, double _angle, double _dist_m) : + TaxiEdge (edgeTy _t, size_t _a, size_t _b, double _angle, double _dist_m) : type(_t), a(_a), b(_b), angle(_angle), dist_m(_dist_m) { - // Normalize edges for 0 <= angle < 180 + Normalize(); + } + + /// Normalize myself to 0 <= angle < 180 + void Normalize () + { if (angle >= 180.0) { std::swap(a,b); angle -= 180.0; } } - /// Special Constructor for comparison objects only - TaxiEdge (double _angle) : - type(TAXI_WAY), a(0), b(0), angle(_angle), dist_m(NAN) - {} + + /// a valid egde to be used? + bool isValid () const { return type == RUN_WAY || type == TAXI_WAY; } /// Return the node's type - nodeTy GetType () const { return type; } + edgeTy GetType () const { return type; } + + /// Equality is based on type and nodes + bool operator== (const TaxiEdge& o) const + { return type == o.type && a == o.a && b == o.b; } - // Poor man's polymorphism: rwy endpoints are stored at a different place - // than taxiway nodes. And we only store indexes as pointers are - // unreliabe. The following functions return the proper object. /// Return the a node, ie. the starting point of the edge const TaxiNode& GetA (const Apt& apt) const; /// Return the b node, ie. the ending point of the edge const TaxiNode& GetB (const Apt& apt) const; - /// Return the first runway endpoint of a runway - const RwyEndPt& GetRwyEP_A (const Apt& apt) const - { return dynamic_cast(GetA(apt)); } - /// Return the second runway endpoint of a runway - const RwyEndPt& GetRwyEP_B (const Apt& apt) const - { return dynamic_cast(GetB(apt)); } - - /// Comparison function for sorting and searching - static bool CompHeadLess (const TaxiEdge& a, const TaxiEdge& b) - { return a.angle < b.angle; } + + /// Return the angle, adjust in a way that it points away from node n (which must be either TaxiEdge::a or TaxiEdge::b) + double GetAngleFrom (size_t n) const { return n == a ? angle : angle + 180.0; } + /// Returns the edge's angle, which is closest to the given heading + double GetAngleByHead (double heading) const + { return std::abs(HeadingDiff(heading, angle)) < 90.0 ? angle : angle + 180.0; } + /// Return the taxi node, that is the "start" when heading in the given direction + const TaxiNode& startByHeading (const Apt& apt, double heading) const + { return std::abs(HeadingDiff(heading, angle)) < 90.0 ? GetA(apt) : GetB(apt); } + /// Return the taxi node, that is the "end" when heading in the given direction + const TaxiNode& endByHeading (const Apt& apt, double heading) const + { return std::abs(HeadingDiff(heading, angle)) < 90.0 ? GetB(apt) : GetA(apt); } + + size_t startNode() const { return a; } ///< index of start node + size_t endNode() const { return b; } ///< index of end node + size_t startByHeading (double heading) const ///< Return the index of that node that is the edge's start if if looking in given direction + { return std::abs(HeadingDiff(heading, angle)) < 90.0 ? a : b; } + size_t endByHeading (double heading) const ///< Return the index of that node that is the edge's end if if looking in given direction + { return std::abs(HeadingDiff(heading, angle)) < 90.0 ? b : a; } + + size_t otherNode(size_t n) const { return n == a ? b : a; } ///< returns the "other" node (`n` should be TaxiEdge::a or TaxiEdge::b) + + /// sets a new end node, usually when splitting edges + void SetEndNode (size_t _b, double _angle, double _dist_m) + { + b = _b; + angle = _angle; + dist_m = _dist_m; + Normalize(); + } + + /// Replaces on node with the other (but does not recalc angle/dist!) + void ReplaceNode (size_t oldIdxN, size_t newIdxN) + { + if (a == oldIdxN) + a = newIdxN; + else if (b == oldIdxN) + b = newIdxN; + // if this leads to both nodes being the same then we are removed + if (a == b) { + type = REMOVED_WAY; + angle = NAN; + dist_m = NAN; + } + } + }; /// Vector of taxi edges typedef std::vector vecTaxiEdgeTy; -/// List of const pointers to taxi edges (for search function results) -typedef std::list lstTaxiEdgeCPtrTy; /// Represents an airport as read from apt.dat class Apt { protected: - static XPLMProbeRef YProbe; ///< Y Probe for terrain altitude computation std::string id; ///< ICAO code or other unique id boundingBoxTy bounds; ///< bounding box around airport, calculated from rwy and taxiway extensions double alt_m = NAN; ///< the airport's altitude vecTaxiNodesTy vecTaxiNodes; ///< vector of taxi network nodes vecRwyEndPtTy vecRwyEndPts; ///< vector of runway endpoints vecTaxiEdgeTy vecTaxiEdges; ///< vector of taxi network edges, each connecting any two nodes + vecIdxTy vecTaxiEdgesIdxHead; ///< vector of indexes into Apt::vecTaxiEdges, sorted by TaxiEdge::angle + vecStartupLocTy vecStartupLocs; ///< vector of startup locations + + static vecTaxiNodesTy vecRwyNodes; ///< temporary storage for rwy ends (to add egdes for the rwy later) + static mapTaxiTmpPosTy mapPos; ///< temporary storage for positions while reading apt.dat + static listTaxiTmpPathTy listPaths; ///< temporary storage for paths while reading apt.dat + static vecIdxTy vecPathEnds; ///< temporary storage for path endpoints (idx into Apt::vecTaxiNodes) + static XPLMProbeRef YProbe; ///< Y Probe for terrain altitude computation + +#ifdef DEBUG +public: + vecTaxiNodesTy vecBezierHandles; +#endif public: /// Constructor expects an id @@ -227,11 +314,69 @@ class Apt { bool HasId () const { return !id.empty(); } /// Valid airport definition requires an id and some taxiways / runways - bool IsValid () const { return HasId() && HasTaxiWays() && HasRwyEndpoints(); } + bool IsValid () const { return HasId() && HasRwyEndpoints(); } + /// Temporary arrays filled, so we can created nodes/edges? + bool HasTempNodesEdges () const { return !mapPos.empty() && !listPaths.empty(); } /// Return a reasonable altitude...effectively one of the rwy ends' altitude double GetAlt_m () const { return alt_m; } + // --- MARK: Temporary data while reading apt.dat + + /// @brief Find a similar position in Apt::mapPos + /// @param _lat Latitude to search for + /// @param _lon Longitude to search for + /// @param _bDoInc Shall the reference counter of the found object be incremented? + /// @return Pointer to closest position, or `nullptr` if no position is deemed "similar" + TaxiTmpPos* GetSimilarTaxiTmpPos (double _lat, double _lon, bool _bDoInc) + { + constexpr double latDiff = Dist2Lat(APT_MAX_SIMILAR_NODE_DIST_M); + const double lonDiff = Dist2Lon(APT_MAX_SIMILAR_NODE_DIST_M, _lat); + + // mapPos is sorted by latitude, so we can restrict our search + // to the matching latitude range + double bestDist2 = HUGE_VAL; + mapTaxiTmpPosTy::iterator bestIter = mapPos.end(); + for (mapTaxiTmpPosTy::iterator lIter = mapPos.lower_bound(_lat - latDiff); + lIter != mapPos.end() && lIter->second.lat() <= _lat + latDiff; + ++lIter) + { + // checking also for matching longitude range + if (std::abs(lIter->second.lat() - _lat) < latDiff && + std::abs(lIter->second.lon() - _lon) < lonDiff) { + // find shortest distance + const double dist2 = DistLatLonSqr(_lat, _lon, lIter->second.lat(), lIter->second.lon()); + if (dist2 < bestDist2) { + bestDist2 = dist2; + bestIter = lIter; + } + } + } + + // Found something? + if (bestIter != mapPos.end()) { + if (_bDoInc) + bestIter->second.inc(); + return &(bestIter->second); + } + + // Found nothing + return nullptr; + } + + /// Add a temporary position or increment reference counter of a "similar" one + void AddTaxiTmpPos (double _lat, double _lon) + { + if (!GetSimilarTaxiTmpPos(_lat, _lon, true)) + mapPos.emplace(_lat, TaxiTmpPos(_lat,_lon)); + } + + /// Moves the edge to the temporary storage + void AddTaxiTmpPath (TaxiTmpPath&& path) + { + listPaths.emplace_back(std::move(path)); + } + // --- MARK: Taxiways /// The vector of taxi network nodes @@ -242,55 +387,240 @@ class Apt { /// Any taxiways/runways defined? bool HasTaxiWays () const { return !vecTaxiEdges.empty(); } + /// Is given node connected to a rwy? + bool IsConnectedToRwy (size_t idxN) const + { + const TaxiNode& n = vecTaxiNodes[idxN]; + for (size_t idxE: n.vecEdges) + if (vecTaxiEdges[idxE].GetType() == TaxiEdge::RUN_WAY) + return true; + return false; + } + + /// Return the edge's idx, which connects the two given nodes, or `EDGE_UNAVAIL` + size_t GetEdgeBetweenNodes (size_t idxA, size_t idxB) + { + const TaxiNode& a = vecTaxiNodes.at(idxA); + for (size_t idxE: a.vecEdges) { + const TaxiEdge& e = vecTaxiEdges[idxE]; + if (e.otherNode(idxA) == idxB) + return idxE; + } + return EDGE_UNAVAIL; + } + + /// return index of closest taxi node within a "close-by" distance (or ULONG_MAX if none close enough) + size_t GetSimilarTaxiNode (double _lat, double _lon, + size_t dontCombineWith = ULONG_MAX) const + { + constexpr double latDiff = Dist2Lat(APT_MAX_SIMILAR_NODE_DIST_M); + const double lonDiff = Dist2Lon(APT_MAX_SIMILAR_NODE_DIST_M, _lat); + + double bestDist2 = HUGE_VAL; + const vecTaxiNodesTy::const_iterator dontIter = + dontCombineWith == ULONG_MAX ? vecTaxiNodes.cend() : + std::next(vecTaxiNodes.cbegin(),dontCombineWith); + vecTaxiNodesTy::const_iterator bestIter = vecTaxiNodes.cend(); + for (auto iter = vecTaxiNodes.cbegin(); iter != vecTaxiNodes.cend(); ++iter) + { + // quick check: reasonable lat/lon range + if (iter != dontIter && // not the one node to skip + std::abs(iter->lat - _lat) < latDiff && + std::abs(iter->lon - _lon) < lonDiff) + { + // find shortest distance + const double dist2 = DistLatLonSqr(_lat, _lon, iter->lat, iter->lon); + if (dist2 < bestDist2) { + bestDist2 = dist2; + bestIter = iter; + } + } + } + + // Found something? + return (bestIter != vecTaxiNodes.cend() ? + std::distance(vecTaxiNodes.cbegin(), bestIter) : + ULONG_MAX); + } + /// @brief Add a new taxi network node /// @return Index of node in Apt::vecTaxiNodes - size_t AddTaxiNode (double lat, double lon) + size_t AddTaxiNode (double lat, double lon, + size_t dontCombineWith = ULONG_MAX, + bool bSearchForSimilar = true) { + // Is there a similar close-by node already? + if (bSearchForSimilar) { + const size_t idx = GetSimilarTaxiNode(lat, lon, dontCombineWith); + if (idx != ULONG_MAX) + return idx; + } + bounds.enlarge_pos(lat, lon); // Potentially expands the airport's boundary vecTaxiNodes.emplace_back(lat, lon); // Add the node to the back of the list return vecTaxiNodes.size()-1; // return the index } - /// @brief Add a new taxi network edge, which must connect 2 existing nodes - /// @return Successfully inserted, ie. found the 2 nodes? - bool AddTaxiEdge (size_t n1, size_t n2, double _dist = NAN) + /// @brief Add a new taxi network node at a given index position + void AddTaxiNodeFixed (double lat, double lon, size_t idx) { - // Indexes must be valid - if (n1 >= vecTaxiNodes.size() || - n2 >= vecTaxiNodes.size()) - { - LOG_MSG(logDEBUG, "apt.dat: Node %lu or &lu not found! Edge not added.", n1, n2); - return false; + // Potentially expands the airport's boundary + bounds.enlarge_pos(lat, lon); + + // Expected case: Just the next index + if (idx == vecTaxiNodes.size()) + vecTaxiNodes.emplace_back(lat, lon); + else { + // make sure the vector is large enough + if (idx > vecTaxiNodes.size()) + vecTaxiNodes.resize(idx+1); + // then assign the value + vecTaxiNodes[idx] = TaxiNode(lat,lon); } - - // Actual nodes must be valid - const TaxiNode& a = vecTaxiNodes[n1]; - const TaxiNode& b = vecTaxiNodes[n2]; + } + + /// @brief Add a new taxi network edge, which must connect 2 existing nodes + /// @return Index into Apt::vecTaxiEdges, or ULONG_MAX if unsuccessful + size_t AddTaxiEdge (size_t n1, size_t n2, + TaxiEdge::edgeTy _type = TaxiEdge::TAXI_WAY) + { + // Actual nodes must be valid, throws exception if not + TaxiNode& a = vecTaxiNodes.at(n1); + TaxiNode& b = vecTaxiNodes.at(n2); if (!a.HasGeoCoords() || !b.HasGeoCoords()) { - LOG_MSG(logDEBUG, "apt.dat: Node %lu or &lu invalid! Edge not added.", n1, n2); - return false; + LOG_MSG(logDEBUG, "apt.dat: Node %lu or %lu invalid! Edge not added.", n1, n2); + return ULONG_MAX; } // Add the edge - if (std::isnan(_dist)) - _dist = DistLatLon(a.lat, a.lon, b.lat, b.lon); - vecTaxiEdges.emplace_back(TaxiEdge::TAXI_WAY, n1, n2, + vecTaxiEdges.emplace_back(_type, n1, n2, CoordAngle(a.lat, a.lon, b.lat, b.lon), - _dist); - return true; + DistLatLon(a.lat, a.lon, b.lat, b.lon)); + + // Tell the nodes they've got a new connection + const size_t eIdx = vecTaxiEdges.size()-1; + a.vecEdges.push_back(eIdx); + b.vecEdges.push_back(eIdx); + + return eIdx; } - /// @brief Update local coordinate system values (taxi nodes and rwy ends) - /// @param bForce `true` recalc all values, `false` calc only missing values - void LocalCoordsUpdate (bool bForce) + /// Recalc heading and angle of a given edge + void RecalcTaxiEdge (size_t eIdx) { - for (TaxiNode& n: vecTaxiNodes) - n.LocalCoordsUpdate(bForce, alt_m); - for (RwyEndPt& re: vecRwyEndPts) - re.LocalCoordsUpdate(bForce, alt_m); + TaxiEdge& e = vecTaxiEdges.at(eIdx); + const TaxiNode& a = e.GetA(*this); + const TaxiNode& b = e.GetB(*this); + e.angle = CoordAngle(a.lat, a.lon, b.lat, b.lon); + e.dist_m = DistLatLon(a.lat, a.lon, b.lat, b.lon); + + // Did this change the orientation? Then we need to swap a<->b + e.Normalize(); + } + + /// Split an edge by inserting a given node + void SplitEdge (size_t eIdx, size_t insNode) + { + // 1. Remember the original target edge + TaxiEdge& e = vecTaxiEdges.at(eIdx); + if (insNode == e.startNode() || insNode == e.endNode()) + return; + size_t joinOrigB = e.endNode(); + TaxiNode& origB = vecTaxiNodes[joinOrigB]; + + // 2. Short-cut existing node at new joint + const TaxiNode& a = e.GetA(*this); + TaxiNode& b = vecTaxiNodes.at(insNode); + + const double newAngle = CoordAngle(a.lat, a.lon, b.lat, b.lon); + const double newDist = DistLatLon(a.lat, a.lon, b.lat, b.lon); + + // It could be that insNode is slightly _before_ a + if (HeadingDiff(newAngle, e.angle) > 90.0) + { + // then we connect them by inserting a completely new node before a + AddTaxiEdge(insNode, e.startNode(), e.GetType()); + } + // else it could be that it is slightly _beyond_ origB + else if (newDist > e.dist_m) + { + // then we connect them by inserting a completely new node after origB + AddTaxiEdge(joinOrigB, insNode, e.GetType()); + } + else + { + // Typical case of insNode between a and origB: + // We have e now end at insNode: + e.SetEndNode(insNode, newAngle, newDist); + + // Node insNode/b now got one more edge connection, origB currently one less + b.vecEdges.push_back(eIdx); + for (auto iter = origB.vecEdges.begin(); // std::remove(_if) should have done the job...but it simply didn't + iter != origB.vecEdges.end();) + { + if (*iter == eIdx) + iter = origB.vecEdges.erase(iter); + else + ++iter; + } + + // 3. Add new edge between insNode and joinOrigB + AddTaxiEdge(insNode, joinOrigB, e.GetType()); + } } + /// @brief Replace one node with the other. Afterwards, oldN is unused + /// @details All edges connect to `old` are connected to `new` instead. + /// This only changes the respective node of the attached edges. + void ReplaceNode (size_t oldIdxN, size_t newIdxN) + { + // All edges using oldN need to be changed to use newN instead + TaxiNode& oldN = vecTaxiNodes.at(oldIdxN); + TaxiNode& newN = vecTaxiNodes.at(newIdxN); + while (!oldN.vecEdges.empty()) + { + // The edge to work on + const size_t idxE = oldN.vecEdges.back(); + oldN.vecEdges.pop_back(); + TaxiEdge& e = vecTaxiEdges[idxE]; + + // Replace the node in the edge and recalculate the edge + e.ReplaceNode(oldIdxN, newIdxN); + if (e.isValid()) { + RecalcTaxiEdge(idxE); + // Add the edge to the new node + newN.vecEdges.push_back(idxE); + } else { + // no longer a valid edge, remove it from the node + for (auto i = newN.vecEdges.begin(); + i != newN.vecEdges.end();) + { + if (*i == idxE) + i = newN.vecEdges.erase(i); + else + ++i; + } + } + } + } + + /// @brief Fill the indirect vector, which sorts edges by heading + void SortTaxiEdges () + { + // We add all valid(!) edges to the sort array + vecTaxiEdgesIdxHead.clear(); + vecTaxiEdgesIdxHead.reserve(vecTaxiEdges.size()); + for (size_t eIdx = 0; eIdx < vecTaxiEdges.size(); ++eIdx) + if (vecTaxiEdges[eIdx].isValid()) + vecTaxiEdgesIdxHead.push_back(eIdx); + + // Now sort the index array by the angle of the linked edge + std::sort(vecTaxiEdgesIdxHead.begin(), + vecTaxiEdgesIdxHead.end(), + [&](size_t a, size_t b) + { return vecTaxiEdges[a].angle < vecTaxiEdges[b].angle; }); + } /// @brief Returns a list of taxiways matching a given heading range /// @param _headSearch The heading we search for and which the edge has to match @@ -300,8 +630,8 @@ class Apt { /// @return Anything found? Basically: `!vec.empty()` bool FindEdgesForHeading (double _headSearch, double _angleTolerance, - lstTaxiEdgeCPtrTy& lst, - TaxiEdge::nodeTy _restrictType = TaxiEdge::UNKNOWN_WAY) const + vecIdxTy& lst, + TaxiEdge::edgeTy _restrictType = TaxiEdge::UNKNOWN_WAY) const { // vecTaxiEdges is sorted by heading (see AddApt) // and TaxiEdge::heading is normalized to [0..180). @@ -336,18 +666,19 @@ class Apt { for (const std::pair& rngPair: vecRanges) { // within that heading range, add all matching edges - for (vecTaxiEdgeTy::const_iterator iter = std::lower_bound(vecTaxiEdges.cbegin(), - vecTaxiEdges.cend(), - TaxiEdge(rngPair.first), - TaxiEdge::CompHeadLess); - iter != vecTaxiEdges.cend() && iter->angle <= rngPair.second; + for (vecIdxTy::const_iterator iter = + std::lower_bound(vecTaxiEdgesIdxHead.cbegin(), + vecTaxiEdgesIdxHead.cend(), + rngPair.first, + [&](const size_t& idx, double _angle) + { return vecTaxiEdges[idx].angle < _angle; }); + iter != vecTaxiEdgesIdxHead.cend() && vecTaxiEdges[*iter].angle <= rngPair.second; ++iter) { // Check for type limitation, then add to `vec` - const TaxiEdge& e = *iter; if (_restrictType == TaxiEdge::UNKNOWN_WAY || - _restrictType == e.GetType()) - lst.push_back(&e); + _restrictType == vecTaxiEdges[*iter].GetType()) + lst.push_back(*iter); } } @@ -357,120 +688,876 @@ class Apt { /// @brief Find closest taxi edge matching the passed position including its heading - /// @param pos Search position, only nearby nodes with a similar heading are considered - /// @param[out] basePt Receives the coordinates of the base point in case of a match. Only lat and lon will be modified. + /// @details Calculations are done based on approximate distances between + /// geographic world coordinates, measured in meter. + /// The passed-in position is considered the (0|0) point, + /// while the nodes to be analyzed are converted to distances to this point + /// before passed on to the DistPointToLineSqr() function. + /// The resulting base point is then converted back to geo world coords. + /// @param _pos Search position, only nearby nodes with a similar heading are considered + /// @param[out] _basePt Receives the coordinates of the base point and the edge's index in case of a match. `positionTy::lat`, `lon`, `heading`, and `edgeIdx` will be modified. /// @param _maxDist_m Maximum distance in meters between `pos` and edge to be considered a match /// @param _angleTolerance Maximum difference between `pos.heading()` and TaxiEdge::angle to be considered a match + /// @param _angleToleranceExt Second priority tolerance, considered only if such a node is more than 5m closer than one that better fits angle + /// @param _vecSkipEIdx (optional) Do not return any of these edge /// @return Pointer to closest taxiway edge or `nullptr` if no match was found - const TaxiEdge* FindClosestEdge (const positionTy& pos, - positionTy& basePt, - int _maxDist_m, - double _angleTolerance = ART_EDGE_ANGLE_TOLERANCE) const + const TaxiEdge* FindClosestEdge (const positionTy& _pos, + positionTy& _basePt, + double _maxDist_m, + double _angleTolerance, + double _angleToleranceExt, + const vecIdxTy& _vecSkipEIdx = vecIdxTy()) const { const TaxiEdge* bestEdge = nullptr; - const TaxiNode* bestFrom = nullptr; - const TaxiNode* bestTo = nullptr; + size_t bestEdgeIdx = EDGE_UNKNOWN; + double best_from_x = NAN; + double best_from_y = NAN; + double best_to_x = NAN; + double best_to_y = NAN; + double bestPrioDist= NAN; distToLineTy bestDist; - bestDist.dist2 = (double)sqr(_maxDist_m); - // At maximum, we allow that the base of the shortest dist to edge is about GetFdSnapTaxiDist_m outside of line ends - const double maxDistBeyondLineEnd2 = (double)sqr(_maxDist_m);; - - // We calculate in local coordinates - double pt_x = NAN, pt_y = NAN, pt_z = NAN; - XPLMWorldToLocal(pos.lat(), pos.lon(), pos.alt_m(), - &pt_x, &pt_y, &pt_z); + // maxDist^2, used in comparisons + const double maxDist2 = sqr(_maxDist_m); + // This is what we add to the square distance for second prio match... + // ...it is not exactly (dist+5m)^2 = dist^2 + 2 * 5 * dist + 5 ^ 2 + // ...but as close as we can get when we want to avoid sqrt for performance reasons + constexpr double SCND_PRIO_ADD = 3 * ART_EDGE_ANGLE_EXT_DIST + ART_EDGE_ANGLE_EXT_DIST*ART_EDGE_ANGLE_EXT_DIST; + // Init as: Nothing found + _basePt.edgeIdx = EDGE_UNAVAIL; // Get a list of edges matching pos.heading() - lstTaxiEdgeCPtrTy lstEdges; - const double headSearch = HeadingNormalize(pos.heading()); - if (!FindEdgesForHeading(headSearch, - _angleTolerance, - lstEdges)) - return nullptr; + vecIdxTy lstEdges; + const double headSearch = HeadingNormalize(_pos.heading()); + if (_angleToleranceExt < 90.0) { // ...if there actually is a limiting heading tolerance + if (!FindEdgesForHeading(headSearch, + std::max(_angleTolerance, _angleToleranceExt), + lstEdges)) + return nullptr; + } - // Edges are normalized to angle of [0..180), - // do we fly the other way round? - const bool bHeadInverted = headSearch >= 180.0; - // Analyze the edges to find the closest edge - for (const TaxiEdge* e: lstEdges) + // Either use the limited list of edges matching a heading or just all edges + const vecIdxTy& edgesToSearch = lstEdges.empty() ? vecTaxiEdgesIdxHead : lstEdges; + for (size_t eIdx: edgesToSearch) { - // Fetch from/to nodes from the edge - const TaxiNode& from = bHeadInverted ? e->GetB(*this) : e->GetA(*this); - const TaxiNode& to = bHeadInverted ? e->GetA(*this) : e->GetB(*this); + // Skip edge if wanted so + if (std::any_of(_vecSkipEIdx.cbegin(), _vecSkipEIdx.cend(), + [eIdx](size_t _e){return eIdx == _e;})) + continue; + + // Skip edge if invalid + const TaxiEdge& e = vecTaxiEdges[eIdx]; + if (!e.isValid()) + continue; + + // Skip edge if pos must be on a rwy but edge is not a rwy + if (isRwyPhase(_pos.f.flightPhase) && + e.GetType() != TaxiEdge::RUN_WAY) + continue; - // Edges need to have local coordaintes for what comes next - if (!from.HasLocalCoords() || !to.HasLocalCoords()) - continue; // no match due to heading + // Fetch from/to nodes from the edge + const TaxiNode& from = e.startByHeading(*this, headSearch); + const TaxiNode& to = e.endByHeading(*this, headSearch); + const double edgeAngle = e.GetAngleByHead(headSearch); + + // Compute temporary "coordinates", relative to the search position + const double from_x = Lon2Dist(from.lon - _pos.lon(), _pos.lat()); // x is eastward + const double from_y = Lat2Dist(from.lat - _pos.lat()); // y is northward + const double to_x = Lon2Dist(to.lon - _pos.lon(), _pos.lat()); + const double to_y = Lat2Dist(to.lat - _pos.lat()); + // As a quick check: (0|0) must be in the bounding box of [from-to] extended by _maxDist_m to all sides + if (std::min(from_x, to_x) - _maxDist_m > 0.0 || // left + std::max(from_y, to_y) + _maxDist_m < 0.0 || // top + std::max(from_x, to_x) + _maxDist_m < 0.0 || // right + std::min(from_y, to_y) - _maxDist_m > 0.0) // bottom + continue; + // Distance to this edge distToLineTy dist; - DistPointToLineSqr(pt_x, pt_z, // plane's position (x is southward, z is eastward) - from.x, from.z, // edge's starting point - to.x, to.z, // edge's end point + DistPointToLineSqr(0.0, 0.0, // plane's position is now by definition in (0|0) + from_x, from_y, // edge's starting point + to_x, to_y, // edge's end point dist); - // If distance is farther then best we know: skip - if (dist.dist2 >= bestDist.dist2) + // If too far away, skip (this considers if base point is outside actual line) + double prioDist = dist.DistSqrPlusOuts(); + if (prioDist > maxDist2) + continue; + + // Distinguish between first prio angle match and second prio angle match + if (std::abs(HeadingDiff(edgeAngle, headSearch)) > _angleTolerance) { + // So this is a second prio match in terms of angle to the edge + // For runways, we require first prio! + if (e.GetType() == TaxiEdge::RUN_WAY) + continue; + + // For others, we consider this, but with higher calculated distance + prioDist += SCND_PRIO_ADD; + } + + // If priorized distance is farther than best we know: skip + if (prioDist >= bestPrioDist) continue; // If base of shortest path to point is too far outside actual line - if (dist.DistSqrOfBaseBeyondLine() > maxDistBeyondLineEnd2) + if (dist.DistSqrOfBaseBeyondLine() > maxDist2) continue; // We have a new best match! - bestEdge = e; - bestFrom = &from; - bestTo = &to; - bestDist = dist; + bestEdge = &e; + bestEdgeIdx = eIdx; + best_from_x = from_x; + best_from_y = from_y; + best_to_x = to_x; + best_to_y = to_y; + bestPrioDist = prioDist; + bestDist = dist; } // Nothing found? - if (!bestEdge || !bestFrom || !bestTo) + if (!bestEdge) return nullptr; // Compute base point on the line, // ie. the point on the line with shortest distance // to pos - DistResultToBaseLoc(bestFrom->x, bestFrom->z, // edge's starting point - bestTo->x, bestTo->z, // edge's end point + double base_x = NAN, base_y = NAN; + DistResultToBaseLoc(best_from_x, best_from_y, // edge's starting point + best_to_x, best_to_y, // edge's end point bestDist, - pt_x, pt_z); // base point's local coordinates - double lat = NAN, lon = NAN, alt = NAN; - XPLMLocalToWorld(pt_x, pt_y, pt_z, - &lat, &lon, &alt); + base_x, base_y); // base point's local coordinates + + // Now only convert back from our local pos-based coordinate system + // to geographic world coordinates + _basePt.lon() = _pos.lon() + (std::isnan(base_x) ? 0.0 : Dist2Lon(base_x, _pos.lat())); + _basePt.lat() = _pos.lat() + (std::isnan(base_y) ? 0.0 : Dist2Lat(base_y)); + _basePt.heading() = bestEdge->GetAngleByHead(_pos.heading()); + _basePt.f.bHeadFixed = true; // We want the plane to head exactly as the line does! + _basePt.f.specialPos = bestEdge->GetType() == TaxiEdge::RUN_WAY ? SPOS_RWY : SPOS_TAXI; + _basePt.edgeIdx = bestEdgeIdx; - basePt.lat() = lat; - basePt.lon() = lon; + // return the found egde return bestEdge; } + /// @brief Return the type of edge the given position is on + /// @param _pos Position to analyse + /// @param[out] _pIdxE Optionally receives the edge's index + TaxiEdge::edgeTy GetPosEdgeType (const positionTy& _pos, size_t* _pIdxE = nullptr) const + { + // Haven't yet analysed this position? + size_t idxE = _pos.edgeIdx; + if (idxE == EDGE_UNKNOWN) { + // Find the closest edge and update idxE + positionTy basePos = _pos; + FindClosestEdge(_pos, basePos, + dataRefs.GetFdSnapTaxiDist_m(), + ART_EDGE_ANGLE_TOLERANCE, + ART_EDGE_ANGLE_TOLERANCE_EXT); + idxE = basePos.edgeIdx; + } + + // return the edge's index if requested + if (_pIdxE) *_pIdxE = idxE; + + // position is analyzed, but no edge found? + if (idxE == EDGE_UNAVAIL) + return TaxiEdge::UNKNOWN_WAY; + + // return the edge's type + return vecTaxiEdges.at(idxE).GetType(); + } + + /// @brief Return the type of edge the given position is on + /// @param[in,out] _pos Position to analyse; will set positionTy.edgeIdx if yet unknown + TaxiEdge::edgeTy GetPosEdgeType (positionTy& _pos) const + { return GetPosEdgeType(_pos, &_pos.edgeIdx); } + + /// @brief Processes the temporary map/list of nodes/edges and transforms them to permanent ones, + /// @details thereby keeping joints (positions where edges meet) but streamlining other nodes, + /// so we don't add all nodes + void PostProcessPaths () + { + // -- 1. Identify joints and add them upfront to our network + for (const auto& p: mapPos) + if (p.second.isJoint()) + AddTaxiNode(p.second.lat(), p.second.lon(), ULONG_MAX, false); + // all joints added, so up to here it is all full of joints: + const size_t firstNonJoint = vecTaxiNodes.size(); + + // -- 2. Process the edges, thereby adding more nodes + for (const TaxiTmpPath& p: listPaths) + { + // The indexes to be used when adding the edge. + // idxA points to the (already added) first node, + // idxB to the just now added second node + size_t idxA=ULONG_MAX, idxB=ULONG_MAX; + + // The first node of the entire list is definitely used, add it already + const size_t idxA_First = + idxA = AddTaxiNode(p.listPos.front().lat(), + p.listPos.front().lon()); + // Remember this node as one of the path's endpoint + vecPathEnds.push_back(idxA); + + // The very last node will also be added later. + // Between these two: + // Combine adges til + // a) reaching a joint, or + // b) heading changes too much. + // Add the remainder to the airport's taxi network + double firstAngle = NAN; + int numSkipped = 0; // number of skipped nodes in a row + for (auto iEnd = p.listPos.cbegin(); + iEnd != std::prev(p.listPos.cend()); + ++iEnd) + { + const TaxiTmpPos& b = *iEnd; // last node that is confirmed to be part of the edge + const TaxiTmpPos& c = *std::next(iEnd); // next node, to be validated if still in the edge + double bcAngle = CoordAngle(b.lat(), b.lon(), c.lat(), c.lon()); + if (std::isnan(firstAngle)) { // new edge has just started, this is our reference angle + firstAngle = bcAngle; + numSkipped = 0; + } + else + { + // is b a joint? + idxB = GetSimilarTaxiNode(b.lat(), b.lon()); + if (idxB < firstNonJoint || + // so many nodes...there's a reason for them, isn't it? + numSkipped >= 4 || + // or has heading changed too much? + std::abs(HeadingDiff(firstAngle, bcAngle)) > APT_MAX_TAXI_SEGM_TURN) + { + // Add the edge to this node + if (idxB == ULONG_MAX) + idxB = AddTaxiNode(b.lat(), b.lon(), ULONG_MAX, false); + if (idxA != idxB) { + AddTaxiEdge(idxA, idxB); + // start a new edge, first segment will be b-c + idxA = idxB; + firstAngle = bcAngle; + numSkipped = 0; + } + } + else + ++numSkipped; + } + } + + // The last node of the list is also always to be added + idxB = AddTaxiNode(p.listPos.back().lat(), + p.listPos.back().lon(), + idxA_First); // never combine with very first node; this ensures that at least one edge will be added! + if (idxA != idxB) { + AddTaxiEdge(idxA, idxB); + // Remember this node as the path's other endpoint + vecPathEnds.push_back(idxB); + } + } + } + + + /// @brief For each path end, try connecting them to some edge (which might by a rwy) + /// @details This shall\n + /// a) connect runways to taxiways\n + /// b) taxiway joints (which don't happen to have a directly overlapping node) + void JoinPathEnds () + { + // We had added entpoints to vecPathsEnds in a random order. + // Let's reduce this list to a unique list of indexes + std::sort(vecPathEnds.begin(), vecPathEnds.end()); + auto lastPE = std::unique(vecPathEnds.begin(), vecPathEnds.end()); + vecPathEnds.erase(lastPE,vecPathEnds.end()); + + // Loop all path ends and see if they are in need of another connection + for (size_t idxN: vecPathEnds) + { + // The node we deal with + TaxiNode& n = vecTaxiNodes[idxN]; + + // The exclusion edge list: With these edges we don't want to join: + // 1. All our direct edges + vecIdxTy vecEdgeExclusions = n.vecEdges; + // Let's reduce this exclusion list to a unique list of indexes + std::sort(vecEdgeExclusions.begin(), vecEdgeExclusions.end()); + auto lastEExcl = std::unique(vecEdgeExclusions.begin(), vecEdgeExclusions.end()); + vecEdgeExclusions.erase(lastEExcl,vecEdgeExclusions.end()); + + // Try finding _another_ edge this one can connect to + positionTy pos(n.lat, n.lon, 0.0, NAN, vecTaxiEdges[n.vecEdges.front()].GetAngleFrom(idxN)); + const TaxiEdge* pJoinE = FindClosestEdge(pos, pos, + // larger distance allowed if I'm a single node, smaller only if I already have connections + n.vecEdges.size() <= 1 ? APT_JOIN_MAX_DIST_M : APT_MAX_SIMILAR_NODE_DIST_M, + APT_JOIN_ANGLE_TOLERANCE, + 90.0, // don't limit by heading...search all edges! + vecEdgeExclusions); + if (!pJoinE) + continue; + + if (std::isnan(pos.lat()) || std::isnan(pos.lon())) + continue; + + // We found just another taxi edge, which we combine: + // We'll now split that found edge by inserting the + // open node, which we move to the base position, + // so that it is exactly on the edge that we split. + // The "join" edge doesn't move. + const size_t joinIdxE = pos.edgeIdx; + + // Move the open node to the base location, ie. to the closest + // point on the pJoinE edge (which is at max APT_JOIN_MAX_DIST_M meters away) + n.lat = pos.lat(); + n.lon = pos.lon(); + + // Along this edge, there could be nodes which are more or less + // equal to our node n. Eg., this happens with taxiways, + // which leave runwas in opposite directions: + // Both taxiways (left/right) have an open end on the rwy, + // one to the left, one to the right of the rwy centerline. + // The algorithm will find one of them first and merge with + // the rwy. Once we find the other side we should combine that + // node now with the already merged node, so that both taxiways + // join with the rwy in one single joint node. + size_t nearIdxN = ULONG_MAX; + if (n.IsCloseTo(vecTaxiNodes[pJoinE->startNode()], APT_MAX_SIMILAR_NODE_DIST_M)) + nearIdxN = pJoinE->startNode(); + else if (n.IsCloseTo(vecTaxiNodes[pJoinE->endNode()], APT_MAX_SIMILAR_NODE_DIST_M)) + nearIdxN = pJoinE->endNode(); + + // One of the nodes is indeed nearby? + if (nearIdxN < ULONG_MAX) { + ReplaceNode(idxN, nearIdxN); + } + // Not nearby: + else { + // Moving n has slightly changed all edges of n, recalc distance and angle + for (size_t idxEE: n.vecEdges) + RecalcTaxiEdge(idxEE); + // Split pJoinE at the base position, now n (whose index is idxN) + SplitEdge(joinIdxE, idxN); + } + + // To ensure FindClosestEdge works we need to sort + SortTaxiEdges(); + #ifdef DEBUG + LOG_ASSERT(ValidateNodesEdges()); + #endif + } // for all path ends (which are nodes) + } + + /// @brief Find shortest path in taxi network with a maximum length between 2 nodes + /// @see https://en.wikipedia.org/wiki/Dijkstra's_algorithm + /// @param _startN Start node in Apt::vecTaxiNodes + /// @param _endN End node in Apt::vecTaxiNodes + /// @param _maxLen Maximum path length, no longer paths will be pursued or returned + /// @param _generalHeading General heading the plane shall follow, typically heading from start to end node. Affects which turns are allowed along that returned path. + /// @param _headingAtEnd The expected heading at the end node, affects how the final leg to the endN may be picked + /// @return List of node indexes _including_ `_end` and `_start` in _reverse_ order, + /// or an empty list if no path of suitable length was found + vecIdxTy ShortestPath (size_t _startN, size_t _endN, double _maxLen, + double _generalHeading, + double _headingAtEnd) + { + // Sanity check: _start and _end should differ + if (_startN == _endN) + return vecIdxTy(); + + + // Initialize the Dijkstra values in the nodes array + for (TaxiNode& n: vecTaxiNodes) + n.InitDijkstraAttr(); + + // This array stores nodes we need to visit + // (have an initial distance, but aren't fully visited yet) + vecIdxTy vecVisit; + + // The start place is the given taxiway node + TaxiNode& startN = vecTaxiNodes.at(_startN); + const TaxiNode& endN = vecTaxiNodes.at(_endN); + startN.pathLen = 0.0; + startN.prevIdx = ULONG_MAX-1; // we use "ULONG_MAX-1" for saying "is a start node" + vecVisit.push_back(_startN); + + // General heading between start and end (reversed) + // defines first heading (how we leave _startN) and + // how far the plane is allowed to turn + const double _headReverse = std::fmod(_generalHeading + 180.0, 360.0); + + // outer loop controls currently visited node and checks if end already found + while (!vecVisit.empty() && endN.prevIdx == ULONG_MAX) + { + // fetch node with shortest yet known distance + // (this isn't awfully efficient, but keeping a separate map or prio-queue + // sorted while updating nodes in the next loop + // is not simple either. I expect vecVisit to stay short + // due to cut-off at _maxLen, so I've decided this way:) + vecIdxTy::iterator shortestIter = vecVisit.begin(); + double shortestDist = vecTaxiNodes[*shortestIter].pathLen; + for (vecIdxTy::iterator i = std::next(shortestIter); + i != vecVisit.end(); ++i) + { + if (vecTaxiNodes[*i].pathLen < shortestDist) { + shortestIter = i; + shortestDist = vecTaxiNodes[*i].pathLen; + } + } + const size_t shortestNIdx = *shortestIter; + TaxiNode& shortestN = vecTaxiNodes[shortestNIdx]; + + // To avoid too sharp corners we need to know the angle by which we reach this shortest node + const size_t idxEdgeToShortestN = + shortestN.prevIdx >= ULONG_MAX-1 ? EDGE_UNKNOWN : + GetEdgeBetweenNodes(shortestN.prevIdx, shortestNIdx); + // start heading for when leaving first node, otherwise heading between previous and current node + const double angleToShortestN = + idxEdgeToShortestN == EDGE_UNKNOWN ? _generalHeading : + vecTaxiEdges[idxEdgeToShortestN].GetAngleFrom(shortestN.prevIdx); + + // This one is now already counted as "visited" so no more updates to its pathLen! + shortestN.bVisited = true; + vecVisit.erase(shortestIter); + + // Update all connected nodes with best possible distance + for (size_t eIdx: shortestN.vecEdges) + { + const TaxiEdge& e = vecTaxiEdges[eIdx]; + if (!e.isValid()) continue; + + size_t updNIdx = e.otherNode(shortestNIdx); + TaxiNode& updN = vecTaxiNodes[updNIdx]; + + // if aleady visited then no need to re-assess + if (updN.bVisited) + continue; + + // Don't allow turns of more than 100°, + // ie. edge not valid if it would turn more than that + const double eAngle = e.GetAngleFrom(shortestNIdx); + if (!std::isnan(angleToShortestN) && + std::abs(HeadingDiff(angleToShortestN, + eAngle)) > APT_MAX_PATH_TURN) + continue; + + // Don't allow to turn backwards compared to initial heading + if (std::abs(HeadingDiff(_headReverse, eAngle)) < ART_EDGE_ANGLE_TOLERANCE) + continue; + + // If the node being analyzed is the end node, then we also + // need to verify if the heading from end node to actual a/c position + // would not again cause too sharp a turn: + if (updNIdx == _endN && !std::isnan(_headingAtEnd) && + std::abs(HeadingDiff(_headingAtEnd, eAngle)) > APT_MAX_PATH_TURN) + continue; + + // Calculate the yet known best distance to this node + const double lenToUpd = shortestDist + e.dist_m; + if (lenToUpd > _maxLen || // too far out? + updN.pathLen <= lenToUpd) // node has a faster path already + continue; + + // Update this node with new best values + updN.pathLen = lenToUpd; // best new known distance + updN.prevIdx = shortestNIdx; // predecessor to achieve that distance + + // Have we reached the wanted end node? + if (updNIdx == _endN) + break; + + // this node is now ready to be visited + push_back_unique(vecVisit, updNIdx); + } + } + + // Found nothing? -> return empty list + if (endN.prevIdx == ULONG_MAX) + return vecIdxTy(); + + // put together the nodes from _start through _end in the right order + vecVisit.clear(); + for (size_t nIdx = _endN; + nIdx < ULONG_MAX-1; // until nIdx becomes invalid + nIdx = vecTaxiNodes[nIdx].prevIdx) // move on to _previous_ node on shortest path + { + LOG_ASSERT(nIdx < vecTaxiNodes.size()); + vecVisit.push_back(nIdx); + } + return vecVisit; + } + /// @brief Find best matching taxi edge based on passed-in position/heading info - bool SnapToTaxiway (positionTy& pos, bool bLogging) const + bool SnapToTaxiway (LTFlightData& fd, dequePositionTy::iterator& posIter, + bool bInsertTaxiTurns) { + // The position we consider and that we potentially change + // by snapping to a taxiway + positionTy& pos = *posIter; const double old_lat = pos.lat(), old_lon = pos.lon(); + // 1. --- Try to match pos with a startup location + double distStartup = NAN; + const StartupLoc* pStartLoc = FindStartupLoc(pos, + dataRefs.GetFdSnapTaxiDist_m() * 2, + &distStartup); + if (pStartLoc) + { + // pos is close to a startup location, so we definitely set + // and keep the startup location's heading + pos.heading() = pStartLoc->heading; + pos.f.bHeadFixed = true; + } + + // 2. --- Find any edge --- // Find the closest edge and right away move pos there - const TaxiEdge* pEdge = FindClosestEdge(pos, pos, dataRefs.GetFdSnapTaxiDist_m()); - if (pEdge) { - // found a match, say hurray - if (bLogging) { - LOG_MSG(logDEBUG, "Snapped to taxiway from (%7.4f, %7.4f) to (%7.4f, %7.4f)", - old_lat, old_lon, pos.lat(), pos.lon()); + const TaxiEdge* pEdge = FindClosestEdge(pos, pos, + dataRefs.GetFdSnapTaxiDist_m(), + ART_EDGE_ANGLE_TOLERANCE, + ART_EDGE_ANGLE_TOLERANCE_EXT); + + // specialPos might have been set to SPOS_TAXI, + // but for startup positions we do want it to be: + if (pStartLoc) + pos.f.specialPos = SPOS_STARTUP; + + // Nothing found? + if (!pEdge) { + + // No edge found, but a startup location? + if (pStartLoc) + { + // Then we should move onto the path leading away from the location + ProjectPosOnStartupPath(pos, *pStartLoc); + if (dataRefs.GetDebugAcPos(fd.key())) + LOG_MSG(logDEBUG, "Snapped to startup location path from (%.5f, %.5f) to (%.5f, %.5f)", + old_lat, old_lon, pos.lat(), pos.lon()); + return true; + } + + // --- Test for Black Hole Horizon problem: + // When planes briefly wait on taxiways then it can happen + // that the previous pos was close enough to a taxiway and + // it snapped while this new pos was just a bit too far + // away and did not snap, but otherwise didn't move ahead much + // either. The snap distance serves as the Black Hole Horizon: + // While some positions are snapped and move up to that distance, + // others just outside don't move. They appear to be + // about 90° away from that previous pos: The planes turns and + // rolls there, ending up with the nose pointing away from + // the taxiway. + // We try to catch that here: If we are just outside snapping distance + // and turned about 90° away from previous pos's edge, + // then we snap onto previous pos nonetheless. + + // Is there a previous position on the ground with an assigned edge? + if (!fd.hasAc() && posIter == fd.posDeque.begin()) + return false; + const positionTy& prevPos = (posIter == fd.posDeque.begin() ? + fd.pAc->GetToPos() : *(std::prev(posIter))); + if (!prevPos.HasTaxiEdge()) + return false; + + // There is. Relative to current position...where? + vectorTy vec = prevPos.between(pos); + // Too far out? + if (vec.dist > dataRefs.GetFdSnapTaxiDist_m() * 2.0) + return false; + + // The edge that previous pos is on. Angle pretty much like 90°? + const TaxiEdge& ePrev = vecTaxiEdges.at(prevPos.edgeIdx); + const double headDiff = std::abs(HeadingDiff(vec.angle, ePrev.angle)); + if (std::abs(headDiff- 90.0) < APT_RECT_ANGLE_TOLERANCE || + std::abs(headDiff-270.0) < APT_RECT_ANGLE_TOLERANCE) + { + // Angle of vector between prevPos and pos is about rectangular + // compared to prevPos's edge --> snap pos on PrevPos + pos.lat() = prevPos.lat(); + pos.lon() = prevPos.lon(); + pos.alt_m() = prevPos.alt_m(); + pos.heading() = ePrev.GetAngleByHead(prevPos.heading()); + pos.edgeIdx = prevPos.edgeIdx; + pos.f = prevPos.f; + if (dataRefs.GetDebugAcPos(fd.key())) + LOG_MSG(logDEBUG, "Snapped to taxiway from (%.5f, %.5f) to (%.5f, %.5f) based on previously snapped position", + old_lat, old_lon, pos.lat(), pos.lon()); + return true; } - // this is now an artificially moved position, don't touch any further - // (we don't makr positions on a runway...it might hamper take off prediction and acceleration if we do, - // downside is that we will pass in this position again and again...) - if (pEdge->GetType() != TaxiEdge::RUN_WAY) - pos.flightPhase = LTAPIAircraft::FPH_TAXI; + return false; + } + + // --- found a match, say hurray --- + if (dataRefs.GetDebugAcPos(fd.key())) { + LOG_MSG(logDEBUG, "Snapped to taxiway from (%.5f, %.5f) to (%.5f, %.5f)", + old_lat, old_lon, pos.lat(), pos.lon()); + } + + // this is now an artificially moved position, don't touch any further + // (we don't mark positions on a runway yet...would be take off or rollout to be distinguished) + if (pEdge->GetType() != TaxiEdge::RUN_WAY) + pos.f.flightPhase = FPH_TAXI; + + // --- Insert shortest path along taxiways --- + // if wanted, that is + if (!bInsertTaxiTurns) + return true; + + // We either need an aircraft (with a current `to` position) + // or a predecessor in the fd.posDeque to come up with a path + if (!fd.hasAc() && posIter == fd.posDeque.begin()) return true; + + // The previous pos before *posIter: + // Either the predecessor in fd.posDeque, if it exists, + // or the plane's `to` position + const positionTy& prevPos = (posIter == fd.posDeque.begin() ? + fd.pAc->GetToPos() : *(std::prev(posIter))); + + // That pos must be on an edge, too + if (!prevPos.HasTaxiEdge() || + // That previous edge isn't by chance the same we just now found? Then the shortest path is to go straight... + (pos.edgeIdx == prevPos.edgeIdx) || + // Also, we don't search for path between any two rwy nodes + (GetPosEdgeType(pos) == TaxiEdge::RUN_WAY && GetPosEdgeType(prevPos) == TaxiEdge::RUN_WAY)) + return true; + + // - relevant nodes: usually the ones away from (prev)pos, + // but if we are very close to a joint node, + // then we pick that joint node. This increased the + // number of possible paths. In case of two edges + // closeby it is well possible that `FindClosestEdge` + // picked the "wrong" one. Then this notion to use + // an adjacent joint will often rectify this error. + + // previous edge's relevant node + bool bSkipStart = false; + const TaxiEdge& prevE = vecTaxiEdges[prevPos.edgeIdx]; + size_t prevErelN = prevE.endByHeading(prevPos.heading()); + { + const TaxiNode& othN = vecTaxiNodes[prevE.otherNode(prevErelN)]; + if (DistLatLonSqr(othN.lat, othN.lon, prevPos.lat(), prevPos.lon()) <= sqr(2*APT_MAX_SIMILAR_NODE_DIST_M)) { + prevErelN = prevE.otherNode(prevErelN); + bSkipStart = true; // this node is now _before_ prevPos, don't add that to the deque! + } } - // nothing found - return false; + // current edge's relevant node + bool bSkipEnd = false; + size_t currEstartN = pEdge->startByHeading(pos.heading()); + { + const TaxiNode& othN = vecTaxiNodes[pEdge->otherNode(currEstartN)]; + if (DistLatLonSqr(othN.lat, othN.lon, pos.lat(), pos.lon()) <= sqr(2*APT_MAX_SIMILAR_NODE_DIST_M)) { + currEstartN = pEdge->otherNode(currEstartN); + bSkipEnd = true; // this node is now _beyond_ pos, don't add that to the deque! + } + } + + // for the maximum allowed path length let's consider taxiing speed, + // but allow 3x taxiing speed if beginning leg is still on a rwy + // (consider high-speed exits!). + const LTAircraft::FlightModel& mdl = fd.pAc ? fd.pAc->mdl : + LTAircraft::FlightModel::FindFlightModel(fd.statData.acTypeIcao); + double maxLen = (pos.ts() - prevPos.ts()) * mdl.MAX_TAXI_SPEED; + if (prevE.GetType() == TaxiEdge::RUN_WAY) + maxLen *= 3.0; + + // let's try finding a shortest path + vecIdxTy vecPath = ShortestPath(prevErelN, + currEstartN, + maxLen, + prevPos.angle(pos), + pEdge->angle); + + // Some path found? + if (vecPath.size() >= 2) + { + // length of total path as returned (this excludes the distance from prevPos to start, and from end to pos) + // Add the end leg, ie. from end of path to pos + const TaxiNode& endN = vecTaxiNodes[vecPath.front()]; // end of path + const double pathLen = vecTaxiNodes[currEstartN].pathLen + + DistLatLon(endN.lat, endN.lon, pos.lat(), pos.lon()); + + // Adjust the startTS (as prevPos is not equal to start of path, + // we need time to travel that short distance) + const TaxiNode& startN = vecTaxiNodes[vecPath.back()]; // start of path + const double prevToStartDist = DistLatLon(prevPos.lat(), prevPos.lon(), startN.lat, startN.lon); + const double speed = (prevToStartDist + pathLen) / (pos.ts() - prevPos.ts()); + // Allow for some time to go from prevPos to start of path: + const double startTS = prevPos.ts() + prevToStartDist / speed; + + // the time we have from start of the path to pos + const double pathTime = pos.ts() - startTS; + + // path is returned in reverse order, so work on it reversely + size_t prevIdxN = ULONG_MAX; + for (vecIdxTy::const_reverse_iterator iter = vecPath.crbegin(); + iter != vecPath.crend(); + ++iter) + { + // Skip artificially moved positions + if ((bSkipStart && iter == vecPath.crbegin()) || + (bSkipEnd && std::next(iter) == vecPath.crend())) + continue; + + // create a proper position and insert it into fd's posDeque + const TaxiNode& n = vecTaxiNodes[*iter]; + positionTy insPos (n.lat, n.lon, NAN, // lat, lon, altitude + startTS + pathTime * n.pathLen / pathLen, + NAN, // heading will be populated later + 0.0, 0.0, // on the ground no pitch/roll + GND_ON, + UNIT_WORLD, + UNIT_DEG, + FPH_TAXI); + + // Which edge is this pos on? (Or, as it is a node: one of the edges it is connected to) + if (prevIdxN == ULONG_MAX) + insPos.edgeIdx = prevPos.edgeIdx; + else + insPos.edgeIdx = GetEdgeBetweenNodes(*iter, prevIdxN); + prevIdxN = *iter; + + // insPos is now either on a taxiway or a runway + insPos.f.specialPos = + vecTaxiEdges[insPos.edgeIdx].GetType() == TaxiEdge::RUN_WAY ? + SPOS_RWY : SPOS_TAXI; + + // Insert before the position that was passed in + posIter = fd.posDeque.insert(posIter, insPos); // posIter now points to inserted element + ++posIter; // posIter points to originally passed in element again + } + + if (dataRefs.GetDebugAcPos(fd.key())) { + LOG_MSG(logDEBUG, "Inserted %lu taxiway nodes", + vecPath.size() - (size_t)bSkipStart - (size_t)bSkipEnd); + } + } // if found a shortest path + // Not found a shortest path -> try finding edges' intersection + else + { + // Let's try finding the intersection point of the 2 edges we are on + const TaxiNode& currA = pEdge->GetA(*this); + const TaxiNode& currB = pEdge->GetB(*this); + const TaxiNode& prevA = prevE.GetA(*this); + const TaxiNode& prevB = prevE.GetB(*this); + positionTy intersec = + CoordIntersect({prevA.lon, prevA.lat}, {prevB.lon, prevB.lat}, + {currA.lon, currA.lat}, {currB.lon, currB.lat}); + intersec.pitch() = 0.0; + intersec.roll() = 0.0; + intersec.f.onGrnd = GND_ON; + intersec.f.flightPhase = FPH_TAXI; + intersec.f.bCutCorner = true; // the corner of this position can be cut short + + // It is essential that the intersection is in front (rather than behind) + vectorTy vecPrevInters = prevPos.between(intersec); + if (std::abs(HeadingDiff(prevPos.heading(),vecPrevInters.angle)) < 90.0) + { + vectorTy vecIntersCurr = intersec.between(pos); + + // turning angle at intersection must not be too sharp + if (std::abs(HeadingDiff(vecPrevInters.angle, vecIntersCurr.angle)) <= APT_MAX_PATH_TURN) + { + double avgSpeed = (vecPrevInters.dist + vecIntersCurr.dist) / (pos.ts() - prevPos.ts()); + + // Distance needs to be manageable, which means: + // On the ground max MAX_TAXI_SPEED, + // when turning off a rwy then the taxi part is restricted to MAX_TAXI_SPEED + if (prevE.GetType() == TaxiEdge::RUN_WAY && + avgSpeed > mdl.MAX_TAXI_SPEED) + { + intersec.ts() = pos.ts() - vecIntersCurr.dist/mdl.MAX_TAXI_SPEED; + // intersection moves too close (in terms of time) to previous position? + if (intersec.ts() < prevPos.ts() + SIMILAR_TS_INTVL) + intersec.ts() = NAN; // then we don't use it + } + else if (avgSpeed <= mdl.MAX_TAXI_SPEED) + // define ts so that we run constant speed from prevPos via intersec to pos + intersec.ts() = prevPos.ts() + (pos.ts()-prevPos.ts()) * vecPrevInters.dist / (vecPrevInters.dist+vecIntersCurr.dist); + + // Did we find a valid timestamp? -> Add the pos into posDeque + if (!std::isnan(intersec.ts())) { + posIter = fd.posDeque.insert(posIter, intersec);// posIter now points to inserted element + ++posIter; // posIter points to originally passed in element again + if (dataRefs.GetDebugAcPos(fd.key())) + LOG_MSG(logDEBUG, "Inserted artificial intersection node"); + } + } + } + } + + // snapping successful + return true; } +#ifdef DEBUG + /// Validates if back references of edges to nodes are still OK + bool ValidateNodesEdges (bool _bValidateIdxHead = true) const + { + bool bRet = true; + + // Validate vecTaxiNodes and vecTaxiEdges + for (size_t idxN = 0; idxN < vecTaxiNodes.size(); ++idxN) + { + const TaxiNode& n = vecTaxiNodes[idxN]; + for (size_t idxE: n.vecEdges) + { + const TaxiEdge& e = vecTaxiEdges[idxE]; + if (!e.isValid()) { + LOG_MSG(logFATAL, "Node %lu includes edge %lu, which is invalid!", + idxN, idxE, e.startNode(), e.endNode()); + bRet = false; + } + if (e.startNode() != idxN && + e.endNode() != idxN) { + LOG_MSG(logFATAL, "Node %lu includes edge %lu, which however goes %lu - %lu!", + idxN, idxE, e.startNode(), e.endNode()); + bRet = false; + } + } + if (!n.HasGeoCoords()) { + LOG_MSG(logFATAL, "Node %lu has no geo coordinates!", idxN); + bRet = false; + } + } + + // Validate vecTaxiEdges + for (size_t idxE = 0; idxE < vecTaxiEdges.size(); ++idxE) + { + const TaxiEdge& e = vecTaxiEdges[idxE]; + if (e.isValid() && e.startNode() == e.endNode()) { + LOG_MSG(logFATAL, "Valid edge %lu has a == b == %lu", + idxE, e.startNode()); + bRet = false; + } + } + + // Validate the index array sorted by heading + if (_bValidateIdxHead) + { + if (vecTaxiEdgesIdxHead.size() > vecTaxiEdges.size()) { + LOG_MSG(logFATAL, "vecTaxiEdgesIdxHead.size() = %lu > %lu = vecTaxiEdges.size()", + vecTaxiEdgesIdxHead.size(), vecTaxiEdges.size()); + bRet = false; + } + double prevAngle = -1.0; + for (size_t idxE: vecTaxiEdgesIdxHead) + { + if (idxE >= vecTaxiEdges.size() || + std::isnan(vecTaxiEdges[idxE].angle) || + vecTaxiEdges[idxE].angle < prevAngle) + { + LOG_MSG(logFATAL, "vecTaxiEdgesIdxHead wrongly sorted, edge %lu (heading %.1f) at wrong place after heading %.1f", + idxE, vecTaxiEdges[idxE].angle, prevAngle); + bRet = false; + } + } + } + + return bRet; + } +#endif + // --- MARK: Runways /// The vector of runway endpoints @@ -479,38 +1566,52 @@ class Apt { /// Any runway endpoints defined? bool HasRwyEndpoints () const { return !vecRwyEndPts.empty(); } + /// Add egdes into the taxinetwork for the runway + void AddRwyEdges () + { + // rwy end nodes are collected in vecRwyNodes in pairs + for (auto i = vecRwyNodes.cbegin(); + i != vecRwyNodes.cend(); + i = std::next(i,2)) + { + const TaxiNode& a = *i; + const TaxiNode& b = *std::next(i); + const size_t idxA = AddTaxiNode(a.lat, a.lon); + const size_t idxB = AddTaxiNode(b.lat, b.lon); + AddTaxiEdge(idxA, idxB, TaxiEdge::RUN_WAY); + } + } + /// Adds both rwy ends from apt.dat information fields void AddRwyEnds (double lat1, double lon1, double displaced1, const std::string& id1, double lat2, double lon2, double displaced2, const std::string& id2) { + // Add this original extend of the runway to the temporary storage, + // so later on we add a proper edge for the taxi network + vecRwyNodes.emplace_back(lat1, lon1); + vecRwyNodes.emplace_back(lat2, lon2); + // Original position of outer end of runway - positionTy re1 (lat1,lon1,NAN,NAN,NAN,NAN,NAN,positionTy::GND_ON); - positionTy re2 (lat2,lon2,NAN,NAN,NAN,NAN,NAN,positionTy::GND_ON); + positionTy re1 (lat1,lon1,NAN,NAN,NAN,NAN,NAN,GND_ON); + positionTy re2 (lat2,lon2,NAN,NAN,NAN,NAN,NAN,GND_ON); vectorTy vecRwy = re1.between(re2); - + // move by displayed threshold // and then by another 10% of remaining length to determine actual touch-down point vecRwy.dist -= displaced1; vecRwy.dist -= displaced2; re1 += vectorTy (vecRwy.angle, displaced1 + vecRwy.dist * ART_RWY_TD_POINT_F ); re2 += vectorTy (vecRwy.angle, -(displaced2 + vecRwy.dist * ART_RWY_TD_POINT_F)); - // Also adapt out knowledge of rwy length: 80% if previous value are left + // Also adapt our knowledge of rwy length: 80% if previous value are left vecRwy.dist *= (1 - 2 * ART_RWY_TD_POINT_F); // 1st rwy end bounds.enlarge(re1); - vecRwyEndPts.emplace_back(id1, re1.lat(), re1.lon()); + vecRwyEndPts.emplace_back(id1, re1.lat(), re1.lon(), vecRwy.angle); - // 2nd rwy end + // 2nd rwy end, opposite direction bounds.enlarge(re2); - vecRwyEndPts.emplace_back(id2, re2.lat(), re2.lon()); - - // The edge between them, making up the actual runway - vecTaxiEdges.emplace_back(TaxiEdge::RUN_WAY, - vecRwyEndPts.size()-2, // index of rwyEp1 - vecRwyEndPts.size()-1, // index of rwyEp2 - vecRwy.angle, - vecRwy.dist); + vecRwyEndPts.emplace_back(id2, re2.lat(), re2.lon(), std::fmod(vecRwy.angle+180.0, 360.0)); } /// @brief Update rwy ends and airport with proper altitude @@ -534,37 +1635,122 @@ class Apt { } } - /// Return iterator to first rwy, or `GetTaxiEdgeVec().cend()` if none found - vecTaxiEdgeTy::const_iterator FirstRwy () const + /// Returns a human-readable string with all runways, mostly for logging purposes + std::string GetRwysString () const + { + std::string s; + // loop all runways (the two ends of a rwy are always added together to the vector) + for (vecRwyEndPtTy::const_iterator i = vecRwyEndPts.cbegin(); + i != vecRwyEndPts.cend(); + ++i) + { + if (!s.empty()) s += " / "; // divider between runways + s += i->id; // add ids of runways + s += '-'; + if ((++i) != vecRwyEndPts.cend()) + s += i->id; + } + return s; + } + + // --- MARK: Startup locations + + /// The vevtor of startup locations + const vecStartupLocTy& GetStartupLocVec() const { return vecStartupLocs; } + + /// Add a startup location + void AddStartupLoc (const std::string& _id, + double _lat, double _lon, + double _heading) { - return std::find_if(vecTaxiEdges.cbegin(), vecTaxiEdges.cend(), - [](const TaxiEdge& te){return te.GetType() == TaxiEdge::RUN_WAY;}); + // Heading could be defined negative + while (_heading < 0.0) + _heading += 360.0; + + // resonabilit check...then add to our list of startup locations + if ( -90.0 <= _lat && _lat <= 90.0 && + -180.0 <= _lon && _lon < 180.0) + { + // The startup location seems to be aligned with the plane's tip + // while all CSL model's origin is at the center of the full. + // To (partly) make up for this we move out the startup location + // by about 10m. (`viaPos` is here just used as temp variable.) + const positionTy origPos (_lat, _lon); + vectorTy vec (std::fmod(_heading+180.0, 360.0), + APT_STARTUP_MOVE_BACK); + const positionTy startPos = CoordPlusVector(origPos, vec); + + // now add this moved out position to our list + vecStartupLocs.emplace_back(_id, startPos.lat(), startPos.lon(), _heading); + StartupLoc& loc = vecStartupLocs.back(); + bounds.enlarge_pos(loc.lat, loc.lon); // make sure it becomes part of the airport boundary + + // Add another position 50m out as the "via" pos: via which we roll to the startup location + vec.dist = APT_STARTUP_VIA_DIST; + loc.viaPos = CoordPlusVector(startPos, vec); + } } - /// Return iterator to next rwy after `i`, or `GetTaxiEdgeVec().cend()` if none found - vecTaxiEdgeTy::const_iterator NextRwy (vecTaxiEdgeTy::const_iterator i) const + /// @brief Find closest startup location, or `nullptr` if non close enough + /// @param pos Search near this position (only lat/lon are used( + /// @param _maxDist Search distance, only return a startup location maximum this far away + /// @param[out] _outDist Distance to returned startup location, `NAN` if none found. + const StartupLoc* FindStartupLoc (const positionTy& pos, + double _maxDist = APT_JOIN_MAX_DIST_M, + double* _outDist = nullptr) const { - return std::find_if(++i, vecTaxiEdges.cend(), - [](const TaxiEdge& te){return te.GetType() == TaxiEdge::RUN_WAY;}); + const StartupLoc* pRet = nullptr; + _maxDist *= _maxDist; // square, more performant for comparison + for (const StartupLoc& loc: vecStartupLocs) + { + const double dist = DistLatLonSqr(loc.lat, loc.lon, + pos.lat(), pos.lon()); + if (dist < _maxDist) + { + _maxDist = dist; + pRet = &loc; + } + } + + // return results + if (_outDist) + *_outDist = pRet ? std::sqrt(_maxDist) : NAN; + return pRet; } - /// Returns a human-readable string with all runways, mostly for logging purposes - std::string GetRwysString () const + /// @brief Project pos onto the path leading away from the startup location + void ProjectPosOnStartupPath (positionTy& _pos, const StartupLoc& _startLoc) { - std::string s; - // loop all runways - for (vecTaxiEdgeTy::const_iterator i = FirstRwy(); - i != vecTaxiEdges.cend(); - i = NextRwy(i)) + // One thing is for sure: the heading must match startup location + _pos.heading() = _startLoc.heading; + _pos.f.bHeadFixed = true; + _pos.f.specialPos = SPOS_STARTUP; + // And the altitude needs re-comupting + _pos.alt_m() = NAN; + + // Compute temporary "coordinates" in meters, relative to the search position + distToLineTy dist; + const double start_x = Lon2Dist(_startLoc.lon - _pos.lon(), _pos.lat()); // x is eastward + const double start_y = Lat2Dist(_startLoc.lat - _pos.lat()); // y is northward + const double via_x = Lon2Dist(_startLoc.viaPos.x - _pos.lon(), _pos.lat()); + const double via_y = Lat2Dist(_startLoc.viaPos.y - _pos.lat()); + DistPointToLineSqr(0.0, 0.0, start_x, start_y, via_x, via_y, dist); + + // We don't want the plane to crash into the gate, so we stop the plane + // at the startup location + if (dist.leg2_len2 > dist.len2) // is base beyond startup location? { - if (!s.empty()) s += " / "; // divider between runways - try { - s += i->GetRwyEP_A(*this).id; // add ids of runways - s += '-'; - s += i->GetRwyEP_B(*this).id; - } catch(...) {} // shouldn't happen...! + _pos.lat() = _startLoc.lat; + _pos.lon() = _startLoc.lon; + } else { + // otherwise move to projection on the path to the startup location + double base_x = NAN, base_y = NAN; + DistResultToBaseLoc(start_x, start_y, + via_x, via_y, + dist, base_x, base_y); + _pos.lon() += Dist2Lon(base_x, _pos.lat()); + _pos.lat() += Dist2Lat(base_y); } - return s; } // --- MARK: Bounding box @@ -586,9 +1772,6 @@ class Apt { }; // class Apt -// Y Probe for terrain altitude computation -XPLMProbeRef Apt::YProbe = NULL; - /// Map of airports, key is the id (typically: ICAO code) typedef std::map mapAptTy; @@ -598,6 +1781,15 @@ static mapAptTy gmapApt; /// Lock to access global map of airports static std::mutex mtxGMapApt; +// Temporary storage while reading an airport from apt.dat +vecTaxiNodesTy Apt::vecRwyNodes; +mapTaxiTmpPosTy Apt::mapPos; +listTaxiTmpPathTy Apt::listPaths; +vecIdxTy Apt::vecPathEnds; + +// Y Probe for terrain altitude computation +XPLMProbeRef Apt::YProbe = NULL; + // Add airport to list of airports /// @details It is actually expected that `apt` is not yet known and really added to the map, /// that's why the fancy debug log message is formatted first. @@ -610,45 +1802,68 @@ void Apt::AddApt (Apt&& apt) // slightly outside the airport are still considered for searching: apt.EnlargeBounds_m(double(dataRefs.GetFdSnapTaxiDist_m())); - // We sort the edges by heading, which allows for faster finding - // of suitable edges - std::sort(apt.vecTaxiEdges.begin(), - apt.vecTaxiEdges.end(), - TaxiEdge::CompHeadLess); + // Post-process the temporary maps/lists into proper apt vectors + // (Only if we processed the 120 taxiways, not if we used the 1200 taxi routes) + if (apt.HasTempNodesEdges()) { + apt.PostProcessPaths(); // add nodes and edges for taxways +#ifdef DEBUG + LOG_ASSERT(apt.ValidateNodesEdges(false)); +#endif + apt.AddRwyEdges(); // add edges for each runway +#ifdef DEBUG + LOG_ASSERT(apt.ValidateNodesEdges(false)); +#endif + } + + // Prepare the indirect array, which sorts by edge angle + // for faster finding of edges by heading + apt.SortTaxiEdges(); + // Now connect open ends, ie. try finding joints between a node and existing edges + apt.JoinPathEnds(); +#ifdef DEBUG + LOG_ASSERT(apt.ValidateNodesEdges()); +#endif + // Fancy debug-level logging message, listing all runways + // (here already as `apt` gets moved soon and becomes reset) LOG_MSG(logDEBUG, "apt.dat: Added %s at %s with %lu runways (%s) and [%lu|%lu] taxi nodes|edges", apt.GetId().c_str(), std::string(apt.GetBounds()).c_str(), apt.GetRwyEndPtVec().size() / 2, apt.GetRwysString().c_str(), apt.GetTaxiNodesVec().size(), - apt.GetTaxiEdgeVec().size() - apt.GetRwyEndPtVec().size()/2); + apt.GetTaxiEdgeVec().size()); // Access to the list of airports is guarded by a lock + const std::string key = apt.GetId(); // make a copy of the key, as `apt` gets moved soon: { std::lock_guard lock(mtxGMapApt); - std::string key = apt.GetId(); // make a copy of the key, as `apt` gets moved soon: - gmapApt.emplace(std::move(key), std::move(apt)); + gmapApt.emplace(key, std::move(apt)); } + + // clear all temporary storage + vecRwyNodes.clear(); + mapPos.clear(); + listPaths.clear(); + vecPathEnds.clear(); + +#ifdef DEBUG + if (dataRefs.GetLogLevel() == logDEBUG) + LTAptDump(key); +#endif } /// Return the a node, ie. the starting point of the edge const TaxiNode& TaxiEdge::GetA (const Apt& apt) const { - if (type == RUN_WAY) - return apt.GetRwyEndPtVec()[a]; - else - return apt.GetTaxiNodesVec()[a]; + return apt.GetTaxiNodesVec()[a]; } /// Return the b node, ie. the ending point of the edge const TaxiNode& TaxiEdge::GetB (const Apt& apt) const { - if (type == RUN_WAY) - return apt.GetRwyEndPtVec()[b]; - else - return apt.GetTaxiNodesVec()[b]; + return apt.GetTaxiNodesVec()[b]; } @@ -656,27 +1871,40 @@ const TaxiNode& TaxiEdge::GetB (const Apt& apt) const // MARK: File Reading Thread // This code runs in the thread for file reading operations // + +/// @brief List of accepted Line Type Codes +/// @see More information on reading from `apt.dat` is on [a separate page](@ref apt_dat). +static const std::array APT_LINE_TYPES { 1, 7, 10, 11, 51, 57, 60, 61 }; +/// @brief These row types terminate a path +/// @see More information on reading from `apt.dat` is on [a separate page](@ref apt_dat). +static const std::array APT_PATH_TERM_ROW_CODES { 113, 114, 115, 116 }; /// @brief Process one "120" section of an `apt.dat` file, which contains a taxi line definitions in the subsequent 111-116 lines /// @details Starts reading in the next line, expecting nodes in lines starting with 111-116. /// According to specs, such a section has to end with 113-116. But we don't rely on it, /// so we are more flexible in case of errorneous files. We read until we find a line _not_ starting /// with 111-116 and return that back to the caller to be processed again.\n -/// We only process line segments with Line Type Codes 1, 7, 51, 57 (Taxiway centerlines).\n -/// All nodes are temporarily stored in a local list. After reading some nodes are removed, +/// We only process line segments with Line Type Codes for taxiway centerlines. +/// A segment ends on _any_ line with no or a non-matching line type code. +/// Such a segment becomes a path in LiveTraffic. One 120-section of apt.dat +/// can contain many such segments ending in lines with no line type code.\n +/// All nodes are temporarily stored in a local list. After reading finished, some nodes are removed, /// as in actual files nodes can be very close together (up to being identical!). -/// We store a minimum segment length of 10m (`APT_MIN_TAXI_SEGM_LEN_M`) only -/// and thin out nodes that are closer together. Only after thinning, the remaining -/// nodes and edges are added to the apt's taxiway network. +/// We combine nodes to longer egdes until the edge's angle turns more than 15° away +/// from the orginal heading. Then only the next edge begins. This thins out nodes and egdes. +/// The remaining nodes and edges are added to the apt's taxiway network. +/// @see More information on reading from `apt.dat` is on [a separate page](@ref apt_dat). /// @returns the next line read from the file, which is after the "120" section -static std::string ReadOneTaxiLine (std::ifstream& fIn, Apt& apt) +static std::string ReadOneTaxiLine (std::ifstream& fIn, Apt& apt, unsigned long& lnNr) { - vecTaxiNodesTy vecNodes; // temporarily stored nodes in order of appearance + TaxiTmpPath path; // holds the path (centerline positions) we are reading now + ptTy prevBezPt; // previous bezier point std::string ln; while (fIn) { // read a line from the input file safeGetline(fIn, ln); + ++lnNr; // ignore empty lines if (ln.empty()) continue; @@ -687,105 +1915,129 @@ static std::string ReadOneTaxiLine (std::ifstream& fIn, Apt& apt) // We need at minimum 3 fields (line id, latitude, longitude) if (fields.size() < 3) break; - // Check for any of "our" line codes (we treat them all equal) - const int lnCod = std::stoi(fields[0]); - if (111 <= lnCod && lnCod <= 116) + // Not any of "our" line codes (we treat them all equal)? -> stop + int lnCod = std::stoi(fields[0]); + if (lnCod < 111 || lnCod > 116) + break; + + // Check for the Line Type Code to be Taxi Centerline + int lnTypeCode = 0; + + // In case of line codes 111, 113 the Line Type Code is in field 3 + if (lnCod == 111 || lnCod == 113) { + if (fields.size() >= 4) + lnTypeCode = std::stoi(fields[3]); + // In case of line codes 112, 114 the Line Type Code is in field 5 + } else if (lnCod == 112 || lnCod == 114) { + if (fields.size() >= 6) + lnTypeCode = std::stoi(fields[5]); + } + + // Is this a node starting/continuing a taxi centerline? + const bool bIsCenterline = std::any_of(APT_LINE_TYPES.cbegin(), APT_LINE_TYPES.cend(), + [lnTypeCode](int c){return c == lnTypeCode;}); + // If this node does not start/continue a centerline, does it at least end an already started one? + const bool bEndsCenterline = + !path.listPos.empty() && // is there any path to terminate? + (!bIsCenterline || std::any_of(APT_PATH_TERM_ROW_CODES.cbegin(), APT_PATH_TERM_ROW_CODES.cend(), + [lnCod](int c){return c == lnCod;})); + + // Do we need to process this node? + if (bIsCenterline || bEndsCenterline) { - // Check for the Line Type Code to be Taxi Centerline - int lnTypeCode = 1; // by default we add (also goes for lnCod 115,116!) - // In case of line codes 111, 113 the Line Type Code is in field 3 - if (lnCod == 111 || lnCod == 113) { - if (fields.size() >= 4) - lnTypeCode = std::stoi(fields[3]); - // In case of line codes 112, 114 the Line Type Code is in field 5 - } else if (lnCod == 112 || lnCod == 114) { - if (fields.size() >= 6) - lnTypeCode = std::stoi(fields[5]); - } - - // Taxi Centerline? - if (lnTypeCode == 1 || lnTypeCode == 7 || - lnTypeCode == 51 || lnTypeCode == 57) + // Read location and Bezier control point + ptTy pos (std::stod(fields[2]), std::stod(fields[1])); // lon, lat + ptTy bezPt; + if ((lnCod == 112 || lnCod == 114 || lnCod == 116) && + fields.size() >= 5) { - // add the node temporarily - vecNodes.emplace_back(std::stod(fields[1]), // latitude - std::stod(fields[2])); // longitude - } else { - // Not a Taxi Centerline, so we don't bother any longer, stop processing - break; + // read Bezier control point + bezPt.x = std::stod(fields[4]); // lon + bezPt.y = std::stod(fields[3]); // lat +#ifdef DEBUG + // remember Bezier handle for output to GPS Visualizer + TaxiNode& n = apt.vecBezierHandles.emplace_back(pos.y, pos.x); + n.prevIdx = lnNr; + n.bVisited = false; + apt.vecBezierHandles.emplace_back(bezPt.y, bezPt.x); + // if there is a previous pos (to which we will apply the control point, too, just mirrored) + // then also add the mirrored handle + if (!path.listPos.empty()) + { + TaxiNode& n2 = apt.vecBezierHandles.emplace_back(pos.y, pos.x); + n2.prevIdx = lnNr; + n2.bVisited = true; // indicates "mirrored" + apt.vecBezierHandles.emplace_back(bezPt.mirrorAt(pos).y, bezPt.mirrorAt(pos).x); + } +#endif } - } - else // not any of our codes -> stop processing - break; - } - - // Reading the section is done, now process the resulting nodes - if (vecNodes.size() >= 2) - { - // The first node is definitely used, add it already - apt.AddTaxiNode(vecNodes.front().lat, - vecNodes.front().lon); - - // The very last node one will also be added later. - // Between these two: - // Remove nodes, which are closer together than 10m, - // add the remainder to the airport's taxi network - if (vecNodes.size() >= 3) { - for (vecTaxiNodesTy::const_iterator iter = vecNodes.cbegin(); - // end when iter points to 3rd-last element of vector - std::next(iter,3) != vecNodes.cend(); - ) + + // If position is different from previous + // (there are quite a number of _exactly_ equal subsequent nodes + // in actual apt.dat, which we filter out this way) + if (path.listPos.empty() || path.listPos.back() != pos) { - const TaxiNode& a = *iter; - const TaxiNode& b = *std::next(iter); - const double distEst = DistLatLonSqr(a.lat, a.lon, b.lat, b.lon); - if (distEst < APT_MIN_TAXI_SEGM_LEN_M2) { - // too close, remove the next nodes - vecNodes.erase(std::next(iter)); - } else { - // long enough an edge, so add it to the airport - const size_t idx = apt.AddTaxiNode(b.lat, b.lon); - apt.AddTaxiEdge(idx-1, idx, std::sqrt(distEst)); - // move on and test the next edge - ++iter; + // We need a loop here as in case of row codes 113/114 we "close a loop", which requires to add to path segments + for(;;) { + // If necessary add additional nodes along the Bezier curve + // from the previous node to the current + if (!path.listPos.empty() && (prevBezPt.isValid() || bezPt.isValid())) + { + // the previous node, where the Bezier curve starts + const TaxiTmpPos& prevPos = path.listPos.back(); + // length of the straight line from prevPos to pos + const double eLen = DistLatLon(pos.y, pos.x, prevPos.lat(), prevPos.lon()); + if (eLen > APT_MAX_SIMILAR_NODE_DIST_M) { + // the second Bezier control point needs to be mirrored at that pos + const ptTy mbezPt = bezPt.mirrorAt(pos); + // number of segments we will create, at least 2 (ie. at least split in half) + const int numSegm = std::max (2, int(eLen / APT_JOIN_MAX_DIST_M / 2)); + for (int s = 1; s < numSegm; ++s) + { + // Calculate a point on the Bezier curve + const ptTy p = + prevBezPt.isValid() && mbezPt.isValid() ? Bezier(double(s)/numSegm, prevPos, prevBezPt, mbezPt, pos) : + !mbezPt.isValid() ? Bezier(double(s)/numSegm, prevPos, prevBezPt, pos) : + Bezier(double(s)/numSegm, prevPos, mbezPt, pos); + // Add the Bezier curve node to our backlog + apt.AddTaxiTmpPos(p.y, p.x); // add the node to the airport's temporary list of nodes + path.listPos.emplace_back(p.y, p.x); // add the node position to the path (temporary storage) + } + } + } + + // Add the actual node to our backlog + apt.AddTaxiTmpPos(pos.y, pos.x); // add the node to the airport's temporary list of nodes + path.listPos.emplace_back(pos.y, pos.x); // add the node position to the path (temporary storage) + + // Exit the loop if not row codes 113/114 (closing loop) + if (lnCod != 113 && lnCod != 114) + break; + + // As we are closing a loop we also need to add the segment + // from this point back to the beginning, so we add the first point once again: + pos.x = path.listPos.front().x; + pos.y = path.listPos.front().y; + prevBezPt = bezPt; + bezPt.clear(); + lnCod += 2; // this makes sure we break out of the loop next time } } - } - // For last 3 nodes (a <-> b <-> c) decide if the middle node b is - // too close to either side; if so: remove and add one egde a<->c, - // else add two edges a<->b, b<->c - double distToLast = NAN; - if (vecNodes.size() >= 3) { - const TaxiNode& a = vecNodes[vecNodes.size()-3]; - const TaxiNode& b = vecNodes[vecNodes.size()-2]; - const TaxiNode& c = vecNodes.back(); - const double AB = DistLatLonSqr(a.lat, a.lon, b.lat, b.lon); - const double BC = DistLatLonSqr(b.lat, b.lon, c.lat, c.lon); - if (AB < APT_MIN_TAXI_SEGM_LEN_M2 || - BC < APT_MIN_TAXI_SEGM_LEN_M2) + // If this ends a path then we add the entire path to our repository + if (bEndsCenterline) { - // too close, remove b, but we know the final dist already - vecNodes.erase(std::prev(vecNodes.cend(),2)); - distToLast = std::sqrt(AB) + std::sqrt(BC); - } else { - // OK, both edges needed, here add the a<->b edge: - const size_t idx = apt.AddTaxiNode(b.lat, b.lon); - apt.AddTaxiEdge(idx-1, idx, std::sqrt(AB)); - // The last distance is now the one from b to c: - distToLast = std::sqrt(BC); + apt.AddTaxiTmpPath(std::move(path)); // move the entire path to the temporary list of paths for post-processing + path.listPos.clear(); } - } - - // Add the final edge between the last two nodes + + // move on to next node + prevBezPt = bezPt; + } // is centerline or ends a centerline + else { - const TaxiNode& y = vecNodes[vecNodes.size()-2]; - const TaxiNode& z = vecNodes.back(); - if (std::isnan(distToLast)) - distToLast = std::sqrt(DistLatLonSqr(y.lat, y.lon, z.lat, z.lon)); - const size_t idx = apt.AddTaxiNode(z.lat, z.lon); - apt.AddTaxiEdge(idx-1, idx, distToLast); - + // don't process this node, clear temp stuff + prevBezPt.clear(); } } @@ -793,12 +2045,21 @@ static std::string ReadOneTaxiLine (std::ifstream& fIn, Apt& apt) return ln; } -/// Read airports in the one given `apt.dat` file +/// @brief Read airports in the one given `apt.dat` file +/// @details The function process the following line types:\n +/// 1 - Airport header to start a new airport and learn its name/id\n +/// 100 - Runway definitions\n +/// 120 - Line segments (incl. subsequent 111-116 codes), or alternatively, if no 120 code is found:\n +/// 1201, 1202 - Taxi route netwirk +/// @see More information on reading from `apt.dat` is on [a separate page](@ref apt_dat). static void ReadOneAptFile (std::ifstream& fIn, const boundingBoxTy& box) { // Walk the file std::string ln; + unsigned long lnNr = 0; // for debugging purposes we are interested to track the file's line number bool bProcessGivenLn = false; // process a line returned by a sub-routine? + // Are we reading 120 taxi centerlines or 1200 taxi route network? + enum netwTypeTy { NETW_UNKOWN=0, NETW_CENTERLINES, NETW_TAXIROUTES } netwType = NETW_UNKOWN; Apt apt; while (!bStopThread && (bProcessGivenLn || fIn)) { @@ -810,6 +2071,7 @@ static void ReadOneAptFile (std::ifstream& fIn, const boundingBoxTy& box) // read a fresh line from the file ln.clear(); safeGetline(fIn, ln); + ++lnNr; } // ignore empty lines @@ -836,6 +2098,7 @@ static void ReadOneAptFile (std::ifstream& fIn, const boundingBoxTy& box) { // re-init apt object, now with the proper id defined apt = Apt(fields[4]); + netwType = NETW_UNKOWN; } } @@ -857,7 +2120,7 @@ static void ReadOneAptFile (std::ifstream& fIn, const boundingBoxTy& box) { // Have we accepted the airport already? // Or - this being the first rwy - does the rwy lie in the search bounding box? - if (apt.HasTaxiWays() || + if (apt.HasRwyEndpoints() || box.contains(positionTy(lat,lon))) { // add both runway ends to the airport @@ -881,66 +2144,85 @@ static void ReadOneAptFile (std::ifstream& fIn, const boundingBoxTy& box) } // if a runway line startin with "100 " // test for the start of a taxi line segment - else if (apt.HasRwyEndpoints() && // apt good enough, has already a runway? (BTW this excludes pure heliports) - ((ln.size() == 3 && // just and only the "120" marker? - ln == "120") || - (ln.size() >= 4 && // or starting with "120 "? - ln[0] == '1' && - ln[1] == '2' && - ln[2] == '0' && - (ln[3] == ' ' || ln[3] == '\t')))) - { - // Read the entire line segment - ln = ReadOneTaxiLine(fIn, apt); - bProcessGivenLn = true; // process the returned line read from the file - } - -/* TODO: Remove completely - // test for a taxi network node - else if (apt.HasId() && - ln.size() >= 15 && // line long enough? - ln[0] == '1' && // starting with "1201 "? + // This is valid for 120 as well as 120x: + else if (apt.HasRwyEndpoints() && + ln.size() >= 3 && + ln[0] == '1' && ln[1] == '2' && - ln[2] == '0' && - ln[3] == '1' && - (ln[4] == ' ' || ln[4] == '\t')) + ln[2] == '0') { - // separate the line into its field values - std::vector fields = str_tokenize(ln, " \t", true); - // We need fields 2, 3, the location, and 5, the index, only - if (fields.size() >= 5) { - // Convert and briefly test the given location - const double lat = std::stod(fields[1]); - const double lon = std::stod(fields[2]); - const size_t idx = (size_t)std::stoul(fields[4]); - if (-90.0 <= lat && lat <= 90.0 && - -180.0 <= lon && lon < 180.0) - { - apt.AddTaxiNode(lat, lon, idx); - } // has valid location - } // enough fields in line? - } // if a taxi network node ("1201 ") - - // test for a taxi network edge - else if (apt.HasId() && - ln.size() >= 8 && // line long enough? - ln[0] == '1' && // starting with "1201 "? - ln[1] == '2' && + if (ln == "120 RM" || ln == "120 TB") { + // specifically ignore these sections, they draw markings + // for gate positions, taxiway borders etc. + // often using taxi centerline codes, + // but the markings aren't actually taxiways + } + // Standard Line segment, that could be a centerline? + else if (netwType != NETW_TAXIROUTES && // not yet decided for the other type of network? + (ln.size() == 3 || // was just the text "120" + (ln.size() >= 4 && (ln[3] == ' ' || ln[3] == '\t')))) // or "120 " plus more + { + // Read the entire line segment + ln = ReadOneTaxiLine(fIn, apt, lnNr); + bProcessGivenLn = true; // process the returned line read from the file + if (apt.HasTempNodesEdges()) // did we (latest now) add taxi segments? + netwType = NETW_CENTERLINES; + } + else if (netwType != NETW_CENTERLINES) + { + // separate the line into its field values + std::vector fields = str_tokenize(ln, " \t", true); + int lnCode = std::stoi(fields[0]); + + // 1201 - Taxi route network node + if (lnCode == 1201 && fields.size() >= 5) { + // Convert and briefly test the given location + const double lat = std::stod(fields[1]); + const double lon = std::stod(fields[2]); + const size_t idx = std::stoul(fields[4]); + if (-90.0 <= lat && lat <= 90.0 && + -180.0 <= lon && lon < 180.0) + { + netwType = NETW_TAXIROUTES; + apt.AddTaxiNodeFixed(lat, lon, idx); + } // has valid location + } + else if (lnCode == 1202 && fields.size() >= 3) { + // Convert indexes and try adding the node + const size_t n1 = std::stoul(fields[1]); + const size_t n2 = std::stoul(fields[2]); + bool bRunway = (fields.size() >= 5 && + fields[4] == "runway"); + apt.AddTaxiEdge(n1, n2, + bRunway ? TaxiEdge::RUN_WAY : TaxiEdge::TAXI_WAY); + } + } // not NETW_CENTERLINE + } // "120" + + // Startup locations, row code 1300 + else if (apt.HasRwyEndpoints() && + ln.size() > 20 && // line long enough? + ln[0] == '1' && // starting with "100 "? + ln[1] == '3' && ln[2] == '0' && - ln[3] == '2' && + ln[3] == '0' && (ln[4] == ' ' || ln[4] == '\t')) { // separate the line into its field values std::vector fields = str_tokenize(ln, " \t", true); - // We need fields 2, 3 only, the node indexes - if (fields.size() >= 3) { - // Convert indexes and try adding the node - const size_t n1 = (size_t)std::stoul(fields[1]); - const size_t n2 = (size_t)std::stoul(fields[2]); - apt.AddTaxiEdge(n1, n2); - } // enough fields in line? - } // if a taxi network edge ("1202 ") -*/ + if (fields.size() >= 4) + { + const double lat = std::stod(fields[1]); // latitude + const double lon = std::stod(fields[2]); // longigtude + const double head = std::stod(fields[3]); // heading + std::string id; // all the rest makes up the id + for (size_t i = 4; i < fields.size(); ++i) + id += fields[i] + ' '; + if (!id.empty()) id.pop_back(); // remove the last separating space + apt.AddStartupLoc(id, lat, lon, head); + } + } + } // for each line of the apt.dat file // If the last airport read is valid don't forget to add it to the list @@ -1119,6 +2401,10 @@ void LTAptUpdateRwyAltitudes () // Update the airport data with airports around current camera position void LTAptRefresh () { + // If not doing snapping, then not doing reading... + if (dataRefs.GetFdSnapTaxiDist_m() <= 0) + return; + // Safety check: Thread already running? // Future object is valid, i.e. initialized with an async operation? if (futRefreshing.valid() && @@ -1140,7 +2426,6 @@ void LTAptRefresh () // But do we need to check for rwy altitudes after last scan of apt.dat file? if (bAptsAdded) { LTAptUpdateRwyAltitudes(); - LTAptLocalCoordsUpdate(false); } bAptsAdded = false; return; @@ -1160,45 +2445,29 @@ void LTAptRefresh () bAptsAdded = true; } -// Update local coordinate system's values due to ref point change -void LTAptLocalCoordsUpdate (bool bForce) -{ - // access is guarded by a lock - std::lock_guard lock(mtxGMapApt); - for (auto& pair: gmapApt) - pair.second.LocalCoordsUpdate(bForce); - LOG_MSG(logDEBUG, "apt.dat: Finished updating local coordinates"); -} - // Return the best possible runway to auto-land at -positionTy LTAptFindRwy (const LTAircraft& _ac) +positionTy LTAptFindRwy (const LTAircraft::FlightModel& _mdl, + const positionTy& _from, + double _speed_m_s, + const std::string& _logTxt) { // --- Preparation of aircraft-related data --- // allowed VSI range depends on aircraft model, converted to m/s - const double vsi_min = _ac.mdl.VSI_FINAL * ART_RWY_MAX_VSI_F * Ms_per_FTm; - const double vsi_max = _ac.mdl.VSI_FINAL / ART_RWY_MAX_VSI_F * Ms_per_FTm; - - // last known go-to position of aircraft, serving as start of search - const positionTy& from = _ac.GetToPos(); - // The heading we compare the runway with is normalized to [0..180) - double headSearch = HeadingNormalize(from.heading()); - bool bHeadInverted = false; - if (headSearch >= 180.0) { - headSearch -= 180.0; - bHeadInverted = true; - } - - // The speed to use, cut off at a reasonable approach speed: - const double speed_m_s = std::min (_ac.GetSpeed_m_s(), - _ac.mdl.FLAPS_DOWN_SPEED * ART_APPR_SPEED_F / KT_per_M_per_S); + const double vsi_min = _mdl.VSI_FINAL * ART_RWY_MAX_VSI_F * Ms_per_FTm; + const double vsi_max = _mdl.VSI_FINAL / ART_RWY_MAX_VSI_F * Ms_per_FTm; + + // The speed to use: cut off at a reasonable approach speed: + if (_speed_m_s > _mdl.FLAPS_DOWN_SPEED * ART_APPR_SPEED_F / KT_per_M_per_S) + _speed_m_s = _mdl.FLAPS_DOWN_SPEED * ART_APPR_SPEED_F / KT_per_M_per_S; // --- Variables holding Best Match --- const Apt* bestApt = nullptr; // best matching apt - const TaxiEdge* bestRwy = nullptr; // best matching rwy const RwyEndPt* bestRwyEndPt = nullptr; // best matching runway endpoint // The heading diff of the best match to its runway // (initialized to the max allowed value so that worse heading diffs aren't considered) double bestHeadingDiff = ART_RWY_MAX_HEAD_DIFF; + // distance to best airport? + double bestDist = ART_RWY_MAX_DIST; // when would we arrive there? double bestArrivalTS = NAN; @@ -1213,52 +2482,60 @@ positionTy LTAptFindRwy (const LTAircraft& _ac) { const Apt& apt = iterApt->second; - // Find the runways matching the current plane's heading - lstTaxiEdgeCPtrTy lstRwys; - if (apt.FindEdgesForHeading(headSearch, - ART_RWY_MAX_HEAD_DIFF, - lstRwys, - TaxiEdge::RUN_WAY)) + // Find the rwy endpoints matching the current plane's heading + for (const RwyEndPt& re: apt.GetRwyEndPtVec()) { - // loop over found runways of this airport - for (const TaxiEdge* e: lstRwys) - { - // The rwy end point we are (potentially) aiming at - const RwyEndPt& rwyEP = bHeadInverted ? e->GetRwyEP_B(apt) : e->GetRwyEP_A(apt); - - // We need to know the runway's altitude for what comes next - if (std::isnan(rwyEP.alt_m)) - continue; - - // Heading towards rwy, compared to current flight's heading - // (Find the rwy which requires least turn now.) - const double bearing = CoordAngle(from.lat(), from.lon(), rwyEP.lat, rwyEP.lon); - const double headingDiff = fabs(HeadingDiff(from.heading(), bearing)); - if (headingDiff > bestHeadingDiff) // worse than best known match? + // skip if rwy heading differs too much from flight heading + if (std::abs(HeadingDiff(re.heading, _from.heading())) > ART_RWY_MAX_HEAD_DIFF) + continue; + + // We need to know the runway's altitude for what comes next + if (std::isnan(re.alt_m)) + continue; + + // Heading towards rwy, compared to current flight's heading + // (Find the rwy which requires least turn now.) + const double bearing = CoordAngle(_from.lat(), _from.lon(), re.lat, re.lon); + const double headingDiff = std::abs(HeadingDiff(_from.heading(), bearing)); + if (headingDiff > bestHeadingDiff) // worse than best known match? + continue; + + // 3. Vertical speed, for which we need to know distance / flying time + const double dist = CoordDistance(_from.lat(), _from.lon(), re.lat, re.lon); + if (dist > bestDist) // too far out + continue; + const double d_ts = dist / _speed_m_s; + const double agl = _from.alt_m() - re.alt_m; + const double vsi = (-agl) / d_ts; + if (vsi < vsi_min) // would need too steep sinking? + continue; + + // flying more than 300ft/100m above ground? + if (agl > 100.0) { + // also consider max_vsi + if (vsi > vsi_max) // would fly too flat? -> too far out? continue; - - // 3. Vertical speed, for which we need to know distance / flying time - const double dist = CoordDistance(from.lat(), from.lon(), rwyEP.lat, rwyEP.lon); - const double d_ts = dist / speed_m_s; - const double vsi = (rwyEP.alt_m - from.alt_m()) / d_ts; - if (vsi < vsi_min || vsi > vsi_max) + } else { + // pretty close too the ground, cut off at shorter distance + if (dist > 3000.0) // 3000m is a runway length...we shouldn't look further that close to the ground continue; - - // We've got a match! - bestApt = &apt; - bestRwy = e; - bestRwyEndPt = &rwyEP; - bestHeadingDiff = headingDiff; // the heading diff (which would be a selection criterion on several rwys match) - bestArrivalTS = from.ts() + d_ts; // the arrival timestamp } + + // We've got a match! + bestApt = &apt; + bestRwyEndPt = &re; + bestHeadingDiff = headingDiff; // the heading diff (which would be a selection criterion on several rwys match) + bestDist = dist; + bestArrivalTS = _from.ts() + d_ts; // the arrival timestamp } } // Didn't find a suitable runway? if (!bestRwyEndPt || !bestApt) { - LOG_MSG(logDEBUG, "Didn't find runway for %s with heading %.0f°", - std::string(_ac).c_str(), - from.heading()); + if (!_logTxt.empty()) + LOG_MSG(logDEBUG, "Didn't find runway for %s with heading %.0f°", + _logTxt.c_str(), + _from.heading()); return positionTy(); } @@ -1267,23 +2544,27 @@ positionTy LTAptFindRwy (const LTAircraft& _ac) bestRwyEndPt->lon, bestRwyEndPt->alt_m, bestArrivalTS, - bestRwy->angle + (bHeadInverted ? 180.0 : 0.0), - _ac.mdl.PITCH_FLARE, + bestRwyEndPt->heading, + _mdl.PITCH_FLARE, 0.0, - positionTy::GND_ON, - positionTy::UNIT_WORLD, positionTy::UNIT_DEG, - LTAPIAircraft::FPH_TOUCH_DOWN); - LOG_MSG(logDEBUG, "Found runway %s/%s at %s for %s", - bestApt->GetId().c_str(), - bestRwyEndPt->id.c_str(), - std::string(retPos).c_str(), - std::string(_ac).c_str()); + GND_ON, + UNIT_WORLD, UNIT_DEG, + FPH_TOUCH_DOWN); + retPos.f.bHeadFixed = true; + retPos.f.specialPos = SPOS_RWY; + if (!_logTxt.empty()) + LOG_MSG(logDEBUG, "Found runway %s/%s at %s for %s", + bestApt->GetId().c_str(), + bestRwyEndPt->id.c_str(), + std::string(retPos).c_str(), + _logTxt.c_str()); return retPos; } // Snaps the passed-in position to the nearest rwy or taxiway if appropriate -bool LTAptSnap (positionTy& pos, bool bLogging) +bool LTAptSnap (LTFlightData& fd, dequePositionTy::iterator& posIter, + bool bInsertTaxiTurns) { // Configured off? if (dataRefs.GetFdSnapTaxiDist_m() <= 0) @@ -1293,12 +2574,12 @@ bool LTAptSnap (positionTy& pos, bool bLogging) std::lock_guard lock(mtxGMapApt); // Which airport are we looking at? - Apt* pApt = LTAptFind(pos); + Apt* pApt = LTAptFind(*posIter); if (!pApt) // not a position in any airport's bounding box return false; // Let's snap! - return pApt->SnapToTaxiway(pos, bLogging); + return pApt->SnapToTaxiway(fd, posIter, bInsertTaxiTurns); } @@ -1314,4 +2595,165 @@ void LTAptDisable () // destroy the Y Probe Apt::DestroyYProbe(); + + // remove all airport data + gmapApt.clear(); + lastCameraPos = positionTy(); + bAptsAdded = false; +} + + +#ifdef DEBUG +// Dumps the entire taxi network into a CSV file readable by GPS Visualizer +/// @see For a suggestion of settings for display: +/// https://www.gpsvisualizer.com/map_input?bg_map=google_openstreetmap&bg_opacity=70&form=leaflet&google_wpt_sym=diamond&trk_list=0&trk_opacity=100&trk_width=2&units=metric&width=1400&wpt_color=aqua +void LTAptDump (const std::string& _aptId) +{ + // find the airport by id + if (gmapApt.count(_aptId) < 1) return; + const Apt& apt = gmapApt.at(_aptId); + + // open the output file + const std::string fileName (dataRefs.GetXPSystemPath() + _aptId + ".csv"); + std::ofstream out (fileName, std::ios_base::out | std::ios_base::trunc); + // column headers + out << "type,BOT,symbol,color,rotation,latitude,longitude,time,speed,course,name,desc\n"; + out.precision(11); // precision is all digits, so we need something like 123.45678901 + + // Dump all rwy endpoints as Waypoints + for (const RwyEndPt& re: apt.GetRwyEndPtVec()) + out + << "W,," // type, BOT + << "arrow," // symbol + << "red," // color + << std::lround(re.heading) << ',' // rotation + << re.lat << ',' << re.lon << ',' // latitude,longitude + << ",,," // time,speed,course + << re.id << ',' // name + << std::lround(re.heading) << "°," // desc + << "\n"; + + // Dump all startup locations as Waypoints + for (const StartupLoc& loc: apt.GetStartupLocVec()) + out + << "W,," // type, BOT + << "wedge," // symbol + << "orange," // color + << std::lround(loc.heading) << ',' // rotation + << loc.lat << ',' << loc.lon << ',' // latitude,longitude + << ",,," // time,speed,course + << loc.id << ',' // name + << std::lround(loc.heading) << "°," // desc + << "\n"; + + // Dump all startup paths as tracks + for (const StartupLoc& loc: apt.GetStartupLocVec()) + { + out + << "T,1,," // type, BOT, symbol + << "orange," // color + << std::lround(loc.heading) << ',' // rotation + << loc.lat << ',' << loc.lon << ',' // latitude,longitude + << ",," // time,speed + << std::lround(loc.heading) << ',' // course + << "Path leaving " << loc.id << ',' // name + << std::lround(loc.heading) << "°," // desc + << "\n"; + + out + << "T,0,," // type, BOT, symbol + << "orange," // color + << std::lround(loc.heading) << ',' // rotation + << loc.viaPos.y << ',' << loc.viaPos.x << ',' // latitude,longitude + << ",," // time,speed + << std::lround(loc.heading) << ',' // course + << ',' // name, desc + << "\n"; + + } + + // Dump all nodes as Waypoints + size_t i = 0; + for (const TaxiNode& n: apt.GetTaxiNodesVec()) { + out + << "W,," // type, BOT + << (n.vecEdges.size() == 0 ? "pin," : // symbol + n.vecEdges.size() == 1 ? "circle," : + n.vecEdges.size() == 2 ? "square," : + n.vecEdges.size() == 3 ? "triangle," : + n.vecEdges.size() == 4 ? "diamond," : "star,") + << (apt.IsConnectedToRwy(i) ? "red," : // color: red if rwy, blue if 2 edges, else auqa + n.vecEdges.size() == 2 ? "blue," : "aqua,") + << "0," // rotation + << n.lat << ',' << n.lon << ',' // latitude,longitude + << ",,," // time,speed,course + << "Node " << i << ',' // name + << n.vecEdges.size() << " edges" // desc + << "\n"; + i++; + } + + // Dump all edges as Tracks + i = 0; + for (const TaxiEdge& e: apt.GetTaxiEdgeVec()) + { + const TaxiNode& a = e.GetA(apt); + const TaxiNode& b = e.GetB(apt); + + out + << "T,1,," // type, BOT, symbol + << (e.GetType() == TaxiEdge::RUN_WAY ? "red," : "blue,") // color + << std::lround(e.angle) << ',' // rotation + << a.lat << ',' << a.lon << ',' // latitude,longitude + << ",," // time,speed + << std::lround(e.angle) << ',' // course + << "Edge " << (i++) << ',' // name + << std::lround(e.angle) << "°, nodes " << e.startNode() << '-' << e.endNode() // desc + << "\n"; + + out + << "T,0,," // type, BOT, symbol + << (e.GetType() == TaxiEdge::RUN_WAY ? "red," : "blue,") // color + << std::lround(e.angle) << ',' // rotation + << b.lat << ',' << b.lon << ',' // latitude,longitude + << ",," // time,speed + << std::lround(e.angle) << ',' // course + << ',' // name, desc + << "\n"; + + } + + // Dump all Bezier handles + for (auto iter = apt.vecBezierHandles.cbegin(); + iter != apt.vecBezierHandles.cend(); + ++iter) + { + const TaxiNode& a = *iter; + const TaxiNode& b = *(++iter); + + out + << "T,1,," // type, BOT, symbol + << (a.bVisited ? "orange," : "magenta,") // color (mirrored control point or not?) + << ',' // rotation + << a.lat << ',' << a.lon << ',' // latitude,longitude + << ",," // time,speed + << ',' // course + << "Bezier Handle Ln " << a.prevIdx << ',' // name + << (a.bVisited ? "mirrored" : "") // desc + << "\n"; + + out + << "T,0,," // type, BOT, symbol + << (a.bVisited ? "orange," : "magenta,") // color (mirrored control point or not?) + << ',' // rotation + << b.lat << ',' << b.lon << ',' // latitude,longitude + << ",," // time,speed + << ',' // course + << ',' // name, desc + << "\n"; + } + + // Close the file + out.close(); } +#endif diff --git a/Src/LTChannel.cpp b/Src/LTChannel.cpp index 9c7ece53..431a4cd6 100644 --- a/Src/LTChannel.cpp +++ b/Src/LTChannel.cpp @@ -770,13 +770,16 @@ void LTFlightDataAcMaintenance() // if buffer-fill countdown is (still) running, update the figures in UI if ( initTimeBufFilled > 0 ) { - CreateMsgWindow(float(AC_MAINT_INTVL - .05), logINFO, MSG_BUF_FILL_COUNTDOWN, - int(mapFd.size()), - numAcAfter, + CreateMsgWindow(float(AC_MAINT_INTVL), + int(mapFd.size()), numAcAfter, int(initTimeBufFilled - dataRefs.GetSimTime())); // buffer fill-up time's up - if (dataRefs.GetSimTime() >= initTimeBufFilled) + if (dataRefs.GetSimTime() >= initTimeBufFilled) { initTimeBufFilled = 0; + CreateMsgWindow(float(AC_MAINT_INTVL), + int(mapFd.size()), numAcAfter, + -1); // clear the message + } } else { // tell the user a change from or to zero aircraft (actually showing) if ( !numAcBefore && (numAcAfter > 0)) diff --git a/Src/LTFlightData.cpp b/Src/LTFlightData.cpp index a38bb1e4..c7b1a79d 100644 --- a/Src/LTFlightData.cpp +++ b/Src/LTFlightData.cpp @@ -74,11 +74,18 @@ LTFlightData::FDStaticData& LTFlightData::FDStaticData::operator |= (const FDSta { // copy filled, and only filled data over current data // do it field-by-field only for fields which are actually filled + + // acTypeICAO: accept another value only if our current one is not helpful + if ((acTypeIcao.empty() || + acTypeIcao == dataRefs.GetDefaultAcIcaoType() || + acTypeIcao == dataRefs.GetDefaultCarIcaoType()) && + !other.acTypeIcao.empty()) + acTypeIcao = other.acTypeIcao; // a/c details if (!other.country.empty()) country = other.country; if (!other.man.empty()) man = other.man; - if (!other.mdl.empty()) mdl = other.mdl; + if (other.mdl.length() > mdl.length()) mdl = other.mdl; if (!other.catDescr.empty()) catDescr = other.catDescr; if (other.year) year = other.year; if (other.mil) mil = other.mil; // this only overwrite if 'true'... @@ -221,7 +228,7 @@ LTFlightData::LTFlightData () : rcvr(0),sig(0), rotateTS(NAN), // created "now"...if no positions are ever added then it will be removed after 2 x outdated interval -youngestTS(dataRefs.GetSimTime() + + dataRefs.GetAcOutdatedIntvl()), +youngestTS(dataRefs.GetSimTime() + 2 * dataRefs.GetAcOutdatedIntvl()), pAc(nullptr), probeRef(NULL), bValid(true) {} @@ -358,8 +365,8 @@ bool LTFlightData::outdated (double simTime) const // i.e. during approach and landing we don't destroy the aircraft // until it finally stopped on the runway if (pAc && - pAc->GetFlightPhase() >= LTAircraft::FPH_APPROACH && - pAc->GetFlightPhase() < LTAircraft::FPH_STOPPED_ON_RWY) + pAc->GetFlightPhase() >= FPH_APPROACH && + pAc->GetFlightPhase() < FPH_STOPPED_ON_RWY) { return false; } @@ -421,7 +428,7 @@ std::string LTFlightData::ComposeLabel() const ADD_LABEL_NUM(cfg.bHeading, pos.heading()); ADD_LABEL_NUM(cfg.bAlt, pos.alt_ft()); if (cfg.bHeightAGL) { - label += pAc->IsOnGrnd() ? positionTy::GrndE2String(positionTy::GND_ON) : + label += pAc->IsOnGrnd() ? positionTy::GrndE2String(GND_ON) : std::to_string(long(pAc->GetPHeight_ft())); trim(label); label += ' '; @@ -450,6 +457,53 @@ std::string LTFlightData::ComposeLabel() const // Data Cleansing of the buffered positions (called from CalcNextPos) void LTFlightData::DataCleansing (bool& bChanged) { + // nothing to cleanse? + if (posDeque.empty()) + return; + + // The flight model to use + const LTAircraft::FlightModel& mdl = pAc ? pAc->mdl : + LTAircraft::FlightModel::FindFlightModel(statData.acTypeIcao); + + // *** Keep last pos in posDeque above 2.5° ILS path + // Relevant if: + // - airborne + // - descending + if (posDeque.size() >= 2) + { + positionTy& last = posDeque.back(); + const positionTy& prev = *std::prev(posDeque.cend(),2); + double terrain_alt_m = pAc ? pAc->GetTerrainAlt_m() : NAN; + if (!std::isnan(last.alt_m()) && // do we have an altitude at all? + last.alt_m() <= KEEP_ABOVE_MAX_ALT && // not way too high (this skips planes which are just cruising + (std::isnan(terrain_alt_m) || (last.alt_m() - terrain_alt_m) < KEEP_ABOVE_MAX_AGL) && // pos not too high AGL + prev.vsi_ft(last) < -mdl.VSI_STABLE) // sinking considerably (this also skips taxiing on the ground as during taxiing we aren't sinking) + { + // Try to find a rwy this plane might be headed for + // based on the last known position + posRwy = LTAptFindRwy(mdl, last, prev.speed_m(last)); + if (posRwy.isNormal()) { // found a suitable runway? + // Now, with this runway, check/correct all previous positions + for (positionTy& pos: posDeque) { + const double dist = DistLatLon(pos.lat(), pos.lon(), + posRwy.lat(), posRwy.lon()); + // Are we flying below the 2.5° glidescope? ("- 0.5" to avoid rounding problems) + if (pos.alt_m() - posRwy.alt_m() < dist * KEEP_ABOVE_RATIO - 0.5) { + // Fix it! + const double old_alt_ft = pos.alt_ft(); + pos.alt_m() = posRwy.alt_m() + dist * KEEP_ABOVE_RATIO; + pos.f.onGrnd = GND_OFF; // we even lift ground positions into the air! + bChanged = true; + if (dataRefs.GetDebugAcPos(key())) { + LOG_MSG(logDEBUG, DBG_KEEP_ABOVE, + old_alt_ft, pos.dbgTxt().c_str()); + } + } + } + } + } + } + // // *** Remove weird positions *** // @@ -499,16 +553,16 @@ void LTFlightData::DataCleansing (bool& bChanged) if (IsPosOK(pos1, *next, &h2, &bChanged)) { bFoundValidNext = true; - if constexpr (VERSION_BETA) { - LOG_MSG(logDEBUG, "%s: Valid next pos: %s", - keyDbg().c_str(), - next->dbgTxt().c_str() ); - LOG_MSG(logDEBUG, Positions2String().c_str() ); - } +// if constexpr (VERSION_BETA) { +// LOG_MSG(logDEBUG, "%s: Valid next pos: %s", +// keyDbg().c_str(), +// next->dbgTxt().c_str() ); +// LOG_MSG(logDEBUG, Positions2String().c_str() ); +// } // that means we need to remove all positions from // 'iter' to _before_ next. // BUT because std::deque::erase can invalidate _all_ iterators - // (including cend!) we cannot do it in one go...after the first erase all iterators are invalid. + // (including next and cend!) we cannot do it in one go...after the first erase all iterators are invalid. // Make use of the fact that the deque is sorted by timestamp. const double rmTsFrom = iter->ts(); const double rmTsTo = next->ts(); @@ -516,11 +570,6 @@ void LTFlightData::DataCleansing (bool& bChanged) { // if current iter falls into the to-be-deleted range if (rmTsFrom <= iter->ts() && iter->ts() < rmTsTo) { - if constexpr (VERSION_BETA) { - LOG_MSG(logDEBUG, DBG_INV_POS_REMOVED, - keyDbg().c_str(), - iter->dbgTxt().c_str()); - } posDeque.erase(iter); // now all iterators are invalid! bChanged = true; iter = posDeque.begin(); @@ -559,7 +608,7 @@ void LTFlightData::DataCleansing (bool& bChanged) // to reappear) if (posDeque.size() <= 1) { // that's the only pos: remove it - if constexpr (VERSION_BETA) { + if (dataRefs.GetDebugAcPos(key())) { LOG_MSG(logDEBUG, DBG_INV_POS_REMOVED, keyDbg().c_str(), iter->dbgTxt().c_str()); @@ -574,9 +623,9 @@ void LTFlightData::DataCleansing (bool& bChanged) // -> remove the aircraft, will be recreated at // the new pos later automatically pAc->SetInvalid(); - if constexpr (VERSION_BETA) { - LOG_MSG(logDEBUG, Positions2String().c_str()); - } +// if constexpr (VERSION_BETA) { +// LOG_MSG(logDEBUG, Positions2String().c_str()); +// } LOG_MSG(logDEBUG, DBG_INV_POS_AC_REMOVED, keyDbg().c_str()); } @@ -585,9 +634,9 @@ void LTFlightData::DataCleansing (bool& bChanged) // stop cleansing break; } // if not found any valid next position - else if constexpr (VERSION_BETA) { - LOG_MSG(logDEBUG, Positions2String().c_str() ); - } +// else if constexpr (VERSION_BETA) { +// LOG_MSG(logDEBUG, Positions2String().c_str() ); +// } } // if invalid pos else { @@ -611,12 +660,11 @@ void LTFlightData::DataCleansing (bool& bChanged) const LTChannel* pChn = nullptr; if (pAc && !posDeque.empty() && - LTAircraft::FPH_APPROACH <= pAc->GetFlightPhase() && - pAc->GetFlightPhase() < LTAircraft::FPH_LANDING && + FPH_APPROACH <= pAc->GetFlightPhase() && + pAc->GetFlightPhase() < FPH_LANDING && GetCurrChannel(pChn) && pChn->DoHoverDetection()) { // We have a plane which is in approach. - const LTAircraft::FlightModel& mdl = pAc->mdl; const double maxHoverAlt_m = pAc->GetTerrainAlt_m() + (MAX_HOVER_AGL * M_per_FT); // What we now search for is data at level altitude following a descend. @@ -645,7 +693,7 @@ void LTFlightData::DataCleansing (bool& bChanged) std::abs(iter->vsi_ft(prevPos)) <= mdl.VSI_STABLE) // and flying level { // remove that hovering position - if constexpr (VERSION_BETA) { + if (dataRefs.GetDebugAcPos(key())) { LOG_MSG(logDEBUG, DBG_HOVER_POS_REMOVED, keyDbg().c_str(), iter->dbgTxt().c_str()); @@ -684,9 +732,9 @@ void LTFlightData::DataSmoothing (bool& bChanged) // most important: leaving allowed smoothing range (in seconds) if (itLast->ts() - posFirst.ts() > tsRange || // don't smooth across gnd status changes - itLast->onGrnd != posFirst.onGrnd || + itLast->f.onGrnd != posFirst.f.onGrnd || // don't smooth across artifically calculated positions - itLast->flightPhase != LTAircraft::FPH_UNKNOWN) + itLast->f.flightPhase != FPH_UNKNOWN) break; } // we went one too far...so how far did we go into the deque? @@ -760,10 +808,10 @@ void LTFlightData::SnapToTaxiways (bool& bChanged) // Only act on positions on the ground, // which have (not yet) been artificially added positionTy& pos = *iter; - if (pos.IsOnGnd() && pos.flightPhase == 0) + if (pos.IsOnGnd() && !pos.IsPostProcessed()) { // Try snapping to a rwy or taxiway - if (LTAptSnap(pos, dataRefs.GetDebugAcPos(key()))) + if (LTAptSnap(*this, iter, true)) bChanged = true; } // non-artificial ground position @@ -788,10 +836,6 @@ bool LTFlightData::CalcNextPos ( double simTime ) // access guarded by a mutex std::lock_guard lock (dataAccessMutex); - // if our buffer of positions is completely empty we can't do much - if ( posDeque.empty() || dynDataDeque.empty() ) - return false; - // *** maintenance of flight data deque *** const LTAircraft::FlightModel& mdl = pAc ? pAc->mdl : LTAircraft::FlightModel::FindFlightModel(statData.acTypeIcao); @@ -831,7 +875,7 @@ bool LTFlightData::CalcNextPos ( double simTime ) if (pAc->GetVSI_ft() < -pAc->mdl.VSI_STABLE) { const positionTy& acTo = pAc->GetToPos(); - positionTy posRwy = LTAptFindRwy(*pAc); + posRwy = LTAptFindRwy(*pAc, dataRefs.GetDebugAcPos(key())); if (posRwy.isNormal()) { // found a landing spot! // If it is 'far' away in terms of time then we don't add it @@ -849,13 +893,14 @@ bool LTFlightData::CalcNextPos ( double simTime ) { // shorten the distance so it only takes as long as a refresh interval vecRwy.dist *= (double)dataRefs.GetFdRefreshIntvl() / d_ts; - posRwy = acTo + vecRwy; - posRwy.flightPhase = LTAircraft::FPH_APPROACH; + positionTy posInterm = acTo + vecRwy; + posInterm.f.flightPhase = FPH_APPROACH; // Add the it to the queue - LOG_MSG(logDEBUG, "%s: Added intermediate %s", - keyDbg().c_str(), - std::string(posRwy).c_str()); - posDeque.emplace_back(std::move(posRwy)); + if (dataRefs.GetDebugAcPos(key())) + LOG_MSG(logDEBUG, "%s: Added intermediate %s", + keyDbg().c_str(), + std::string(posInterm).c_str()); + posDeque.emplace_back(std::move(posInterm)); } else { // The final leg down onto the runway. // Little trick here: We add 2 stops to make sure @@ -872,22 +917,42 @@ bool LTFlightData::CalcNextPos ( double simTime ) // but posBefore is _before_ posRwy: posBefore.ts() -= 2 * (posBefore.ts() - posRwy.ts()); posBefore.pitch() = 0.0; - posBefore.onGrnd = positionTy::GND_OFF; - posBefore.flightPhase = LTAircraft::FPH_FINAL; + posBefore.f.onGrnd = GND_OFF; + posBefore.f.flightPhase = FPH_FINAL; // Add both position to the queue - LOG_MSG(logDEBUG, "%s: Added final %s", - keyDbg().c_str(), - std::string(posBefore).c_str()); + if (dataRefs.GetDebugAcPos(key())) + LOG_MSG(logDEBUG, "%s: Added final %s", + keyDbg().c_str(), + std::string(posBefore).c_str()); posDeque.emplace_back(std::move(posBefore)); - LOG_MSG(logDEBUG, "%s: Added touch-down %s", - keyDbg().c_str(), - std::string(posRwy).c_str()); - posDeque.emplace_back(std::move(posRwy)); + if (dataRefs.GetDebugAcPos(key())) + LOG_MSG(logDEBUG, "%s: Added touch-down %s", + keyDbg().c_str(), + std::string(posRwy).c_str()); + posDeque.push_back(posRwy); // make a copy, we want to keep posRwy! } bChanged = true; } } + // No more positions on the ground: Make the a/c stop + // by adding the last known position just once again as artifical stop. + else if (pAc->IsOnGrnd()) { + positionTy stopPos = pAc->GetToPos(); + if (stopPos.IsOnGnd() && + stopPos.f.flightPhase != FPH_TOUCH_DOWN && // don't copy touch down pos, that looks ugly, and hinders auto-land/stop + stopPos.f.flightPhase != FPH_STOPPED_ON_RWY) // avoid adding several stops + { + stopPos.ts() += simTime + 10.0; // just assume _some_ time + stopPos.f.flightPhase = FPH_STOPPED_ON_RWY; // indicator for aritifical stop (not only on rwy now...) + if (dataRefs.GetDebugAcPos(key())) + LOG_MSG(logDEBUG, "%s: Added stop-position %s", + keyDbg().c_str(), + std::string(stopPos).c_str()); + posDeque.emplace_back(std::move(stopPos)); // add it to the deque + bChanged = true; + } + } // still no positions left? if (posDeque.empty()) @@ -904,16 +969,6 @@ bool LTFlightData::CalcNextPos ( double simTime ) if (simTime < posDeque.front().ts()) return false; - // The first pos is in the past, good, make sure it's the only one - // [0] <= simTime < [1] - while (posDeque.size() >= 2 && posDeque[1].ts() <= simTime) { - posDeque.pop_front(); - bChanged = true; - } - - // Unlikely, but theoretically there could now be just one (past) pos left - if (posDeque.size() < 2) - return false; } // *** Data Cleansing *** @@ -959,18 +1014,23 @@ bool LTFlightData::CalcNextPos ( double simTime ) // but only reasonably a _new_ position if between to pos and next // with some minima distance if (timeToTouchDown > TIME_REQU_POS && - tsOfTouchDown + TIME_REQU_POS < next.ts()) { - vectorTy vecTouch(pAc->GetTrack(), // angle: current flight path + tsOfTouchDown + TIME_REQU_POS < next.ts()) + { + vectorTy vecTouch(toPos_ac.angle(next), // angle: as per last leg timeToTouchDown * pAc->GetSpeed_m_s(), // distance pAc->GetVSI_m_s(), // vsi pAc->GetSpeed_m_s()); // speed // insert touch-down point at beginning of posDeque positionTy& touchDownPos = posDeque.emplace_front(toPos_ac.destPos(vecTouch)); - touchDownPos.onGrnd = positionTy::GND_ON; - touchDownPos.flightPhase = LTAircraft::FPH_TOUCH_DOWN; + touchDownPos.f.onGrnd = GND_ON; + touchDownPos.f.flightPhase = FPH_TOUCH_DOWN; touchDownPos.alt_m() = NAN; // will set correct terrain altitude during TryFetchNewPos + // Snap the touch down pos to the rwy: + dequePositionTy::iterator iter = posDeque.begin(); + LTAptSnap(*this, iter, false); + // output debug info on request if (dataRefs.GetDebugAcPos(key())) { LOG_MSG(logDEBUG,DBG_INVENTED_TD_POS,touchDownPos.dbgTxt().c_str()); @@ -979,16 +1039,15 @@ bool LTFlightData::CalcNextPos ( double simTime ) else { // not enough distance to 'next', so we declare 'next' the landing spot - next.flightPhase = LTAircraft::FPH_TOUCH_DOWN; + next.f.flightPhase = FPH_TOUCH_DOWN; } - // Remove positions down the runway. + // Remove positions down the runway until the last RWY position // That allows for a better deceleration simulation. - // We remove all positions on the current track, - // i.e. as long as they point along the current track +/- 3° while (posDeque.size() > 2 && // keep at least two positions posDeque[1].IsOnGnd() && - abs(HeadingDiff(posDeque[1].heading(),pAc->GetTrack())) <= MDL_SAME_TRACK_DIFF) + posDeque[1].f.specialPos == SPOS_RWY && + posDeque[2].f.specialPos == SPOS_RWY) { // remove the second element (first is the just inserted touch-down pos) posDeque.erase(std::next(posDeque.begin())); @@ -1032,7 +1091,7 @@ bool LTFlightData::CalcNextPos ( double simTime ) positionTy& to_i = posDeque[i]; const double to_i_ts = to_i.ts(); // the reference might become invalid later once we start erasing, so we copy this timestamp that we need - // we look up to 35s into the future + // we look up to 60s into the future if (ppos_i.ts() > simTime + MDL_TO_LOOK_AHEAD) break; @@ -1072,13 +1131,14 @@ bool LTFlightData::CalcNextPos ( double simTime ) // insert take-off point ('to' minus vector from take-off to 'to') // at beginning of posDeque positionTy takeOffPos = to_i.destPos(vecTO); - takeOffPos.onGrnd = positionTy::GND_ON; - takeOffPos.flightPhase = LTAircraft::FPH_LIFT_OFF; + takeOffPos.f.onGrnd = GND_ON; + takeOffPos.f.flightPhase = FPH_LIFT_OFF; takeOffPos.alt_m() = NAN; // TryFetchNewPos will calc terrain altitude takeOffPos.heading() = vec.angle; // from 'reverse' back to forward takeOffPos.ts() = takeOffTS; // ts was computed forward...we need it backward // find insert position, remove on-runway positions along the way + bool bDelRwyPos = false; dequePositionTy::iterator toIter = posDeque.end(); for (dequePositionTy::iterator iter = posDeque.begin(); iter != posDeque.end(); @@ -1086,20 +1146,23 @@ bool LTFlightData::CalcNextPos ( double simTime ) { // before take off... if (*iter < takeOffPos) { - // If this pos's heading is the same as for take off we just remove it. + // Keep the first RWY position, but remove any later RWY positions, // Which allows the accelerate algorithm to accelerate all the distance to take-off point - if (abs(HeadingDiff(iter->heading(), vec.angle)) <= MDL_SAME_TRACK_DIFF) { - iter = posDeque.erase(iter); - continue; // start over loop with next element after the erased one + if (iter->f.specialPos == SPOS_RWY) { + if (bDelRwyPos) { + iter = posDeque.erase(iter); + continue; // start over loop with next element after the erased one + } + bDelRwyPos = true; // any further RWY positions can be deleted } // before take off we stay on the ground if (!iter->IsOnGnd()) { - iter->onGrnd = positionTy::GND_ON; + iter->f.onGrnd = GND_ON; iter->alt_m() = NAN; // TryFetchNewPos will calc terrain altitude } } else { - // found insert position! + // found insert position! Insert and snap it to the rwy toIter = posDeque.insert(iter, takeOffPos); break; } @@ -1147,6 +1210,27 @@ bool LTFlightData::CalcNextPos ( double simTime ) } // loop over szenarios } // (has a/c and do landing / take-off detection) + // *** Snap any newly inserted positions to taxiways *** + if (bChanged) + SnapToTaxiways(bChanged); + + // A lot might have changed now, even added. + // If there is no aircraft yet then we need to "normalize" + // to creation conditions: One pos in the past, the next in the future + if ( !pAc ) + { + // The first pos is in the past, good, make sure it's the only one + // [0] <= simTime < [1] + while (posDeque.size() >= 2 && posDeque[1].ts() <= simTime) { + posDeque.pop_front(); + bChanged = true; + } + + // Unlikely, but theoretically there could now be just one (past) pos left + if (posDeque.size() < 2) + return false; + } + // if something changed if (bChanged) { // recalc all headings @@ -1287,19 +1371,35 @@ void LTFlightData::TriggerCalcNewPos ( double simTime ) // flight data. void LTFlightData::CalcHeading (dequePositionTy::iterator it) { + // skip any fiddling with the heading in case it is fixed + if (it->f.bHeadFixed) + return; + // vectors to / from the position at "it" vectorTy vecTo, vecFrom; // is there a predecessor to "it"? if (it != posDeque.cbegin()) { - vecTo = std::prev(it)->between(*it); - if (vecTo.dist < SIMILAR_POS_DIST) // clear the vector if too short - vecTo = vectorTy(); + const positionTy& prePos = *std::prev(it); + vecTo = prePos.between(*it); + if (vecTo.dist < SIMILAR_POS_DIST) // distance from predecessor to it too short + { + it->heading() = prePos.heading(); // by default don't change heading for this short distance to avoid turning planes "on the spot" + if (!std::isnan(it->heading())) // if we now have a heading -> just use it + return; + vecTo = vectorTy(); // clear the vector + } } else if (pAc) { // no predecessor in the queue...but there is an a/c, take that - vecTo = pAc->GetToPos().between(*it); - if (vecTo.dist < SIMILAR_POS_DIST) // clear the vector if too short - vecTo = vectorTy(); + const positionTy& prePos = pAc->GetToPos(); + vecTo = prePos.between(*it); + if (vecTo.dist < SIMILAR_POS_DIST) // distance from predecessor to it too short + { + it->heading() = prePos.heading(); // by default don't change heading for this short distance to avoid turning planes "on the spot" + if (!std::isnan(it->heading())) // if we now have a heading -> just use it + return; + vecTo = vectorTy(); // clear the vector + } } // is there a successor to it? @@ -1369,8 +1469,9 @@ bool LTFlightData::IsPosOK (const positionTy& lastPos, MDL_MAX_TURN_GND : MDL_MAX_TURN); // angle between last and this, i.e. turn angle at thisPos - const double hDiff = (std::isnan(lastHead) ? 0 : - v.dist <= SIMILAR_POS_DIST ? 0 : + const double hDiff = (std::isnan(lastHead) ? 0.0 : + lastPos.f.bHeadFixed || thisPos.f.bHeadFixed ? 0.0 : + v.dist <= SIMILAR_POS_DIST ? 0.0 : HeadingDiff(lastHead, v.angle)); // Too much of a turn? VSI/speed out of range? @@ -1414,6 +1515,9 @@ void LTFlightData::AddNewPos ( positionTy& pos ) // (we shall not do Y probes but need accurate GND info...) posToAdd.emplace_back(pos); flagNoNewPosToAdd.clear(); + + if (dataRefs.GetDebugAcPos(key())) + LOG_MSG(logDEBUG,DBG_ADDED_NEW_POS,pos.dbgTxt().c_str()); } catch(const std::system_error& e) { LOG_MSG(logERR, ERR_LOCK_ERROR, key().c_str(), e.what()); } @@ -1497,7 +1601,8 @@ void LTFlightData::AppendNewPos() if (i != posDeque.end()) { // found merge partner! // make sure we don't overlap with predecessor/successor position if (((i == posDeque.begin()) || (*std::prev(i) < pos)) && - ((std::next(i) == posDeque.end()) || (*std::next(i) > pos))) + ((std::next(i) == posDeque.end()) || (*std::next(i) > pos)) && + !i->IsPostProcessed()) // don't merge with optimized positions...rather throw the new one away { *i |= pos; // merge them (if pos.heading is nan then i.heading prevails) if (dataRefs.GetDebugAcPos(key())) @@ -1561,23 +1666,6 @@ void LTFlightData::AppendNewPos() // *** heading *** - // Most calculations and data cleansing actions base on timestamp. - // Here for heading we do one early check based on _distance_ - // between adjacent positions: If the new position i / p is - // near (SIMILAR_POS_DIST) to std::prev(i) then we override - // i's heading with that of prev(i) to prevent 'dancing' planes - // on the spot. - // This has beed added when taking RealTraffic on board, which - // delivers 'dancing' track/heading values for stationary planes. - // There is no way of finding the 'right' heading in these cases, - // the plane points anywhere...but at least it doesn't dance. - if (i != posDeque.begin()) { // is there anything before i? - const positionTy& prevP = *std::prev(i); - const vectorTy vec = prevP.between(p); - if (vec.dist <= SIMILAR_POS_DIST) - p.heading() = prevP.heading(); - } - // Recalc heading of adjacent positions: before p, p itself, and after p if (i != posDeque.begin()) // is there anything before i? CalcHeading(std::prev(i)); @@ -1589,7 +1677,7 @@ void LTFlightData::AppendNewPos() // *** pitch *** // just a rough value, LTAircraft::CalcPPos takes care of the details - if (p.onGrnd) + if (p.IsOnGnd()) p.pitch() = 0; else p.pitch() = 2; @@ -1643,7 +1731,7 @@ LTFlightData::tryResult LTFlightData::TryFetchNewPos (dequePositionTy& acPosList // so we take our chance to determine proper terrain altitudes for (positionTy& pos: posDeque) { if ((pos.IsOnGnd() && std::isnan(pos.alt_m())) || // GND_ON but alt unknown - pos.onGrnd == positionTy::GND_UNKNOWN) { // GND_UNKNOWN + pos.f.onGrnd == GND_UNKNOWN) { // GND_UNKNOWN TryDeriveGrndStatus(pos); } } @@ -1652,25 +1740,28 @@ LTFlightData::tryResult LTFlightData::TryFetchNewPos (dequePositionTy& acPosList if (!pAc) { // there must be two positions, one in the past, one in the future! LOG_ASSERT_FD(*this, validForAcCreate()); - // copy the first two positions, so that the a/c can start flying from/to - acPosList.emplace_back(posDeque[0]); - acPosList.emplace_back(posDeque[1]); + // move the first two positions to the a/c, so that the a/c can start flying from/to + acPosList.emplace_back(std::move(posDeque.front())); + posDeque.pop_front(); + acPosList.emplace_back(std::move(posDeque.front())); + posDeque.pop_front(); } else { - // there is an a/c...only copy stuff past current 'to'-pos + // there is an a/c...only use stuff past current 'to'-pos const positionTy& to = pAc->GetToPos(); LOG_ASSERT_FD(*this, !std::isnan(to.ts())); - // find the first position beyond current 'to' (is usually right away the first one!) - dequePositionTy::const_iterator i = - std::find_if(posDeque.cbegin(), posDeque.cend(), - [&to](const positionTy& p){return to < p;}); + // Remove outdated positions from posDeque, + // ie. all positions before 'to' + while (!posDeque.empty() && posDeque.front() < to) + posDeque.pop_front(); - // nothing??? - if (i == posDeque.cend()) + // nothing left??? + if (posDeque.empty()) return TRY_NO_DATA; - // add that next position to the a/c - acPosList.emplace_back(*i); + // move that next position to the a/c + acPosList.emplace_back(std::move(posDeque.front())); + posDeque.pop_front(); } // store rotate timestamp if there is one (never overwrite with NAN!) @@ -1710,9 +1801,9 @@ bool LTFlightData::TryDeriveGrndStatus (positionTy& pos) // If position already says itself: I'm on the ground, then keep it like that // Otherwise decide based on altitude _if_ it's on the ground if (!pos.IsOnGnd() && - // say it's on the ground if below terrain+10ft + // say it's on the ground if below terrain+50ft pos.alt_m() < terrainAlt + FD_GND_AGL) - pos.onGrnd = positionTy::GND_ON; + pos.f.onGrnd = GND_ON; // if it was or now is on the ground correct the altitue to terrain altitude // (very slightly below to be sure to actually touch down even after rounding effects) @@ -1720,7 +1811,7 @@ bool LTFlightData::TryDeriveGrndStatus (positionTy& pos) pos.alt_m() = terrainAlt - MDL_CLOSE_TO_GND; else // make sure it's either GND_ON or GND_OFF, nothing lese - pos.onGrnd = positionTy::GND_OFF; + pos.f.onGrnd = GND_OFF; // successfully determined a status return true; @@ -1740,24 +1831,23 @@ double LTFlightData::YProbe_at_m (const positionTy& pos) } // returns vector at timestamp (which has speed, direction and the like) -LTFlightData::tryResult LTFlightData::TryGetVec (double ts, vectorTy& vec) const +LTFlightData::tryResult LTFlightData::TryGetNextPos (double ts, positionTy& pos) const { try { std::unique_lock lock (dataAccessMutex, std::try_to_lock ); if ( lock ) { - // find positions around timestamp + // find first posititon _after_ ts dequePositionTy::const_iterator i = - std::adjacent_find(posDeque.cbegin(),posDeque.cend(), - [ts](const positionTy& a, const positionTy& b) - {return a.ts() <= ts && ts <= b.ts();}); + std::find_if(posDeque.cbegin(),posDeque.cend(), + [ts](const positionTy& p){return p.ts() > ts;}); - // no pair of positions found -> can't compute vector + // no positions found -> no data! if (i == posDeque.cend()) return TRY_NO_DATA; - // found a pair, return the vector between them - vec = i->between(*(std::next(i))); + // return the position + pos = *i; return TRY_SUCCESS; } else @@ -2037,17 +2127,6 @@ void LTFlightData::UpdateData (const LTFlightData::FDStaticData& inStat) // (a/c type, operator, registration) bool bMdlInfoChange = false; - // a/c type: is empty and new data has a type? - // or: is currently just default and new one is something non-default? - if (!inStat.acTypeIcao.empty() && - (statData.acTypeIcao.empty() || - (statData.acTypeIcao == dataRefs.GetDefaultAcIcaoType() && - inStat.acTypeIcao != dataRefs.GetDefaultAcIcaoType()))) - { - statData.acTypeIcao = inStat.acTypeIcao; - bMdlInfoChange = true; - } - // operator ICAO: we only accept a change from nothing to something if (statData.opIcao.empty() && !inStat.opIcao.empty()) { @@ -2062,14 +2141,18 @@ void LTFlightData::UpdateData (const LTFlightData::FDStaticData& inStat) bMdlInfoChange = true; } - // if model-defining fields changed then (potentially) change the CSL model - if (bMdlInfoChange && pAc) { - pAc->ChangeModel (statData); - } + // Re-determine a/c model (only if it was determined before: + // the first determination shall be made as late as possible + // in LTFlightData::CreateAircraft()) + if (pAc && DetermineAcModel()) + bMdlInfoChange = true; - // tell the aircraft to report new info data, e.g. to multiplayer clients - if (pAc) + // if model-defining fields changed then (potentially) change the CSL model + if (pAc) { + if (bMdlInfoChange) + pAc->ChangeModel (statData); pAc->SetSendNewInfoData(); + } // update the static parts of the label UpdateStaticLabel(); @@ -2174,22 +2257,128 @@ bool LTFlightData::AircraftMaintenance ( double simTime ) } +// try interpreting model text or check for ground vehicle +bool LTFlightData::DetermineAcModel() +{ + // We don't change the a/c type if it is already something reasonable + const std::string prevType = statData.acTypeIcao; + if (!prevType.empty() && + prevType != dataRefs.GetDefaultAcIcaoType() && + prevType != dataRefs.GetDefaultCarIcaoType()) + return false; + + // Try finding a CSL model by interpreting the human-readable model text + std::string mdl (statData.mdl); + str_toupper(trim(mdl)); + statData.acTypeIcao = ModelIcaoType::getIcaoType(mdl); + if ( !statData.acTypeIcao.empty() ) + { + // yea, found something by mdl! + if (prevType != statData.acTypeIcao) { + LOG_MSG(logWARN,ERR_NO_AC_TYPE_BUT_MDL, + key().c_str(), + statData.man.c_str(), statData.mdl.c_str(), + statData.acTypeIcao.c_str()); + return true; + } + return false; + } + + // Ground vehicle maybe? Shall be on the ground then + if (( + (pAc && pAc->IsOnGrnd() && pAc->GetSpeed_kt() < pAc->mdl.MAX_TAXI_SPEED) || + (!posDeque.empty() && posDeque.front().IsOnGnd()) + ) && + // OpenSky only delivers "category description" and has a + // pretty clear indicator for a ground vehicle + (statData.catDescr.find(OPSKY_MD_TEXT_VEHICLE) != std::string::npos || + // I'm having the feeling that if nearly all is empty and the category description is "No Info" then it's often also a ground vehicle + (statData.catDescr.find(OPSKY_MD_TEX_NO_CAT) != std::string::npos && + statData.man.empty() && statData.mdl.empty() && statData.opIcao.empty()) || + // ADSBEx doesn't send as clear an indicator, but data analysis + // suggests that EngType/Mount == 0 is a good indicator + (statData.engType == 0 && statData.engMount == 0) || + // for RealTraffic it is even more difficult...we best identify RT-only data with no operator (opIcao is taken from call sign) + (statData.op.empty() && statData.reg.empty() && statData.destAp.empty())) + ) + { + // assume surface vehicle + statData.acTypeIcao = dataRefs.GetDefaultCarIcaoType(); + return prevType != statData.acTypeIcao; + } + + // we have no better idea than standard + statData.acTypeIcao = dataRefs.GetDefaultAcIcaoType(); + if (prevType != statData.acTypeIcao) + { + LOG_MSG(logWARN,ERR_NO_AC_TYPE, + key().c_str(), + statData.man.c_str(), statData.mdl.c_str(), + statData.acTypeIcao.c_str()); + return true; + } + return false; +} + +// checks if there is a slot available to create this a/c, tries to remove the farest a/c if too many a/c rendered +/// @warning Caller must own `mapFdMutex`! +bool LTFlightData::AcSlotAvailable (double simTime) +{ + // time we had shown the "Too many a/c" warning last: + static double tTooManyAcMsgShown = 0.0; + + // Haven't reach the limit in terms of number of a/c yet? + if (dataRefs.GetNumAc() < dataRefs.GetMaxNumAc()) + return true; + + // Have no positions? (Need one to determine distance to camera) + if (posDeque.empty()) + return false; + + // Now we need to see if we are closer to the camera than other a/c. + // If so remove the farest a/c to make room for us. + LTFlightData* pFarestAc = nullptr; + + // NOTE: We can loop mapFd without lock only because we assume that + // calling function owns mapFdMutex already! + // find the farest a/c...if it is further away than us: + double farestDist = CoordDistance(dataRefs.GetViewPos(), posDeque.front()); + for (mapLTFlightDataTy::value_type& p: mapFd) + { + LTFlightData& fd = p.second; + if (fd.hasAc() && fd.pAc->GetVecView().dist > farestDist) { + farestDist = fd.pAc->GetVecView().dist; + pFarestAc = &fd; + } + } + + // So we definitely have too many aircraft! + if (tTooManyAcMsgShown + 180.0 < simTime) { // Show message at most every 3 minutes + SHOW_MSG(logWARN,MSG_TOO_MANY_AC,dataRefs.GetMaxNumAc()); + tTooManyAcMsgShown = simTime; + } + + // If we didn't find an active a/c farther away than us then bail with message + if (!pFarestAc) { + return false; + } + + // We found the a/c farest away...remove it! + pFarestAc->DestroyAircraft(); + return true; +} + + // create (at most one) aircraft from this flight data bool LTFlightData::CreateAircraft ( double simTime ) { - static bool bTooManyAcMsgShown = false; - // short-cut if exists already if ( hasAc() ) return true; - // short-cut if too many aircraft created already - if ( dataRefs.GetNumAc() >= dataRefs.GetMaxNumAc() ) { - if ( !bTooManyAcMsgShown ) // show warning once only per session - SHOW_MSG(logWARN,MSG_TOO_MANY_AC,dataRefs.GetMaxNumAc()); - bTooManyAcMsgShown = true; + // exit if too many a/c shown and this one wouldn't be one of the nearest ones + if (!AcSlotAvailable(simTime)) return false; - } try { // get the mutex, not so much for protection, @@ -2202,56 +2391,15 @@ bool LTFlightData::CreateAircraft ( double simTime ) if ( !CalcNextPos(simTime) ) return false; - // If we still have no acTypeIcao we can try a lookup by model text - if ( statData.acTypeIcao.empty() ) - { - std::string mdl (statData.mdl); - str_toupper(trim(mdl)); - if ( !(statData.acTypeIcao = ModelIcaoType::getIcaoType(mdl)).empty() ) - { - // yea, found something by mdl! - LOG_MSG(logWARN,ERR_NO_AC_TYPE_BUT_MDL, - key().c_str(), - statData.man.c_str(), statData.mdl.c_str(), - statData.acTypeIcao.c_str()); - } - } + // This can have change data in the posDeque...let's see if we are still valid for a/c create + // Remove outdated positions from posDeque, ie. all positions before simTime + while (posDeque.size() >= 2 && posDeque[1].ts() <= simTime) + posDeque.pop_front(); + if ( !validForAcCreate(simTime) ) + return false; - // a few last checks and decisions, e.g. now we definitely do need a plane type - if ( statData.acTypeIcao.empty() ) - { - // this is gonna be the first 'from' position - LOG_ASSERT_FD(*this, !posDeque.empty()) - const positionTy& firstPos = posDeque.front(); - - // Ground vehicle maybe? Shall be on the ground then - if (firstPos.IsOnGnd() && - // OpenSky only delivers "category description" and has a - // pretty clear indicator for a ground vehicle - (statData.catDescr.find(OPSKY_MD_TEXT_VEHICLE) != std::string::npos || - // I'm having the feeling that if nearly all is empty and the category description is "No Info" then it's often also a ground vehicle - (statData.catDescr.find(OPSKY_MD_TEX_NO_CAT) != std::string::npos && - statData.man.empty() && statData.mdl.empty() && statData.opIcao.empty()) || - // ADSBEx doesn't send as clear an indicator, but data analysis - // suggests that EngType/Mount == 0 is a good indicator - (statData.engType == 0 && statData.engMount == 0) || - // for RealTraffic it is even more difficult...we best identify RT-only data with no operator (opIcao is taken from call sign) - (statData.op.empty() && statData.reg.empty() && statData.destAp.empty())) - ) - { - // assume surface vehicle - statData.acTypeIcao = dataRefs.GetDefaultCarIcaoType(); - } - else - { - // we have no better idea than standard - statData.acTypeIcao = dataRefs.GetDefaultAcIcaoType(); - LOG_MSG(logWARN,ERR_NO_AC_TYPE, - key().c_str(), - statData.man.c_str(), statData.mdl.c_str(), - statData.acTypeIcao.c_str()); - } - } + // Make sure we have a valid a/c model now + DetermineAcModel(); // create the object (constructor will recursively re-access the lock) try { @@ -2344,6 +2492,29 @@ const LTFlightData* LTFlightData::FindFocusAc (const double bearing) return ret; } +#ifdef DEBUG +// This helps focusing on one aircraft and debug through the position calculation code +void LTFlightData::RemoveAllAcButSelected () +{ + // access guarded by the fd mutex + std::lock_guard lock (mapFdMutex); + + // hard and directly remove all other aircraft without any further ado + for (mapLTFlightDataTy::iterator i = mapFd.begin(); + i != mapFd.end();) + { + if (!i->second.bIsSelected) + i = mapFd.erase(i); + else + ++i; + } + + // reduce allow a/c to 1 so no new aircraft gets created + dataRefs.SetMaxNumAc(1); +} +#endif + + // // MARK: mapLTFlightDataTy // diff --git a/Src/LTMain.cpp b/Src/LTMain.cpp index 7c96de6b..97a59a88 100644 --- a/Src/LTMain.cpp +++ b/Src/LTMain.cpp @@ -99,7 +99,7 @@ std::istream& safeGetline(std::istream& is, std::string& t) std::getline(is, t, '\n'); // if last character is CR then remove it - if (t.back() == '\r') + if (!t.empty() && t.back() == '\r') t.pop_back(); return is; @@ -238,8 +238,6 @@ float LoopCBAircraftMaintenance (float inElapsedSinceLastCall, float, int, void* // *** check for new positons that require terrain altitude (Y Probes) *** // LiveTraffic Top Level Exception handling: catch all, reinit if something happens try { - // Check for changed reference point of local coordinate system - HandleRefPointChanged(); // handle new network data (that func has a short-cut exit if nothing to do) LTFlightData::AppendAllNewPos(); @@ -373,18 +371,6 @@ std::string NextValidCSLPath (DataRefs::vecCSLPaths::const_iterator& cslIter, return std::string(); } -/// @details Called during a flight loop callback in case -/// the local coordinate's reference point had changed -void HandleRefPointChanged () -{ - // Check if the reference point of the local coordinate system had changed - if (!dataRefs.DidLocalRefPointChange()) - return; - - // Force recalculation of all local coordinates of the airport/taxi network - LTAptLocalCoordsUpdate(true); -} - // //MARK: Init/Destroy // diff --git a/Src/LTOpenSky.cpp b/Src/LTOpenSky.cpp index 45ffa0f3..f1cf048d 100644 --- a/Src/LTOpenSky.cpp +++ b/Src/LTOpenSky.cpp @@ -154,7 +154,7 @@ bool OpenSkyConnection::ProcessFetchedData (mapLTFlightDataTy& fdMap) jag_n_nan(pJAc, OPSKY_ELEVATION), posTime, dyn.heading); - pos.onGrnd = dyn.gnd ? positionTy::GND_ON : positionTy::GND_OFF; + pos.f.onGrnd = dyn.gnd ? GND_ON : GND_OFF; // position is rather important, we check for validity // (we do allow alt=NAN if on ground as this is what OpenSky returns) @@ -226,7 +226,7 @@ bool OpenSkyAcMasterdata::FetchAllData (const positionTy& /*pos*/) std::string data("{"); // *** Fetch Masterdata *** - pos.onGrnd = positionTy::GND_ON; // flag for: master data + pos.f.onGrnd = GND_ON; // flag for: master data // skip icao of which we know they will come back invalid if ( std::find(invIcaos.cbegin(),invIcaos.cend(),info.acKey.icao) == invIcaos.cend() ) @@ -267,7 +267,7 @@ bool OpenSkyAcMasterdata::FetchAllData (const positionTy& /*pos*/) break; // *** Fetch Flight Info *** - pos.onGrnd = positionTy::GND_OFF; // flag for: route info + pos.f.onGrnd = GND_OFF; // flag for: route info // call sign shall be alphanumeric but nothing else str_toupper(info.callSign); diff --git a/Src/LTRealTraffic.cpp b/Src/LTRealTraffic.cpp index 11317e3f..01d4589b 100644 --- a/Src/LTRealTraffic.cpp +++ b/Src/LTRealTraffic.cpp @@ -747,12 +747,20 @@ bool RealTrafficConnection::ProcessRecvedTrafficData (const char* traffic) // Also, reported altitude never seems to become negative, // though this would be required in high pressure weather // at airports roughly at sea level. - // If "0" is reported we assume "on gnd" and bypass + // And altitude is rounded to 250ft which means that close + // to the ground it could be rounded down to 0! + // + // If "0" is reported we need to assume "on gnd" and bypass // the pressure correction. - // Otherwise we need to completely - // rely on our own altitude-to-ground detection. + // If at the same time VSI is reported significantly (> +/- 100) + // then we assume plane is already/still flying, but as we + // don't know exact altitude we just skip this record. if (tfc[RT_TFC_ALT] == "0") { - pos.alt_m() = NAN; // have proper gnd altitude calculated + // skip this dynamic record in case VSI is too large + if (std::abs(dyn.vsi) > RT_VSI_AIRBORNE) + return true; + // have proper gnd altitude calculated + pos.alt_m() = NAN; dyn.gnd = true; } else { // probably not on gnd, so take care of altitude @@ -763,7 +771,7 @@ bool RealTrafficConnection::ProcessRecvedTrafficData (const char* traffic) } // don't forget gnd-flag in position - pos.onGrnd = dyn.gnd ? positionTy::GND_ON : positionTy::GND_OFF; + pos.f.onGrnd = dyn.gnd ? GND_ON : GND_OFF; // add dynamic data fd.AddDynData(dyn, 0, 0, &pos); @@ -865,7 +873,7 @@ bool RealTrafficConnection::ProcessRecvedWeatherData (const char* weather) return true; // ignore silently lastWeather = weather; - LOG_MSG(logDEBUG, "Received new Weather: %s", weather); + LOG_MSG(logDEBUG, "Received Weather: %s", weather); // interpret weather JSON_Value* pRoot = json_parse_string(weather); diff --git a/Src/LiveTraffic.cpp b/Src/LiveTraffic.cpp index 114910b9..8fbdb4b5 100755 --- a/Src/LiveTraffic.cpp +++ b/Src/LiveTraffic.cpp @@ -58,6 +58,7 @@ enum menuItems { MENU_ID_NEWVER, #ifdef DEBUG MENU_ID_RELOAD_PLUGINS, + MENU_ID_REMOVE_ALL_BUT, #endif CNT_MENU_ID // always last, number of elements }; @@ -112,6 +113,9 @@ void MenuHandler(void * /*mRef*/, void * iRef) case MENU_ID_RELOAD_PLUGINS: XPLMReloadPlugins(); break; + case MENU_ID_REMOVE_ALL_BUT: + LTFlightData::RemoveAllAcButSelected(); + break; #endif } } catch (const std::exception& e) { @@ -283,6 +287,10 @@ bool RegisterMenuItem () // Reload Plugins aMenuItems[MENU_ID_RELOAD_PLUGINS] = XPLMAppendMenuItem(menuID, MENU_RELOAD_PLUGINS, (void *)MENU_ID_RELOAD_PLUGINS,1); + + // Remove all a/c except for the currently selected one + aMenuItems[MENU_ID_REMOVE_ALL_BUT] = + XPLMAppendMenuItem(menuID, MENU_REMOVE_ALL_BUT, (void *)MENU_ID_REMOVE_ALL_BUT,1); #endif // check for errors @@ -381,7 +389,7 @@ float LoopCBOneTimeSetup (float, float, int, void*) case ONCE_CB_AUTOSTART: // Auto Start display of aircraft - if (dataRefs.GetAutoStart()) + if (dataRefs.GetAutoStart() && !dataRefs.UsingModernDriver()) dataRefs.SetAircraftDisplayed(true); // check at X-Plane.org for version updates @@ -482,13 +490,21 @@ PLUGIN_API int XPluginEnable(void) XPLMRegisterFlightLoopCallback(LoopCBOneTimeSetup, 1, NULL); // Start reading apt.dat - LTAptEnable(); + if (dataRefs.GetFdSnapTaxiDist_m() > 0.0) + LTAptEnable(); // Enable showing aircraft if (!LTMainEnable()) return 0; // Create a message window and say hello - SHOW_MSG(logINFO, MSG_WELCOME, LT_VERSION_FULL); + if (dataRefs.UsingModernDriver()) { + // This version cannot run under Vulkan/Metal! + SHOW_MSG(logFATAL, MSG_NOT_MODERN_DRIVER, LT_VERSION_FULL); + SHOW_MSG(logFATAL, MSG_NOT_MODERN_DRIVER2); + dataRefs.SetAircraftDisplayed(false); + } else { + SHOW_MSG(logINFO, MSG_WELCOME, LT_VERSION_FULL); + } if constexpr (VERSION_BETA) SHOW_MSG(logWARN, BETA_LIMITED_VERSION, LT_BETA_VER_LIMIT_TXT); #ifdef DEBUG diff --git a/Src/TextIO.cpp b/Src/TextIO.cpp index 79d9d6ce..4af9cd56 100644 --- a/Src/TextIO.cpp +++ b/Src/TextIO.cpp @@ -102,6 +102,11 @@ float COL_LVL[logMSG+1][3] = { // text colors [RGB] depending on log le {1.00f, 1.00f, 1.00f} // MSG (white) }; +/// Values for "Seeing aircraft...showing..." +int gNumSee = 0, gNumShow = 0, gBufTime = -1; + +inline bool needSeeingShowMsg () { return gBufTime >= 0; } + //MARK: custom X-Plane message Window - Private Callbacks // Callbacks we will register when we create our window void draw_msg(XPLMWindowID in_window_id, void * /*in_refcon*/) @@ -125,10 +130,20 @@ void draw_msg(XPLMWindowID in_window_id, void * /*in_refcon*/) XPLMDrawTranslucentDarkBox(l, t, r, b); b = WIN_WIDTH; // word wrap width = window width + t -= WIN_ROW_HEIGHT; // move down to text's baseline + + // "Seeing aircraft...showing..." message + if (needSeeingShowMsg()) + { + char s[100]; + snprintf(s, sizeof(s), MSG_BUF_FILL_COUNTDOWN, gNumSee, gNumShow, gBufTime); + XPLMDrawString(COL_LVL[logINFO], l, t, s, &b, xplmFont_Proportional); + fTimeRemove = NAN; + t -= 2*WIN_ROW_HEIGHT; + } // for each line of text to be displayed float currTime = dataRefs.GetTotalRunningTimeSec(); - t -= WIN_ROW_HEIGHT; // move down to text's baseline for (auto iter = listTexts.cbegin(); iter != listTexts.cend(); t -= 2*WIN_ROW_HEIGHT) // can't deduce number of rwos (after word wrap)...just assume 2 rows are enough @@ -154,7 +169,7 @@ void draw_msg(XPLMWindowID in_window_id, void * /*in_refcon*/) // No texts left? Remove window in 1s if ((g_window == in_window_id) && - listTexts.empty()) + listTexts.empty() && !needSeeingShowMsg()) { if (std::isnan(fTimeRemove)) // set time when to remove @@ -187,31 +202,32 @@ XPLMWindowID CreateMsgWindow(float fTimeToDisplay, logLevelTy lvl, const char* s if ( lvl < dataRefs.GetMsgAreaLevel()) return g_window; - va_list args; - - // save the text in a static buffer queried by the drawing callback - char aszMsgTxt[500]; - va_start (args, szMsg); - vsnprintf(aszMsgTxt, - sizeof(aszMsgTxt), - szMsg, - args); - va_end (args); - - // define the text to display: - dispTextTy dispTxt = { - // set the timer if a limit is given - fTimeToDisplay >= 0.0f ? dataRefs.GetTotalRunningTimeSec() + fTimeToDisplay : 0, - // log level to define the color - lvl, - // finally the text - aszMsgTxt - }; - - // add to list of display texts - listTexts.emplace_back(std::move(dispTxt)); + // put together the formatted message if given + if (szMsg) { + va_list args; + char aszMsgTxt[500]; + va_start (args, szMsg); + vsnprintf(aszMsgTxt, + sizeof(aszMsgTxt), + szMsg, + args); + va_end (args); - // Otherwise: Create the message window + // define the text to display: + dispTextTy dispTxt = { + // set the timer if a limit is given + fTimeToDisplay >= 0.0f ? dataRefs.GetTotalRunningTimeSec() + fTimeToDisplay : 0, + // log level to define the color + lvl, + // finally the text + aszMsgTxt + }; + + // add to list of display texts + listTexts.emplace_back(std::move(dispTxt)); + } + + // Create the message window XPLMCreateWindow_t params; params.structSize = IS_XPLM301 ? sizeof(params) : XPLMCreateWindow_s_210; params.visible = 1; @@ -240,7 +256,8 @@ XPLMWindowID CreateMsgWindow(float fTimeToDisplay, logLevelTy lvl, const char* s params.top -= WIN_FROM_TOP; params.right -= WIN_FROM_RIGHT; params.left = params.right - WIN_WIDTH; - params.bottom = params.top - (WIN_ROW_HEIGHT * (2*int(listTexts.size())+1)); + params.bottom = params.top - (WIN_ROW_HEIGHT * (2*int(listTexts.size())+1+ + (needSeeingShowMsg() ? 2 : 0))); // if the window still exists just resize it if (g_window) { @@ -258,6 +275,16 @@ XPLMWindowID CreateMsgWindow(float fTimeToDisplay, logLevelTy lvl, const char* s } +// Show the special text "Seeing aircraft...showing..." +XPLMWindowID CreateMsgWindow(float fTimeToDisplay, int numSee, int numShow, int bufTime) +{ + gNumSee = numSee; + gNumShow = numShow; + gBufTime = bufTime; + return needSeeingShowMsg() ? CreateMsgWindow(fTimeToDisplay, logINFO, nullptr) : nullptr; +} + + void DestroyWindow() { if ( g_window ) diff --git a/docs/Apt.md b/docs/Apt.md new file mode 100644 index 00000000..17927585 --- /dev/null +++ b/docs/Apt.md @@ -0,0 +1,119 @@ +Airport Scenery Data {#apt_dat} +======================== + +LiveTraffic reads some airport scenery data from `apt.dat` files +as configured in `Custom Scenery/scenery_packs.ini`. + +Elements Read from apt.dat +-- + +The format of an `apt.dat` file is take from +[X-Plane's developer documentation](https://developer.x-plane.com/article/airport-data-apt-dat-file-format-specification/). +But apparently this document in version 11-April-2019 is not fully updates with +latest additions to Line Type Codes (p. 17). + +So I tried all line markings and light types and came up with this sample file: + + A + 1130 Generated by WorldEditor 2.2.0r2 + + 1 16 0 0 LANG Langeoog + 1302 country DE + 1302 gui_label 2D + 100 36.00 1 0 0.00 1 2 1 05 53.74070666 007.49366777 0 0 1 0 0 0 23 53.74404535 007.50169467 0 0 1 0 0 0 + 120 ILS Critical Wide Black + amber/green uni + 111 53.73609086 007.49351677 61 108 + 111 53.73612375 007.49593548 61 108 + 115 53.73612375 007.49827078 + 120 ILS Critical Wide + amber/green + 111 53.73655134 007.49348897 11 105 + 111 53.73648556 007.49607448 11 105 + 115 53.73651845 007.49840979 + 120 Single Taxi Wide Black + Unidirectional amber/green + 111 53.73691316 007.49354451 60 108 + 111 53.73692960 007.49621363 60 108 + 115 53.73696249 007.49852130 + 120 Single Taxi Wide + Green Lights + 111 53.73737365 007.49343329 10 101 + 111 53.73734075 007.49626923 10 101 + 115 53.73735720 007.49849350 + 120 Solid Yellow + Green Centerlights + 111 53.73923205 007.49850740 1 101 + 111 53.73923205 007.49611631 1 101 + 115 53.73923205 007.49350280 + 120 ILS-critical Centerline Black + Hold Short Centerlights amber/green + 111 53.73780124 007.49332208 57 105 + 111 53.73775190 007.49601900 57 105 + 115 53.73773546 007.49860471 + 120 ILS-critical Centerline + Hold Short Centerlights amber/green + 111 53.73831107 007.49332208 7 105 + 111 53.73829462 007.49618582 7 105 + 115 53.73826173 007.49882714 + 120 Solid Yellow + Unidirectional Centerlights + 111 53.73875511 007.49339159 1 107 + 111 53.73878800 007.49700602 1 107 + 115 53.73877156 007.49878543 + 99 + +### Line Types + +The following line types are accepted as nodes of taxiway centerlines and make up the +taxiway network in LiveTraffic (after quite some optimizations): + +Row Code | Meaning +-------- | ------- +120 | Header of a group of "Linear Features", specifically painted surface markings and light strings on them +111 | Node in such a group, plain +112 | Node with Bezier control point +113 | Node, which closes a loop +114 | Node, which closes a loop, with Bezier control point +115 | Node, which terminates a line +116 | Node, which terminates a line, with Bezier control point + +**Note:** Only nodes with the following Line Type Codes are used as taxiway centerlines, +other nodes are simply ignored: + +Line Type Code | Solid yellow | wide | broken yellow on each side | black border | Usage +-------- | - | - | - | - | ----- +1 | X | | | | Standard Taxiway Centerline +7 | X | | X | | Taxiway centerlines in runway safety zones, ILS critical areas +10 | X | X | | | Standard Taxiway Centerline (wide) +11 | X | X | X | | Taxiway centerlines in runway safety zones, ILS critical areas (wide) +51 | X | | | X | Standard Taxiway Centerline (on concrete) +57 | X | | X | X | Taxiway centerlines in runway safety zones, ILS critical areas (on concrete) +60 | X | X | | X | Standard Taxiway Centerline (wide, on concrete) +61 | X | X | X | X | Taxiway centerlines in runway safety zones, ILS critical areas (wide, on concrete) + +Line Type Code (Lights) | green | amber | uni-directional | Usage +------------- | ----- | ----- | ---------- | -------- +101 | X | | | Standard taxiway center lights +105 | X | X | | Center lights in runway safety zones, ILS critical areas +107 | X | | X | Standard taxiway center lights (unidirectional) +108 | X | X | X | Center lights in runway safety zones, ILS critical areas (uni-directional) + +Alternatively, if no taxi centerlines are found (which is possible on grass fields), +the taxiroute network can be used instead. Typically, the `1200` groups of taxiroute +networks are added to the end of `apt.dat`. So, if LiveTraffic finds a `1200` row +without having seen a `120` row previously, then it reads the `1200` groups. + +Row Code | Meaning +-------- | ------- +1201 | A node in the taxi route network +1202 | An edge in the taxi route network, connecting two previously defined nodes + +Ramp location are read from the `1300` class of rows: + +Row Code | Meaning +-------- | ------- +1300 | Start up location + +Definitions +-- + +The following terms are used throught the (code) documentation: + +- **Node**: A position, where edges connect, relates to a 111-116 line in `apt.dat`\n +- **Edge**: The connection of two nodes, relates to two consecutive 111-116 lines in `apt.dat`\n +- **Path**: A set of connecting edges, relates to a gorup of 111-116 lines in `apt.dat`, headed by a 120 line\n +- **Network**: All paths and runways of an airport combined. The network is what is searched +for shortest paths between any two points. diff --git a/docs/MainPage.md b/docs/MainPage.md index da20ca5e..8d2622d9 100644 --- a/docs/MainPage.md +++ b/docs/MainPage.md @@ -17,6 +17,8 @@ Quick Links: - File List +- Information on [reading airport scenery data](@ref apt_dat) + Links to outside locations: -- diff --git a/docs/Notes.txt b/docs/Notes.txt index 2ece03a3..343df185 100644 --- a/docs/Notes.txt +++ b/docs/Notes.txt @@ -1,12 +1,15 @@ TODO === -TEST -=== -Loading apt.dat currently uses just getline -- Test on Windows -- Test on Mac with Windows file -- Test on Windows using Mac file +#13 ensure nearest a/c are shown + +Identify Vulkan/Metal and warn fatally: +sim/graphics/view/using_modern_driver + + +LTApt +- Instead of wasting space with all nodes supporting Dijkstra, + consider a map holding Dijkstra info per visited node DOCUMENTATION diff --git a/docs/readme.html b/docs/readme.html index 38991af0..8d0eb20b 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -82,13 +82,45 @@

Release Notes

The number-link refers to the issue in GitHub with (often technical) details.

+

v1.5

+ +

v1.50

+ +

To update coming from v1.24: Copy the 3 executables + LiveTraffic/64/*.xpl and the updated + Resources/Doc8643.txt file.

+ +
    +
  • ADDED #100: + Snapping to taxiways to make aircraft stay on taxiways more often. + Also determines gate/ramp positions and turns aircraft the right way. + Controlled by Snap to Taxiways setting in + + Advanced Settings.
    + See documentation + for some background of how it works. +
  • +
  • ADDED #17: + Soft Bezier-style curves for turning aircraft. +
  • +
  • ADDED safeguard to keep approaching aircraft above 2.5° glidescope + to avoid landing short of the runway. +
  • +
  • ADDED #13: + Ensure nearest aircraft are shown when reaching limit. +
  • +
  • FIXED #170 (Windows only): + Multiplayer location info initialized with "far away" again instead of zeroes. +
  • +
  • ADDED message that LiveTraffic does not yet work under Vulkan/Metal. + Watch this post and await v2.0. +
  • +
+

v1.2

v1.24

-

To update coming from v1.23 copy the 3 executables - LiveTraffic/64/*.xpl.

-
  • FIXED #169 auto-land.