diff --git a/.gitmodules b/.gitmodules index 4b177dd..eb016c2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,10 @@ [submodule "Lib/XPMP2"] path = Lib/XPMP2 url = https://github.com/TwinFan/XPMP2 - branch = master \ No newline at end of file + branch = master +[submodule "Lib/parson"] + path = Lib/parson + url = https://github.com/kgabis/parson +[submodule "Lib/metaf"] + path = Lib/metaf + url = https://github.com/TwinFan/metaf diff --git a/AviationWeather/METAR.sjson/747604614.297283 b/AviationWeather/METAR.sjson/747604614.297283 new file mode 100644 index 0000000..9a4e73c Binary files /dev/null and b/AviationWeather/METAR.sjson/747604614.297283 differ diff --git a/AviationWeather/METAR.sjson/747604784.031099 b/AviationWeather/METAR.sjson/747604784.031099 new file mode 100644 index 0000000..75281ad Binary files /dev/null and b/AviationWeather/METAR.sjson/747604784.031099 differ diff --git a/AviationWeather/METAR.sjson/data b/AviationWeather/METAR.sjson/data new file mode 100644 index 0000000..daf1d5e Binary files /dev/null and b/AviationWeather/METAR.sjson/data differ diff --git a/AviationWeather/METAR.sjson/metaData b/AviationWeather/METAR.sjson/metaData new file mode 100644 index 0000000..ed57b82 Binary files /dev/null and b/AviationWeather/METAR.sjson/metaData differ diff --git a/CMakeLists.txt b/CMakeLists.txt index a9fbb71..934c0ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,11 +21,11 @@ else() endif() project(LiveTraffic - VERSION 3.6.0 + VERSION 3.6.9 DESCRIPTION "LiveTraffic X-Plane plugin") +set(VERSION_BETA 1) # Building a Beta version can be demanded via ENV variable 'version_beta' being set to 1 -set(VERSION_BETA 0) if(DEFINED ENV{version_beta}) if ($ENV{version_beta} GREATER 0) set(VERSION_BETA 1) @@ -141,6 +141,7 @@ include_directories("${CMAKE_CURRENT_SOURCE_DIR}/Lib/ImGui") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/Lib/ImGui/misc/cpp") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/Lib/ImgWindow") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/Lib/Font") +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/Lib/metaf/include") ################################################################################ # Source groups @@ -184,6 +185,7 @@ set(Header_Files Lib/ImgWindow/SystemGL.h Lib/Font/IconsFontAwesome5.h Lib/Font/fa-solid-900.inc + Lib/metaf/include/metaf.hpp Lib/XPMP2/inc/XPCAircraft.h Lib/XPMP2/inc/XPMPMultiplayer.h Lib/XPMP2/lib/fmod/logo/FMOD_Logo.h diff --git a/Data/RealTraffic/RTAPI_example.py b/Data/RealTraffic/RTAPI_example.py deleted file mode 100755 index e9df886..0000000 --- a/Data/RealTraffic/RTAPI_example.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 - -# RTAPI_example.py v1.2 7/11/2023 balt@inside.net -# Illustrates data retrieval and broadcasting via UDP - -import requests -import json -import time -import sys -import signal -import psutil -from datetime import datetime -from argparse import ArgumentParser -import threading -from socket import SO_REUSEADDR, SOCK_STREAM, socket, SOL_SOCKET, AF_INET, SOCK_DGRAM, IPPROTO_UDP, SO_BROADCAST - -def sighandler(signum, frame): - if signum == signal.SIGINT: - print("Ctrl-C captured. Exiting.") - sys.exit(0) - -def UDPbcast(ip, bcast, port, data): - sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) # UDP - sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) - sock.bind((ip,0)) - sock.sendto(data, (bcast, port)) - sock.close() - -if __name__ == '__main__': - - parser = ArgumentParser(description='RealTraffic API Demo') - - # Add optional argument, with given default values if user gives no arg - parser.add_argument('-p', '--port', default=10747, type=int, help='Server port') - parser.add_argument('-s', '--host', default="127.0.0.1", type=str, help='Server host') - parser.add_argument('-l', '--license', required=True, help='Your RealTraffic license, e.g. AABBCC-1234-AABBCC-123456') - args = parser.parse_args() - - license = args.license - - # application specific settings - software = "RTAPI_demo" - - # API constants - auth_url = "https://rtw.flyrealtraffic.com/v3/auth" - traffic_url = "https://rtw.flyrealtraffic.com/v3/traffic" - weather_url = "https://rtw.flyrealtraffic.com/v3/weather" - header = { "Accept-encoding": "gzip" } - license_types = { 1: "Standard", 2: "Professional" } - special_keys = [ "full_count", "dataepoch", "source", "rrl", "status" ] - - # capture signals such as ctrl-c in the loop - signal.signal(signal.SIGINT, sighandler) - - ############################################################### - # enumerate all network interfaces and get their IPs - # broadcasting to 255.255.255.255 is bad practice, need to find the correct bcast addr for - # the local subnet on each interface only - bcast_addrs = [] - ip_addrs = [] - ifs = psutil.net_if_addrs() - for key in ifs.keys(): - for intf in ifs[key]: - if intf.broadcast != None: - bcast_addrs.append(intf.broadcast) - ip_addrs.append(intf.address) - - print("Will broadcast to:", bcast_addrs) - - ############################################################### - # authenticate - payload = { "license": "%s" % license, "software": "%s" % software } - json_data = requests.post(auth_url, payload, headers=header).json() - if json_data["status"] != 200: - print(json_data["message"]) - sys.exit(0) - - ############################################################### - # retrieve our GUID to use for data access as well as the license details - GUID = json_data["GUID"] - license_type = json_data["type"] - expiry = datetime.fromtimestamp(json_data["expiry"]) - sleep_seconds = int(json_data["rrl"]) / 1000 - - print("Successfully authenticated. %s license valid until %s UTC" % (license_types[license_type], expiry.strftime("%Y-%m-%d %H:%M:%S"))) - print("Sleeping %ds to avoid request rate violation..." % sleep_seconds) - time.sleep(sleep_seconds) - - # set up the payload for the traffic queries - # Example area: Heidiland - traffic_payload = { "GUID": "%s" % GUID, - "querytype": "locationtraffic", - "top": 52.4, # Around EDLE - "bottom": 50.4, - "left": 5.9, - "right": 7.9, - "toffset": 0 } - - weather_payload = { "GUID": "%s" % GUID, - "querytype": "locwx", - "lat": 51.4069, # EDLE - "lon": 6.9391, - "alt": 0, - "airports": "UNKN", # Don't need airport METAR - "toffset": 0 } - - ############################################################### - # keep fetching traffic forever (or until ctrl-c is pressed) - while True: - # fetch weather - - response = requests.post(weather_url, weather_payload, headers=header) - print(response.text) - try: - json_data = response.json() - except Exception as e: - print(e) - print(response.text) - # something borked. abort. - raise - - if json_data["status"] != 200: - print(json_data["message"]) - time.sleep(2) - continue - - print(json_data["data"]) - - # fetch traffic - response = requests.post(traffic_url, traffic_payload, headers=header) - try: - json_data = response.json() - except Exception as e: - print(e) - print(response.text) - # something borked. abort. - raise - - if json_data["status"] != 200: - print(json_data["message"]) - time.sleep(2) - continue - - flights = [] - for key in json_data: - if key not in special_keys: - for ip, bcast in zip(ip_addrs, bcast_addrs): - UDPbcast(ip, bcast, 49005, str.encode(json.dumps(json_data[key]))) - # and extract a few data points for show and tell - flights.append("%08s %08s %04s %05s %03s %04s %04s" % (json_data[key][13], json_data[key][16], json_data[key][8], json_data[key][4], json_data[key][5], json_data[key][11], json_data[key][12])) - - print("------------------------------------------------------") - print("Current time: %s UTC" % datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")) - print("Traffic time: %s UTC" % datetime.utcfromtimestamp(json_data["dataepoch"])) - print("Traffic source: %s" % json_data["source"]) - print("Request rate limit: %sms" % json_data["rrl"]) - print("Total flights in the system: %s" % json_data["full_count"]) - print("") - print("Callsign Flight Type Alt Gsp Orig Dest") - for f in sorted(flights): - print(f) - - sleep_seconds = int(json_data["rrl"]) / 1000 - print("Sleeping %d seconds..." % sleep_seconds) - - time.sleep(sleep_seconds) - - - diff --git a/Data/RealTraffic/RTTFC_Template.xlsx b/Data/RealTraffic/RTTFC_Template.xlsx deleted file mode 100644 index 4629ebd..0000000 Binary files a/Data/RealTraffic/RTTFC_Template.xlsx and /dev/null differ diff --git a/Data/RealTraffic/RT_API.sjson/725578599.580375 b/Data/RealTraffic/RT_API.sjson/725578599.580375 deleted file mode 100644 index 68cf132..0000000 Binary files a/Data/RealTraffic/RT_API.sjson/725578599.580375 and /dev/null differ diff --git a/Data/RealTraffic/RT_API.sjson/725717638.083122 b/Data/RealTraffic/RT_API.sjson/725717638.083122 deleted file mode 100644 index 13e5477..0000000 Binary files a/Data/RealTraffic/RT_API.sjson/725717638.083122 and /dev/null differ diff --git a/Data/RealTraffic/RT_API.sjson/742250820.106701 b/Data/RealTraffic/RT_API.sjson/742250820.106701 new file mode 100644 index 0000000..60fde25 Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/742250820.106701 differ diff --git a/Data/RealTraffic/RT_API.sjson/742250820.107324 b/Data/RealTraffic/RT_API.sjson/742250820.107324 new file mode 100644 index 0000000..f6e465e Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/742250820.107324 differ diff --git a/Data/RealTraffic/RT_API.sjson/742607783.227589 b/Data/RealTraffic/RT_API.sjson/742607783.227589 new file mode 100644 index 0000000..8b2efa8 Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/742607783.227589 differ diff --git a/Data/RealTraffic/RT_API.sjson/744148065.431184 b/Data/RealTraffic/RT_API.sjson/744148065.431184 new file mode 100644 index 0000000..a6ee5d4 Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/744148065.431184 differ diff --git a/Data/RealTraffic/RT_API.sjson/744148113.273644 b/Data/RealTraffic/RT_API.sjson/744148113.273644 new file mode 100644 index 0000000..a6ae566 Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/744148113.273644 differ diff --git a/Data/RealTraffic/RT_API.sjson/746213911.74435 b/Data/RealTraffic/RT_API.sjson/746213911.74435 new file mode 100644 index 0000000..b898a4a Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/746213911.74435 differ diff --git a/Data/RealTraffic/RT_API.sjson/746214863.95976 b/Data/RealTraffic/RT_API.sjson/746214863.95976 new file mode 100644 index 0000000..ef7010c Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/746214863.95976 differ diff --git a/Data/RealTraffic/RT_API.sjson/746312457.328537 b/Data/RealTraffic/RT_API.sjson/746312457.328537 new file mode 100644 index 0000000..9fa8bfb Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/746312457.328537 differ diff --git a/Data/RealTraffic/RT_API.sjson/746828180.600688 b/Data/RealTraffic/RT_API.sjson/746828180.600688 new file mode 100644 index 0000000..0f0fdfd Binary files /dev/null and b/Data/RealTraffic/RT_API.sjson/746828180.600688 differ diff --git a/Data/RealTraffic/RT_API.sjson/data b/Data/RealTraffic/RT_API.sjson/data index d22ac3b..ea72a21 100644 Binary files a/Data/RealTraffic/RT_API.sjson/data and b/Data/RealTraffic/RT_API.sjson/data differ diff --git a/Data/RealTraffic/RT_API.sjson/metaData b/Data/RealTraffic/RT_API.sjson/metaData index 2c9a101..f04759d 100644 Binary files a/Data/RealTraffic/RT_API.sjson/metaData and b/Data/RealTraffic/RT_API.sjson/metaData differ diff --git a/Data/RealTraffic/RealTraffic API.pdf b/Data/RealTraffic/RealTraffic API.pdf index db53bde..932c7fe 100644 Binary files a/Data/RealTraffic/RealTraffic API.pdf and b/Data/RealTraffic/RealTraffic API.pdf differ diff --git a/Data/RealTraffic/Weather.xls b/Data/RealTraffic/Weather.xls new file mode 100644 index 0000000..b2b5993 Binary files /dev/null and b/Data/RealTraffic/Weather.xls differ diff --git a/Include/Constants.h b/Include/Constants.h index 10a2630..cbf4cac 100644 --- a/Include/Constants.h +++ b/Include/Constants.h @@ -49,6 +49,7 @@ constexpr double M_per_FT = 0.3048; // meter per 1 foot constexpr int M_per_KM = 1000; constexpr double KT_per_M_per_S = 1.94384; // 1m/s = 1.94384kt constexpr double NM_per_KM = 1000.0 / double(M_per_NM); +constexpr double M_per_SM = 1609.344; ///< meters per statute mile constexpr int SEC_per_M = 60; // 60 seconds per minute constexpr int SEC_per_H = 3600; // 3600 seconds per hour constexpr int H_per_D = 24; // 24 hours per day @@ -67,7 +68,7 @@ constexpr double TEMP_LAPS_R = -0.0065f; ///< K/m //MARK: Flight Data-related constexpr unsigned MAX_TRANSP_ICAO = 0xFFFFFF; // max transponder ICAO code (24bit) -constexpr int MAX_NUM_AIRCRAFT = 200; ///< maximum number of aircraft allowed to be rendered +constexpr int MAX_NUM_AIRCRAFT = 300; ///< maximum number of aircraft allowed to be rendered 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 @@ -270,6 +271,7 @@ constexpr const char* REMOTE_SIGNATURE = "TwinFan.plugin.XPMP2.Remote"; #define HELP_SET_OUTPUT_CH "setup/installation/foreflight" // currently the same as ForeFlight, which is the only output channel #define HELP_SET_CH_FOREFLIGHT "setup/installation/foreflight" #define HELP_SET_ACLABELS "setup/configuration/settings-a-c-labels" +#define HELP_SET_WEATHER "setup/configuration/settings-weather" #define HELP_SET_ADVANCED "setup/configuration/settings-advanced" #define HELP_SET_CSL "setup/configuration/settings-csl" #define HELP_SET_DEBUG "setup/configuration/settings-debug" @@ -367,7 +369,7 @@ constexpr int SERR_LEN = 100; // size of buffer for IO error t #define ERR_CFG_FILE_OPEN_OUT "Could not create config file '%s': %s" #define ERR_CFG_FILE_WRITE "Could not write into config file '%s': %s" #define ERR_CFG_FILE_OPEN_IN "Could not open '%s': %s" -#define ERR_CFG_FILE_VER "Config file '%s' first line: Unsupported format or version: %s" +#define ERR_CFG_FILE_VER "Config file '%s' first line: Unsupported format or version: '%s'" #define ERR_CFG_FILE_VER_UNEXP "Config file '%s' first line: Unexpected version %s, expected %s...trying to continue" #define ERR_CFG_FILE_IGNORE "Ignoring unkown entry '%s' from config file '%s'" #define ERR_CFG_FILE_WORDS "Expected two words (key, value) in config file '%s', line '%s': ignored" diff --git a/Include/CoordCalc.h b/Include/CoordCalc.h index b54c489..17f2e33 100644 --- a/Include/CoordCalc.h +++ b/Include/CoordCalc.h @@ -34,11 +34,11 @@ // /// Square, ie. a^2 template -inline T sqr (T a) { return a*a; } +inline T sqr (const T a) { return a*a; } /// Pythagoras square, ie. a^2 + b^2 template -inline T pyth2 (T a, T b) { return sqr(a) + sqr(b); } +inline T pyth2 (const T a, const T b) { return sqr(a) + sqr(b); } // //MARK: Degree/Radian conversion @@ -392,7 +392,9 @@ struct positionTy { // normalizes to -90/+90 lat, -180/+180 lon, 360° heading, return *this positionTy& normalize(); - // is a good valid position? + // has a position and altitude? + bool hasPosAlt () const { return !std::isnan(lat()) && !std::isnan(lon()) && !std::isnan(alt_m()); } + // is a good valid normalized position incl timestamp? bool isNormal (bool bAllowNanAltIfGnd = false) const; // is fully valid? (isNormal + heading, pitch, roll)? bool isFullyValid() const; diff --git a/Include/DataRefs.h b/Include/DataRefs.h index a70cfe5..8c35cb1 100644 --- a/Include/DataRefs.h +++ b/Include/DataRefs.h @@ -55,7 +55,8 @@ const bool DEF_SND_FMOD_INST = true; ///< Enforce using our own FMOD #endif const int DEF_SUI_TRANSP = 0; ///< Settings UI: transaprent background? const int DEF_MAX_NETW_TIMEOUT = 5; ///< [s] default maximum network request timeout - +const int DEF_WEATHER_MAX_METAR_AGL_FT = 5000; ///< Max height AGL up to which we weigh METAR higher than other weather data +const int DEF_WEATHER_MAX_METAR_DIST_NM = 25; ///< Max distance up to which to use a METAR for weather constexpr int DEF_UI_FONT_SCALE = 100; ///< [%] Default font scaling constexpr int DEF_UI_OPACITY = 25; ///< [%] Default background opacity @@ -395,6 +396,9 @@ enum dataRefsLT { DR_CFG_CONTRAIL_MAX_ALT, DR_CFG_CONTRAIL_LIFE_TIME, DR_CFG_CONTRAIL_MULTIPLE, + DR_CFG_WEATHER_CONTROL, + DR_CFG_WEATHER_MAX_METAR_AGL, + DR_CFG_WEATHER_MAX_METAR_DIST, DR_CFG_REMOTE_SUPPORT, DR_CFG_EXTERNAL_CAMERA, DR_CFG_LAST_CHECK_NEW_VER, @@ -403,6 +407,7 @@ enum dataRefsLT { DR_DBG_AC_FILTER, DR_DBG_AC_POS, DR_DBG_LOG_RAW_FD, + DR_DBG_LOG_WEATHER, DR_DBG_MODEL_MATCHING, DR_DBG_EXPORT_FD, DR_DBG_EXPORT_USER_AC, @@ -468,6 +473,14 @@ enum SimTimeCtrlTy : int { STC_SIM_TIME_PLUS_BUFFER, ///< Send current sim time plus buffering period, so that the traffic, when it appears, matches up with current sim time }; +/// How to control weather? +enum WeatherCtrlTy : int { + WC_INIT = -1, ///< Initial value when not available in config file, then a default is determined in first flight loop depending if XP is using real weather + WC_NONE = 0, ///< No weather control, X-Plane is in control + WC_METAR_XP, ///< Use METAR to control weather in lower altitudes, X-Plane Live Weather in higher altitudes + WC_REAL_TRAFFIC, ///< Use RealTraffic's weather data to control all weather +}; + /// Which RealTraffic connection type to use? enum RTConnTypeTy : int { RT_CONN_REQU_REPL = 0, ///< Expect a license and use request/reply @@ -655,6 +668,7 @@ class DataRefs unsigned uDebugAcFilter = 0; // icao24 for a/c filter int bDebugAcPos = false;// output debug info on position calc into log file? int bDebugLogRawFd = false;// log raw flight data to LTRawFD.log + int bDebugWeather = false;///< log weather data for debugging exportFDFormat eDebugExportFdFormat = EXP_FD_AITFC; ///< Which format to use when exporting flight data? int bDebugExportFd = false;// export flight data to LTExportFD.csv int bDebugExportUserAc = false;///< export user's aircraft data to LTExportFD.csv @@ -722,6 +736,9 @@ class DataRefs SimTimeCtrlTy rtSTC = STC_SIM_TIME_PLUS_BUFFER; ///< Which sim time to send to RealTraffic? int rtManTOfs = 0; ///< manually configure time offset for requesting historic data RTConnTypeTy rtConnType = RT_CONN_REQU_REPL; ///< Which type of connection to use for RealTraffic data + WeatherCtrlTy weatherCtl = WC_INIT; ///< How to control weather? + int weatherMaxMETARheight_ft = DEF_WEATHER_MAX_METAR_AGL_FT; ///< height AGL up to which we use/prefer METAR data + int weatherMaxMETARdist_nm = DEF_WEATHER_MAX_METAR_DIST_NM; ///< distance [nm] up to which to use METAR for weather int ffListenPort = 63093; ///< UDP Port to listen to ForeFlight announcing itself, https://www.foreflight.com/connect/spec/ int ffSendPort = 49002; ///< UDP Port to send simulator data to ForeFlight, https://www.foreflight.com/support/network-gps/ int bffUserPlane = 1; // bool Send User plane data? @@ -847,7 +864,9 @@ class DataRefs std::string GetXPSimTimeStr() const; ///< Return a nicely formated time string with XP's simulated time in UTC void SetViewType(XPViewTypes vt); - positionTy GetUsersPlanePos(double& trueAirspeed_m, double& track) const; + positionTy GetUsersPlanePos(double* pTrueAirspeed_m = nullptr, + double* pTrack = nullptr, + double* pHeightAGL_m = nullptr) const; //MARK: DataRef provision by LiveTraffic // Generic Get/Set callbacks @@ -898,8 +917,8 @@ class DataRefs static void LTSetLogLevel(void* p, int i); void SetLogLevel ( int i ); void SetMsgAreaLevel ( int i ); - inline logLevelTy GetLogLevel() { return iLogLevel; } - inline logLevelTy GetMsgAreaLevel() { return iMsgAreaLevel; } + logLevelTy GetLogLevel() const { return iLogLevel; } + logLevelTy GetMsgAreaLevel() const { return iMsgAreaLevel; } /// Reinit data usage void ForceDataReload (); @@ -951,6 +970,8 @@ class DataRefs hideNearbyGnd > 0 || hideNearbyAir > 0 || hideInReplay; } bool IsAutoHidingActive() const ///< any auto-hiding activated, including options no warning is issued about? { return hideStaticTwr || WarnAutoHiding(); } + /// "Keep Parked Aircraft" is equivalent to "Synthetic Channel enabled" + bool ShallKeepParkedAircraft () const { return IsChannelEnabled(DR_CHANNEL_SYNTHETIC); } bool ShallCpyObjFiles () const { return cpyObjFiles != 0; } int GetContrailAltMin_ft () const { return contrailAltMin_ft; } int GetContrailAltMax_ft () const { return contrailAltMax_ft; } @@ -970,6 +991,12 @@ class DataRefs bool SetDefaultAcIcaoType(const std::string type); bool SetDefaultCarIcaoType(const std::string type); + WeatherCtrlTy GetWeatherControl() const { return weatherCtl; } + int GetWeatherMaxMetarHeight_ft() const { return weatherMaxMETARheight_ft; } + float GetWeatherMaxMetarHeight_m() const { return float(weatherMaxMETARheight_ft * M_per_FT); } + int GetWeatherMaxMetarDist_nm() const { return weatherMaxMETARdist_nm; } + float GetWeatherMaxMetarDist_m() const { return weatherMaxMETARdist_nm * M_per_NM; } + // livetraffic/channel/... void SetChannelEnabled (dataRefsLT ch, bool bEnable); inline bool IsChannelEnabled (dataRefsLT ch) const { return bChannel[ch - DR_CHANNEL_FIRST]; } @@ -1017,6 +1044,9 @@ class DataRefs inline bool GetDebugLogRawFD() const { return bDebugLogRawFd; } void SetDebugLogRawFD (bool bLog) { bDebugLogRawFd = bLog; } + bool ShallLogWeather() const { return bDebugWeather && GetLogLevel() == logDEBUG; } + void SetDebugLogWeather(bool bLog) { bDebugWeather = bLog; } + exportFDFormat GetDebugExportFormat() const { return eDebugExportFdFormat; } void SetDebugExportFormat (exportFDFormat e) { eDebugExportFdFormat = e; } bool GetDebugExportFD() const { return bDebugExportFd; } @@ -1070,11 +1100,12 @@ class DataRefs bool ToggleLabelDraw(); // returns new value // Weather - bool WeatherUpdate (); ///< check if weather updated needed, then do - /// @brief set/update current weather + bool WeatherFetchMETAR (); ///< check if weather updated needed, then do + /// @brief set/update current weather, tries reading QNH from METAR /// @details if lat/lon ar NAN, then location of provided station is taken if found, else current camera pos - void SetWeather (float hPa, float lat, float lon, const std::string& stationId, - const std::string& METAR); + /// @returns QNH (`hPa` if not read from `METAR`) + float SetWeather (float hPa, float lat, float lon, const std::string& stationId, + const std::string& METAR); /// Get current sea level air pressure double GetPressureHPA() const { return lastWeatherHPA; } /// Thread-safely gets current weather info diff --git a/Include/LTApt.h b/Include/LTApt.h index 9f93f2f..3546565 100644 --- a/Include/LTApt.h +++ b/Include/LTApt.h @@ -27,8 +27,12 @@ /// Start reading apt.dat file(s), build an index bool LTAptEnable (); -/// Update the airport data with airports around current camera position -void LTAptRefresh (); +/// @brief Update the airport data with airports around current camera position +/// @returns if fresh airport data has just been loaded completely +bool LTAptRefresh (); + +/// Has LTAptRefresh finished, ie. do we have airport data? +bool LTAptAvailable (); /// @brief Return the best possible runway to auto-land at /// @param _mdl flight model definitions to use for calculations diff --git a/Include/LTChannel.h b/Include/LTChannel.h index dfe1520..bc14b59 100644 --- a/Include/LTChannel.h +++ b/Include/LTChannel.h @@ -459,6 +459,9 @@ inline long jag_l (const JSON_Array *array, size_t idx) return std::lround(json_array_get_number(array, idx)); } +/// return an entire JSON array as float vector +std::vector jag_f_vector (const JSON_Array* array); + /// Find first non-Null value in several JSON array fields JSON_Value* jag_FindFirstNonNull(const JSON_Array* pArr, std::initializer_list aIdx); diff --git a/Include/LTRealTraffic.h b/Include/LTRealTraffic.h index c124047..7d0bd03 100644 --- a/Include/LTRealTraffic.h +++ b/Include/LTRealTraffic.h @@ -42,13 +42,21 @@ #define REALTRAFFIC_NAME "RealTraffic" -#define RT_AUTH_URL "https://rtw.flyrealtraffic.com/v3/auth" +#define RT_ENDP "v4" +#define RT_METAR_UNKN "UNKN" + +#define RT_AUTH_URL "https://rtw.flyrealtraffic.com/" RT_ENDP "/auth" #define RT_AUTH_POST "license=%s&software=%s" -#define RT_WEATHER_URL "https://rtw.flyrealtraffic.com/v3/weather" -#define RT_WEATHER_POST "GUID=%s&lat=%.2f&lon=%.2f&alt=%ld&airports=UNKN&querytype=locwx&toffset=%ld" -#define RT_TRAFFIC_URL "https://rtw.flyrealtraffic.com/v3/traffic" +#define RT_DEAUTH_URL "https://rtw.flyrealtraffic.com/" RT_ENDP "/deauth" +#define RT_DEAUTH_POST "GUID=%s" +#define RT_NEAREST_METAR_URL "https://rtw.flyrealtraffic.com/" RT_ENDP "/nearestmetar" +#define RT_NEAREST_METAR_POST "GUID=%s&lat=%.2f&lon=%.2f&toffset=%ld&maxcount=7" +#define RT_WEATHER_URL "https://rtw.flyrealtraffic.com/" RT_ENDP "/weather" +#define RT_WEATHER_POST "GUID=%s&lat=%.2f&lon=%.2f&alt=%ld&airports=%s&querytype=locwx&toffset=%ld" +#define RT_TRAFFIC_URL "https://rtw.flyrealtraffic.com/" RT_ENDP "/traffic" #define RT_TRAFFIC_POST "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=locationtraffic&toffset=%ld" - +#define RT_TRAFFIC_POST_BUFFER "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=locationtraffic&toffset=%ld&buffercount=%d&buffertime=10" +#define RT_TRAFFIC_POST_PARKED "GUID=%s&top=%.2f&bottom=%.2f&left=%.2f&right=%.2f&querytype=parkedtraffic&toffset=%ld" #define RT_LOCALHOST "0.0.0.0" constexpr size_t RT_NET_BUF_SIZE = 8192; @@ -78,9 +86,8 @@ constexpr double RT_VSI_AIRBORNE = 80.0; ///< if VSI is more than this then w // Constant for direct connection constexpr long RT_DRCT_DEFAULT_WAIT = 8000L; ///< [ms] Default wait time between traffic requests constexpr std::chrono::seconds RT_DRCT_ERR_WAIT = std::chrono::seconds(5); ///< standard wait between errors -constexpr std::chrono::minutes RT_DRCT_WX_WAIT = std::chrono::minutes(10); ///< How often to update weather? -constexpr std::chrono::seconds RT_DRCT_WX_ERR_WAIT = std::chrono::seconds(60); ///< How long to wait after receiving an weather error? -constexpr long RT_DRCT_WX_DIST = 10L * M_per_NM; ///< Distance for which weather is considered valid, greater than that and we re-request +constexpr std::chrono::seconds RT_DRCT_ERR_RATE = std::chrono::seconds(10); ///< wait in case of rate violations, too many sessions +constexpr std::chrono::minutes RT_DRCT_WX_WAIT = std::chrono::minutes(1); ///< How often to update weather? constexpr int RT_DRCT_MAX_WX_ERR = 5; ///< Max number of consecutive errors during initial weather requests we wait for...before not asking for weather any longer /// Fields in a response of a direct connection's request @@ -136,6 +143,18 @@ enum RT_DIRECT_FIELDS_TY { RT_DRCT_NUM_FIELDS ///< Number of known fields }; +/// Fields in a response to a parked aircraft request +enum RT_PARKED_FIELDS_TY { + RT_PARK_Lat = 0, ///< latitude (-33.936407) + RT_PARK_Lon, ///< longitude (151.169229) + RT_PARK_ParkPosName, ///< some text indicating the parking position as per Jepperson + RT_PARK_AcType, ///< Type ("A388") + RT_PARK_Reg, ///< Registration ("VH-OQA") + RT_PARK_LastTimeStamp, ///< Last Epoch timestamp when moved into position, can be long ago (1721590016.01) + RT_PARK_CallSign, ///< ATC Callsign ("QFA2") + RT_PARK_NUM_FIELDS ///< Number of known fields +}; + /// Fields in a RealTraffic AITFC message (older format on port 49003) enum RT_AITFC_FIELDS_TY { RT_AITFC_REC_TYPE = 0, ///< "AITFC" or "XTRAFFICPSX" @@ -253,6 +272,9 @@ class RealTrafficConnection : public LTFlightDataChannel /// Which kind of call do we need next? enum RTRequestTypeTy : int { RT_REQU_AUTH = 1, ///< Perform Authentication request + RT_REQU_DEAUTH, ///< Perform De-authentication request (closing the session) + RT_REQU_PARKED, ///< Perform Parked Traffic request + RT_REQU_NEAREST_METAR, ///< Perform nearest METAR location request RT_REQU_WEATHER, ///< Perform Weather request RT_REQU_TRAFFIC, ///< Perform Traffic request } eRequType = RT_REQU_AUTH; ///< Which type of request is being performed now? @@ -260,22 +282,46 @@ class RealTrafficConnection : public LTFlightDataChannel positionTy pos; ///< viewer position for which we receive Realtraffic data long tOff = 0; ///< time offset for which we request data } curr; ///< Data for the current request - /// How long to wait before making the next request? - std::chrono::milliseconds rrlWait = std::chrono::milliseconds(0); + /// What's the next time we could send a traffic request? + std::chrono::time_point tNextTraffic; + /// What's the next time we could send a weather request? + std::chrono::time_point tNextWeather; + + /// METAR entry in the NearestMETAR response + struct NearestMETAR { + std::string ICAO = RT_METAR_UNKN; ///< ICAO code of METAR station + double dist = NAN; ///< distance to station + double brgTo = NAN; ///< bearing to station + + NearestMETAR() {} ///< Standard constructor, all empty + NearestMETAR(const JSON_Object* pObj) { Parse (pObj); } ///< Fill from JSON + + void clear() { *this = NearestMETAR(); } ///< reset to defaults + bool Parse (const JSON_Object* pObj); ///< parse RT's NearestMETAR response array entry, reutrns if valid + bool isValid () const ///< valid, ie. all fields properly set? + { return !ICAO.empty() && ICAO != RT_METAR_UNKN && !std::isnan(dist) && !std::isnan(brgTo); } + }; + /// Weather data struct WxTy { double QNH = NAN; ///< baro pressure std::chrono::steady_clock::time_point next; ///< next time to request RealTraffic weather - positionTy pos; ///< viewer position for which we received Realtraffic weather + positionTy pos; ///< plane position for which we requested Realtraffic weather + NearestMETAR nearestMETAR; ///< info on nearest METAR long tOff = 0; ///< time offset for which we requested weather int nErr = 0; ///< How many errors did we have during weather requests? + LTWeather w; ///< interface to setting X-Plane's weather + std::array interp; ///< interpolation settings to convert from RT's 20 layers to XP's 13 + /// Set all relevant values - void set (double qnh, const CurrTy& o, bool bResetErr = true); + void set (double qnh, long _tOff, bool bResetErr = true); } rtWx; ///< Data with which latest weather was requested /// How many flights does RealTraffic have in total? long lTotalFlights = -1; + /// Shall we check for parked traffic next time around? (Set from main thread after airport data updates) + bool bDoParkedTraffic = false; // TCP connection to send current position std::thread thrTcpServer; ///< thread of the TCP listening thread (short-lived) @@ -305,6 +351,10 @@ class RealTrafficConnection : public LTFlightDataChannel // interface called from LTChannel // SetValid also sets internal status void SetValid (bool _valid, bool bMsg = true) override; + + /// Have connection read traffic data at next chance + void DoReadParkedTraffic () { bDoParkedTraffic = true; } + // // shall data of this channel be subject to LTFlightData::DataSmoothing? // bool DoDataSmoothing (double& gndRange, double& airbRange) const override // { gndRange = RT_SMOOTH_GROUND; airbRange = RT_SMOOTH_AIRBORNE; return true; } @@ -312,7 +362,9 @@ class RealTrafficConnection : public LTFlightDataChannel bool DoHoverDetection () const override { return true; } // Status - std::string GetStatusText () const override; ///< return a human-readable status + std::string GetStatusText () const override; ///< return a human-readable status + bool isHistoric () const { return curr.tOff > 0; } ///< serving historic data? + double GetTSAdjust () const { return tsAdjust; } ///< current timestamp adjustment protected: void Main () override; ///< virtual thread main function @@ -320,11 +372,18 @@ class RealTrafficConnection : public LTFlightDataChannel // MARK: Direct Connection by Request/Reply protected: void MainDirect (); ///< thread main function for the direct connection - void SetRequType (const positionTy& pos); ///< Which request do we need now? + /// Which request do we need next and when can we send it? + std::chrono::time_point SetRequType (const positionTy& pos); public: - std::string GetURL (const positionTy&) override; ///< in direct mode return URL and set + bool IsFirstRequ () const { return lTotalFlights < 0; } ///< Have not received any traffic data before? + std::string GetURL (const positionTy&) override; ///< in direct mode return URL and set void ComputeBody (const positionTy& pos) override; ///< in direct mode puts together the POST request with the position data etc. bool ProcessFetchedData () override; ///< in direct mode process the received data + bool ProcessTrafficBuffer (const JSON_Object* pBuf); ///< in direct mode process an object with aircraft data, essentially a fake array + bool ProcessParkedAcBuffer (const JSON_Object* pData); ///< in direct mode process an object with parked aircraft data, essentially a fake array + void ProcessNearestMETAR (const JSON_Array* pData); ///< in direct mode process NearestMETAR response, find a suitable METAR from the returned array + void ProcessWeather(const JSON_Object* pData); ///< in direct mode process detailed weather information + void ProcessCloudLayer(const JSON_Object* pCL,size_t i);///< in direct mode process one cloud layer // MARK: UDP/TCP via App protected: diff --git a/Include/LTWeather.h b/Include/LTWeather.h index 6d65c66..899f7b3 100644 --- a/Include/LTWeather.h +++ b/Include/LTWeather.h @@ -1,8 +1,7 @@ /// @file LTWeather.h -/// @brief Fetch real weather information from AWC -/// @see https://aviationweather.gov/data/api/#/Dataserver/dataserverMetars +/// @brief Set X-Plane weather / Fetch real weather information from AWC /// @author Birger Hoppe -/// @copyright (c) 2018-2023 Birger Hoppe +/// @copyright (c) 2018-2024 Birger Hoppe /// @copyright Permission is hereby granted, free of charge, to any person obtaining a /// copy of this software and associated documentation files (the "Software"), /// to deal in the Software without restriction, including without limitation @@ -22,7 +21,169 @@ #ifndef LTWeather_h #define LTWeather_h +class LTWeather; + +/// Initialize Weather module, dataRefs +bool WeatherInit (); +/// Shutdown Weather module +void WeatherStop (); + +/// Can we, technically, set weather? (X-Plane 12 forward only) +bool WeatherCanSet (); +/// Are we controlling weather? +bool WeatherInControl (); +/// Is X-Plane set to use real weather? +bool WeatherIsXPRealWeather (); +/// Have X-Plane use its real weather +void WeatherSetXPRealWeather (); + +/// Thread-safely store weather information to be set in X-Plane in the main thread later +void WeatherSet (const LTWeather& w); +/// Thread-safely store weather information to be set in X-Plane in the main thread later +void WeatherSet (const std::string& metar, const std::string& metarIcao); +/// @brief Set weather constantly to this METAR +/// @details Defines a weather solely based on the METAR, sets it, +/// then turns _off_ any further weather generation, so it stays constant. +/// @note Must be called from main thread +void WeatherSetConstant (const std::string& metar); +/// Actually update X-Plane's weather if there is anything to do (called from main thread) +void WeatherUpdate (); +/// Reset weather settings to what they were before X-Plane took over +void WeatherReset (); + +/// Log current weather +void WeatherLogCurrent (const std::string& msg); + +/// Current METAR in use for weather generation +const std::string& WeatherGetMETAR (); +/// Return a human readable string on the weather source, is "LiveTraffic" if WeatherInControl() +std::string WeatherGetSource (); + +/// Extract QNH or SLP from METAR, NAN if not found any info, which is rather unlikely +float WeatherQNHfromMETAR (const std::string& metar); + +// +// MARK: Set X-Plane Weather +// + +/// Distance when next weather is set to update immediately instead of gradually +constexpr double WEATHER_MAX_DIST_M = 50 * M_per_NM; +/// Standard thickness of a METAR cloud layer [m] +constexpr float WEATHER_METAR_CLOUD_HEIGHT_M = 500; +/// Minimum thickness of a METAR cloud layer [m] +constexpr float WEATHER_MIN_CLOUD_HEIGHT_M = 100; +/// Thickness of a METAR Cumulo-nimbus cloud layer [m] +constexpr float WEATHER_METAR_CB_CLOUD_HEIGHT_M = 5000; + +/// @brief Weather data to be set in X-Plane +/// @details A value of `NAN` means: don't set. +class LTWeather +{ +public: + /// Interpolation settings: indexes and weights to take over values from a differently sized float array + struct InterpolSet { + size_t i = 0; ///< lower index, other is i+1 + float w = 1.0f; ///< weight on lower index' value, other weight is 1.0f-w + }; + + positionTy pos; ///< position the weather refers to, effectively the camera view pos, including its altitude + + float visibility_reported_sm = NAN; ///< float y statute_miles = 0. The reported visibility (e.g. what the METAR/weather window says). + float sealevel_pressure_pas = NAN; ///< float y pascals Pressure at sea level, current planet + float sealevel_temperature_c = NAN; ///< float y degreesC The temperature at sea level. + float qnh_base_elevation = NAN; ///< float y float Base elevation for QNH. Takes into account local physical variations from a spheroid. + float qnh_pas = NAN; ///< float y float Base elevation for QNH. Takes into account local physical variations from a spheroid. + float rain_percent = NAN; ///< float y ratio [0.0 - 1.0] The percentage of rain falling. + std::array atmosphere_alt_levels_m; ///< float[13] n meters The altitudes for the thirteen atmospheric layers returned in other sim/weather/region datarefs. + std::array wind_altitude_msl_m; ///< float[13] y meters >= 0. The center altitude of this layer of wind in MSL meters. + std::array wind_speed_msc; ///< float[13] y kts >= 0. The wind speed in knots. + std::array wind_direction_degt; ///< float[13] y degrees [0 - 360] The direction the wind is blowing from in degrees from true north clockwise. + std::array shear_speed_msc; ///< float[13] y kts >= 0. The gain from the shear in knots. + std::array shear_direction_degt; ///< float[13] y degrees [0 - 360]. The direction for a wind shear, per above. + std::array turbulence; ///< float[13] y float [0.0 - 1.0] A turbulence factor, 0-10, the unit is just a scale. + std::array dewpoint_deg_c; ///< float[13] y degreesC The dew point at specified levels in the atmosphere. + std::array temperature_altitude_msl_m;///< float[13] y meters >= 0. Altitudes used for the temperatures_aloft_deg_c array. + std::array temperatures_aloft_deg_c; ///< float[13] y degreesC Temperature at pressure altitudes given in sim/weather/region/atmosphere_alt_levels. If the surface is at a higher elevation, the ISA difference at wherever the surface is is assumed to extend all the way down to sea level. + std::array cloud_type; ///< float[3] y float Blended cloud types per layer. 0 = Cirrus, 1 = Stratus, 2 = Cumulus, 3 = Cumulo-nimbus. Intermediate values are to be expected. + std::array cloud_coverage_percent; ///< float[3] y float Cloud coverage per layer, range 0 - 1. + std::array cloud_base_msl_m; ///< float[3] y meters MSL >= 0. The base altitude for this cloud layer. + std::array cloud_tops_msl_m; ///< float[3] y meters >= 0. The tops for this cloud layer. + float tropo_temp_c = NAN; ///< float y degreesC Temperature at the troposphere + float tropo_alt_m = NAN; ///< float y meters Altitude of the troposphere + float thermal_rate_ms = NAN; ///< float y m/s >= 0 The climb rate for thermals. + float wave_amplitude = NAN; ///< float y meters Amplitude of waves in the water (height of waves) + float wave_dir = NAN; ///< float y degrees Direction of waves. + int runway_friction = -1; ///< int y enum The friction constant for runways (how wet they are). Dry = 0, wet(1-3), puddly(4-6), snowy(7-9), icy(10-12), snowy/icy(13-15) + float variability_pct = 0; ///< float y ratio How randomly variable the weather is over distance. Range 0 - 1. + bool update_immediately = false; ///< int y bool If this is true, any weather region changes EXCEPT CLOUDS will take place immediately instead of at the next update interval (currently 60 seconds). + int change_mode = -1; ///< int y enum How the weather is changing. 0 = Rapidly Improving, 1 = Improving, 2 = Gradually Improving, 3 = Static, 4 = Gradually Deteriorating, 5 = Deteriorating, 6 = Rapidly Deteriorating, 7 = Using Real Weather + int weather_source = -1; ///< int n enum What system is currently controlling the weather. 0 = Preset, 1 = Real Weather, 2 = Controlpad, 3 = Plugin. + int weather_preset = -1; ///< int y enum Read the UI weather preset that is closest to the current conditions, or set an UI preset. Clear(0), VFR Few(1), VFR Scattered(2), VFR Broken(3), VFR Marginal(4), IFR Non-precision(5), IFR Precision(6), Convective(7), Large-cell Storms(8) + + // METAR + std::string metar; ///< METAR, if filled combine METAR data into weather generation + std::string metarFieldIcao; ///< METAR field's ICAO code + positionTy posMetarField; ///< position of the field the METAR refers to + +public: + LTWeather(); ///< Constructor sets all arrays to all `NAN` + + /// @brief Compute interpolation settings to fill one array from a differently sized one + /// @details: Both arrays are supposed to be sorted ascending. + /// They hold e.g. altimeter values of weather layers. + /// The result is how to interpolate values from one layer to the other + static std::array ComputeInterpol (const std::vector& from, + const std::array& to); + /// Fill values from a differently sized input vector based on interpolation + static void Interpolate (const std::array& aInterpol, + const std::vector& from, + std::array& to); + /// @brief Fill directions/headings from a differently sized input vector based on interpolation + /// @details Headings need to be interpolated separately as the average of 359 and 001 is 000 rather than 180... + static void InterpolateDir (const std::array& aInterpol, + const std::vector& from, + std::array& to); + + /// Get interpolated value for a given altitude + static float GetInterpolated (const std::array& levels_m, + const std::array& vals, + float alt_m); + + /// Fill value equally up to given altitude + static void FillUp (const std::array& levels_m, + std::array& to, + float alt_m, + float val, + bool bInterpolateNext); + /// Fill value equally to the given minimum up to given altitude (ie. don't overwrite values that are already larger) + static void FillUpMin (const std::array& levels_m, + std::array& to, + float alt_m, + float valMin, + bool bInterpolateNext); + +protected: + void Set () const; ///< Set the given weather in X-Plane + void Get (const std::string& logMsg = ""); ///< Read weather from X-Plane, if `logMsg` non-empty then log immediately (mith `logMsg` appearing on top) + void Log (const std::string& msg) const; ///< Log values to Log.txt + + bool IncorporateMETAR (); ///< add information from the METAR into the data (run from XP's main thread, so can use XP SDK, just before LTWeather::Set()) + +// Some global functions require access +friend void WeatherSet (const LTWeather& w); +friend void WeatherSet (const std::string& metar, const std::string& metarIcao); +friend void WeatherSetConstant (const std::string& metar); +friend void WeatherDoSet (); +friend void WeatherUpdate (); +friend void WeatherReset (); +friend void WeatherLogCurrent (const std::string& msg); +}; + +// +// MARK: Fetch METAR +// + /// Asynchronously, fetch fresh weather information -bool WeatherUpdate (const positionTy& pos, float radius_nm); +bool WeatherFetchUpdate (const positionTy& pos, float radius_nm); #endif /* LTWeather_h */ diff --git a/Include/LiveTraffic.h b/Include/LiveTraffic.h index 4142085..9be52ae 100644 --- a/Include/LiveTraffic.h +++ b/Include/LiveTraffic.h @@ -77,6 +77,8 @@ #include #include #include +#include +#include // X-Plane SDK #include "XPLMDisplay.h" @@ -373,6 +375,9 @@ std::string GetNearestAirportId (const positionTy& _pos, positionTy* outApPos = inline std::string GetNearestAirportId (float lat, float lon) { return GetNearestAirportId(positionTy((double)lat,(double)lon)); } +/// Fetch specific airport location/altitude +positionTy GetAirportLoc (const std::string sICAO); + /// Convert ADS-B Emitter Category to text const char* GetADSBEmitterCat (const std::string& cat); @@ -407,6 +412,11 @@ bool dequal ( const double d1, const double d2 ); inline double nanToZero (double d) { return std::isnan(d) ? 0.0 : d; } +/// Find an interpolated value +float interpolate (const std::vector& scale, + const std::vector& values, + float pos_in_scale); + /// @brief random long between too given values invlusive /// @see https://stackoverflow.com/a/7560171 inline long randoml (long min, long max) diff --git a/Include/SettingsUI.h b/Include/SettingsUI.h index f0d8674..d62a18f 100644 --- a/Include/SettingsUI.h +++ b/Include/SettingsUI.h @@ -64,6 +64,8 @@ class LTSettingsUI : public LTImgWindow std::string sFSCPwd; ///< FSC password bool bFSCPwdClearText = false; ///< Is FSC pwd displayed clear text? + // Weather + std::string txtManualMETAR; ///< manually set METAR for fixed weather // CSL int cslActiveLn = -1; ///< CSL path line currently being edited diff --git a/Lib/ImGui/imgui_widgets.cpp b/Lib/ImGui/imgui_widgets.cpp index 79d52dc..cafa836 100644 --- a/Lib/ImGui/imgui_widgets.cpp +++ b/Lib/ImGui/imgui_widgets.cpp @@ -8402,7 +8402,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) const float padding_auto_x = table->CellPaddingX2; const float min_column_width = TableGetMinColumnWidth(); - int count_fixed = 0; +// int count_fixed = 0; -- set but never used float sum_weights_stretched = 0.0f; // Sum of all weights for weighted columns. float sum_width_fixed_requests = 0.0f; // Sum of all width for fixed and auto-resize columns, excluding width contributed by Stretch columns. table->LeftMostStretchedColumnDisplayOrder = -1; @@ -8448,7 +8448,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) if (column->Flags & (ImGuiTableColumnFlags_WidthAlwaysAutoResize | ImGuiTableColumnFlags_WidthFixed)) { // Latch initial size for fixed columns - count_fixed += 1; +// count_fixed += 1; -- set but never used const bool auto_fit = (column->AutoFitQueue != 0x00) || (column->Flags & ImGuiTableColumnFlags_WidthAlwaysAutoResize); if (auto_fit) { diff --git a/Lib/XPMP2 b/Lib/XPMP2 index 2c1377a..38579cb 160000 --- a/Lib/XPMP2 +++ b/Lib/XPMP2 @@ -1 +1 @@ -Subproject commit 2c1377a21967fb4b3157d836d8317cabca6b4cc4 +Subproject commit 38579cb34ddc16e36befef6e0a7ea815cf7f708f diff --git a/Lib/metaf b/Lib/metaf new file mode 160000 index 0000000..21f23a2 --- /dev/null +++ b/Lib/metaf @@ -0,0 +1 @@ +Subproject commit 21f23a22686060ae5345d87b305645911e9cdaa7 diff --git a/Lib/parson b/Lib/parson new file mode 160000 index 0000000..ba29f4e --- /dev/null +++ b/Lib/parson @@ -0,0 +1 @@ +Subproject commit ba29f4eda9ea7703a9f6a9cf2b0532a2605723c3 diff --git a/Lib/parson/parson.c b/Lib/parson/parson.c deleted file mode 100644 index 526aab4..0000000 --- a/Lib/parson/parson.c +++ /dev/null @@ -1,2486 +0,0 @@ -/* - SPDX-License-Identifier: MIT - - Parson 1.5.3 (https://github.com/kgabis/parson) - Copyright (c) 2012 - 2023 Krzysztof Gabis - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ -#ifdef _MSC_VER -#ifndef _CRT_SECURE_NO_WARNINGS -#define _CRT_SECURE_NO_WARNINGS -#endif /* _CRT_SECURE_NO_WARNINGS */ -#endif /* _MSC_VER */ - -#include "parson.h" - -#define PARSON_IMPL_VERSION_MAJOR 1 -#define PARSON_IMPL_VERSION_MINOR 5 -#define PARSON_IMPL_VERSION_PATCH 3 - -#if (PARSON_VERSION_MAJOR != PARSON_IMPL_VERSION_MAJOR)\ -|| (PARSON_VERSION_MINOR != PARSON_IMPL_VERSION_MINOR)\ -|| (PARSON_VERSION_PATCH != PARSON_IMPL_VERSION_PATCH) -#error "parson version mismatch between parson.c and parson.h" -#endif - -#include -#include -#include -#include -#include -#include -#include - -/* Apparently sscanf is not implemented in some "standard" libraries, so don't use it, if you - * don't have to. */ -#ifdef sscanf -#undef sscanf -#define sscanf THINK_TWICE_ABOUT_USING_SSCANF -#endif - -/* strcpy is unsafe */ -#ifdef strcpy -#undef strcpy -#endif -#define strcpy USE_MEMCPY_INSTEAD_OF_STRCPY - -#define STARTING_CAPACITY 16 -#define MAX_NESTING 2048 - -#ifndef PARSON_DEFAULT_FLOAT_FORMAT -#define PARSON_DEFAULT_FLOAT_FORMAT "%1.17g" /* do not increase precision without incresing NUM_BUF_SIZE */ -#endif - -#ifndef PARSON_NUM_BUF_SIZE -#define PARSON_NUM_BUF_SIZE 64 /* double printed with "%1.17g" shouldn't be longer than 25 bytes so let's be paranoid and use 64 */ -#endif - -#ifndef PARSON_INDENT_STR -#define PARSON_INDENT_STR " " -#endif - -#define SIZEOF_TOKEN(a) (sizeof(a) - 1) -#define SKIP_CHAR(str) ((*str)++) -#define SKIP_WHITESPACES(str) while (isspace((unsigned char)(**str))) { SKIP_CHAR(str); } -#define MAX(a, b) ((a) > (b) ? (a) : (b)) - -#undef malloc -#undef free - -#if defined(isnan) && defined(isinf) -#define IS_NUMBER_INVALID(x) (isnan((x)) || isinf((x))) -#else -#define IS_NUMBER_INVALID(x) (((x) * 0.0) != 0.0) -#endif - -#define OBJECT_INVALID_IX ((size_t)-1) - -static JSON_Malloc_Function parson_malloc = malloc; -static JSON_Free_Function parson_free = free; - -static int parson_escape_slashes = 1; - -static char *parson_float_format = NULL; - -static JSON_Number_Serialization_Function parson_number_serialization_function = NULL; - -#define IS_CONT(b) (((unsigned char)(b) & 0xC0) == 0x80) /* is utf-8 continuation byte */ - -typedef int parson_bool_t; - -#define PARSON_TRUE 1 -#define PARSON_FALSE 0 - -typedef struct json_string { - char *chars; - size_t length; -} JSON_String; - -/* Type definitions */ -typedef union json_value_value { - JSON_String string; - double number; - JSON_Object *object; - JSON_Array *array; - int boolean; - int null; -} JSON_Value_Value; - -struct json_value_t { - JSON_Value *parent; - JSON_Value_Type type; - JSON_Value_Value value; -}; - -struct json_object_t { - JSON_Value *wrapping_value; - size_t *cells; - unsigned long *hashes; - char **names; - JSON_Value **values; - size_t *cell_ixs; - size_t count; - size_t item_capacity; - size_t cell_capacity; -}; - -struct json_array_t { - JSON_Value *wrapping_value; - JSON_Value **items; - size_t count; - size_t capacity; -}; - -/* Various */ -static char * read_file(const char *filename); -static void remove_comments(char *string, const char *start_token, const char *end_token); -static char * parson_strndup(const char *string, size_t n); -static char * parson_strdup(const char *string); -static int parson_sprintf(char * s, const char * format, ...); - -static int hex_char_to_int(char c); -static JSON_Status parse_utf16_hex(const char *string, unsigned int *result); -static int num_bytes_in_utf8_sequence(unsigned char c); -static JSON_Status verify_utf8_sequence(const unsigned char *string, int *len); -static parson_bool_t is_valid_utf8(const char *string, size_t string_len); -static parson_bool_t is_decimal(const char *string, size_t length); -static unsigned long hash_string(const char *string, size_t n); - -/* JSON Object */ -static JSON_Object * json_object_make(JSON_Value *wrapping_value); -static JSON_Status json_object_init(JSON_Object *object, size_t capacity); -static void json_object_deinit(JSON_Object *object, parson_bool_t free_keys, parson_bool_t free_values); -static JSON_Status json_object_grow_and_rehash(JSON_Object *object); -static size_t json_object_get_cell_ix(const JSON_Object *object, const char *key, size_t key_len, unsigned long hash, parson_bool_t *out_found); -static JSON_Status json_object_add(JSON_Object *object, char *name, JSON_Value *value); -static JSON_Value * json_object_getn_value(const JSON_Object *object, const char *name, size_t name_len); -static JSON_Status json_object_remove_internal(JSON_Object *object, const char *name, parson_bool_t free_value); -static JSON_Status json_object_dotremove_internal(JSON_Object *object, const char *name, parson_bool_t free_value); -static void json_object_free(JSON_Object *object); - -/* JSON Array */ -static JSON_Array * json_array_make(JSON_Value *wrapping_value); -static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value); -static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity); -static void json_array_free(JSON_Array *array); - -/* JSON Value */ -static JSON_Value * json_value_init_string_no_copy(char *string, size_t length); -static const JSON_String * json_value_get_string_desc(const JSON_Value *value); - -/* Parser */ -static JSON_Status skip_quotes(const char **string); -static JSON_Status parse_utf16(const char **unprocessed, char **processed); -static char * process_string(const char *input, size_t input_len, size_t *output_len); -static char * get_quoted_string(const char **string, size_t *output_string_len); -static JSON_Value * parse_object_value(const char **string, size_t nesting); -static JSON_Value * parse_array_value(const char **string, size_t nesting); -static JSON_Value * parse_string_value(const char **string); -static JSON_Value * parse_boolean_value(const char **string); -static JSON_Value * parse_number_value(const char **string); -static JSON_Value * parse_null_value(const char **string); -static JSON_Value * parse_value(const char **string, size_t nesting); - -/* Serialization */ -static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, parson_bool_t is_pretty, char *num_buf); -static int json_serialize_string(const char *string, size_t len, char *buf); - -/* Various */ -static char * read_file(const char * filename) { - FILE *fp = fopen(filename, "r"); - size_t size_to_read = 0; - size_t size_read = 0; - long pos; - char *file_contents; - if (!fp) { - return NULL; - } - fseek(fp, 0L, SEEK_END); - pos = ftell(fp); - if (pos < 0) { - fclose(fp); - return NULL; - } - size_to_read = pos; - rewind(fp); - file_contents = (char*)parson_malloc(sizeof(char) * (size_to_read + 1)); - if (!file_contents) { - fclose(fp); - return NULL; - } - size_read = fread(file_contents, 1, size_to_read, fp); - if (size_read == 0 || ferror(fp)) { - fclose(fp); - parson_free(file_contents); - return NULL; - } - fclose(fp); - file_contents[size_read] = '\0'; - return file_contents; -} - -static void remove_comments(char *string, const char *start_token, const char *end_token) { - parson_bool_t in_string = PARSON_FALSE, escaped = PARSON_FALSE; - size_t i; - char *ptr = NULL, current_char; - size_t start_token_len = strlen(start_token); - size_t end_token_len = strlen(end_token); - if (start_token_len == 0 || end_token_len == 0) { - return; - } - while ((current_char = *string) != '\0') { - if (current_char == '\\' && !escaped) { - escaped = PARSON_TRUE; - string++; - continue; - } else if (current_char == '\"' && !escaped) { - in_string = !in_string; - } else if (!in_string && strncmp(string, start_token, start_token_len) == 0) { - for(i = 0; i < start_token_len; i++) { - string[i] = ' '; - } - string = string + start_token_len; - ptr = strstr(string, end_token); - if (!ptr) { - return; - } - for (i = 0; i < (ptr - string) + end_token_len; i++) { - string[i] = ' '; - } - string = ptr + end_token_len - 1; - } - escaped = PARSON_FALSE; - string++; - } -} - -static char * parson_strndup(const char *string, size_t n) { - /* We expect the caller has validated that 'n' fits within the input buffer. */ - char *output_string = (char*)parson_malloc(n + 1); - if (!output_string) { - return NULL; - } - output_string[n] = '\0'; - memcpy(output_string, string, n); - return output_string; -} - -static char * parson_strdup(const char *string) { - return parson_strndup(string, strlen(string)); -} - -static int parson_sprintf(char * s, const char * format, ...) { - int result; - va_list args; - va_start(args, format); - - #if defined(__APPLE__) && defined(__clang__) - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - #endif - result = vsprintf(s, format, args); - #if defined(__APPLE__) && defined(__clang__) - #pragma clang diagnostic pop - #endif - - va_end(args); - return result; - -} - -static int hex_char_to_int(char c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } else if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } else if (c >= 'A' && c <= 'F') { - return c - 'A' + 10; - } - return -1; -} - -static JSON_Status parse_utf16_hex(const char *s, unsigned int *result) { - int x1, x2, x3, x4; - if (s[0] == '\0' || s[1] == '\0' || s[2] == '\0' || s[3] == '\0') { - return JSONFailure; - } - x1 = hex_char_to_int(s[0]); - x2 = hex_char_to_int(s[1]); - x3 = hex_char_to_int(s[2]); - x4 = hex_char_to_int(s[3]); - if (x1 == -1 || x2 == -1 || x3 == -1 || x4 == -1) { - return JSONFailure; - } - *result = (unsigned int)((x1 << 12) | (x2 << 8) | (x3 << 4) | x4); - return JSONSuccess; -} - -static int num_bytes_in_utf8_sequence(unsigned char c) { - if (c == 0xC0 || c == 0xC1 || c > 0xF4 || IS_CONT(c)) { - return 0; - } else if ((c & 0x80) == 0) { /* 0xxxxxxx */ - return 1; - } else if ((c & 0xE0) == 0xC0) { /* 110xxxxx */ - return 2; - } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx */ - return 3; - } else if ((c & 0xF8) == 0xF0) { /* 11110xxx */ - return 4; - } - return 0; /* won't happen */ -} - -static JSON_Status verify_utf8_sequence(const unsigned char *string, int *len) { - unsigned int cp = 0; - *len = num_bytes_in_utf8_sequence(string[0]); - - if (*len == 1) { - cp = string[0]; - } else if (*len == 2 && IS_CONT(string[1])) { - cp = string[0] & 0x1F; - cp = (cp << 6) | (string[1] & 0x3F); - } else if (*len == 3 && IS_CONT(string[1]) && IS_CONT(string[2])) { - cp = ((unsigned char)string[0]) & 0xF; - cp = (cp << 6) | (string[1] & 0x3F); - cp = (cp << 6) | (string[2] & 0x3F); - } else if (*len == 4 && IS_CONT(string[1]) && IS_CONT(string[2]) && IS_CONT(string[3])) { - cp = string[0] & 0x7; - cp = (cp << 6) | (string[1] & 0x3F); - cp = (cp << 6) | (string[2] & 0x3F); - cp = (cp << 6) | (string[3] & 0x3F); - } else { - return JSONFailure; - } - - /* overlong encodings */ - if ((cp < 0x80 && *len > 1) || - (cp < 0x800 && *len > 2) || - (cp < 0x10000 && *len > 3)) { - return JSONFailure; - } - - /* invalid unicode */ - if (cp > 0x10FFFF) { - return JSONFailure; - } - - /* surrogate halves */ - if (cp >= 0xD800 && cp <= 0xDFFF) { - return JSONFailure; - } - - return JSONSuccess; -} - -static int is_valid_utf8(const char *string, size_t string_len) { - int len = 0; - const char *string_end = string + string_len; - while (string < string_end) { - if (verify_utf8_sequence((const unsigned char*)string, &len) != JSONSuccess) { - return PARSON_FALSE; - } - string += len; - } - return PARSON_TRUE; -} - -static parson_bool_t is_decimal(const char *string, size_t length) { - if (length > 1 && string[0] == '0' && string[1] != '.') { - return PARSON_FALSE; - } - if (length > 2 && !strncmp(string, "-0", 2) && string[2] != '.') { - return PARSON_FALSE; - } - while (length--) { - if (strchr("xX", string[length])) { - return PARSON_FALSE; - } - } - return PARSON_TRUE; -} - -static unsigned long hash_string(const char *string, size_t n) { -#ifdef PARSON_FORCE_HASH_COLLISIONS - (void)string; - (void)n; - return 0; -#else - unsigned long hash = 5381; - unsigned char c; - size_t i = 0; - for (i = 0; i < n; i++) { - c = string[i]; - if (c == '\0') { - break; - } - hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ - } - return hash; -#endif -} - -/* JSON Object */ -static JSON_Object * json_object_make(JSON_Value *wrapping_value) { - JSON_Status res = JSONFailure; - JSON_Object *new_obj = (JSON_Object*)parson_malloc(sizeof(JSON_Object)); - if (new_obj == NULL) { - return NULL; - } - new_obj->wrapping_value = wrapping_value; - res = json_object_init(new_obj, 0); - if (res != JSONSuccess) { - parson_free(new_obj); - return NULL; - } - return new_obj; -} - -static JSON_Status json_object_init(JSON_Object *object, size_t capacity) { - unsigned int i = 0; - - object->cells = NULL; - object->names = NULL; - object->values = NULL; - object->cell_ixs = NULL; - object->hashes = NULL; - - object->count = 0; - object->cell_capacity = capacity; - object->item_capacity = (unsigned int)(capacity * 7/10); - - if (capacity == 0) { - return JSONSuccess; - } - - object->cells = (size_t*)parson_malloc(object->cell_capacity * sizeof(*object->cells)); - object->names = (char**)parson_malloc(object->item_capacity * sizeof(*object->names)); - object->values = (JSON_Value**)parson_malloc(object->item_capacity * sizeof(*object->values)); - object->cell_ixs = (size_t*)parson_malloc(object->item_capacity * sizeof(*object->cell_ixs)); - object->hashes = (unsigned long*)parson_malloc(object->item_capacity * sizeof(*object->hashes)); - if (object->cells == NULL - || object->names == NULL - || object->values == NULL - || object->cell_ixs == NULL - || object->hashes == NULL) { - goto error; - } - for (i = 0; i < object->cell_capacity; i++) { - object->cells[i] = OBJECT_INVALID_IX; - } - return JSONSuccess; -error: - parson_free(object->cells); - parson_free(object->names); - parson_free(object->values); - parson_free(object->cell_ixs); - parson_free(object->hashes); - return JSONFailure; -} - -static void json_object_deinit(JSON_Object *object, parson_bool_t free_keys, parson_bool_t free_values) { - unsigned int i = 0; - for (i = 0; i < object->count; i++) { - if (free_keys) { - parson_free(object->names[i]); - } - if (free_values) { - json_value_free(object->values[i]); - } - } - - object->count = 0; - object->item_capacity = 0; - object->cell_capacity = 0; - - parson_free(object->cells); - parson_free(object->names); - parson_free(object->values); - parson_free(object->cell_ixs); - parson_free(object->hashes); - - object->cells = NULL; - object->names = NULL; - object->values = NULL; - object->cell_ixs = NULL; - object->hashes = NULL; -} - -static JSON_Status json_object_grow_and_rehash(JSON_Object *object) { - JSON_Value *wrapping_value = NULL; - JSON_Object new_object; - char *key = NULL; - JSON_Value *value = NULL; - unsigned int i = 0; - size_t new_capacity = MAX(object->cell_capacity * 2, STARTING_CAPACITY); - JSON_Status res = json_object_init(&new_object, new_capacity); - if (res != JSONSuccess) { - return JSONFailure; - } - - wrapping_value = json_object_get_wrapping_value(object); - new_object.wrapping_value = wrapping_value; - - for (i = 0; i < object->count; i++) { - key = object->names[i]; - value = object->values[i]; - res = json_object_add(&new_object, key, value); - if (res != JSONSuccess) { - json_object_deinit(&new_object, PARSON_FALSE, PARSON_FALSE); - return JSONFailure; - } - value->parent = wrapping_value; - } - json_object_deinit(object, PARSON_FALSE, PARSON_FALSE); - *object = new_object; - return JSONSuccess; -} - -static size_t json_object_get_cell_ix(const JSON_Object *object, const char *key, size_t key_len, unsigned long hash, parson_bool_t *out_found) { - size_t cell_ix = hash & (object->cell_capacity - 1); - size_t cell = 0; - size_t ix = 0; - unsigned int i = 0; - unsigned long hash_to_check = 0; - const char *key_to_check = NULL; - size_t key_to_check_len = 0; - - *out_found = PARSON_FALSE; - - for (i = 0; i < object->cell_capacity; i++) { - ix = (cell_ix + i) & (object->cell_capacity - 1); - cell = object->cells[ix]; - if (cell == OBJECT_INVALID_IX) { - return ix; - } - hash_to_check = object->hashes[cell]; - if (hash != hash_to_check) { - continue; - } - key_to_check = object->names[cell]; - key_to_check_len = strlen(key_to_check); - if (key_to_check_len == key_len && strncmp(key, key_to_check, key_len) == 0) { - *out_found = PARSON_TRUE; - return ix; - } - } - return OBJECT_INVALID_IX; -} - -static JSON_Status json_object_add(JSON_Object *object, char *name, JSON_Value *value) { - unsigned long hash = 0; - parson_bool_t found = PARSON_FALSE; - size_t cell_ix = 0; - JSON_Status res = JSONFailure; - - if (!object || !name || !value) { - return JSONFailure; - } - - hash = hash_string(name, strlen(name)); - found = PARSON_FALSE; - cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); - if (found) { - return JSONFailure; - } - - if (object->count >= object->item_capacity) { - res = json_object_grow_and_rehash(object); - if (res != JSONSuccess) { - return JSONFailure; - } - cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); - } - - object->names[object->count] = name; - object->cells[cell_ix] = object->count; - object->values[object->count] = value; - object->cell_ixs[object->count] = cell_ix; - object->hashes[object->count] = hash; - object->count++; - value->parent = json_object_get_wrapping_value(object); - - return JSONSuccess; -} - -static JSON_Value * json_object_getn_value(const JSON_Object *object, const char *name, size_t name_len) { - unsigned long hash = 0; - parson_bool_t found = PARSON_FALSE; - size_t cell_ix = 0; - size_t item_ix = 0; - if (!object || !name) { - return NULL; - } - hash = hash_string(name, name_len); - found = PARSON_FALSE; - cell_ix = json_object_get_cell_ix(object, name, name_len, hash, &found); - if (!found) { - return NULL; - } - item_ix = object->cells[cell_ix]; - return object->values[item_ix]; -} - -static JSON_Status json_object_remove_internal(JSON_Object *object, const char *name, parson_bool_t free_value) { - unsigned long hash = 0; - parson_bool_t found = PARSON_FALSE; - size_t cell = 0; - size_t item_ix = 0; - size_t last_item_ix = 0; - size_t i = 0; - size_t j = 0; - size_t x = 0; - size_t k = 0; - JSON_Value *val = NULL; - - if (object == NULL) { - return JSONFailure; - } - - hash = hash_string(name, strlen(name)); - found = PARSON_FALSE; - cell = json_object_get_cell_ix(object, name, strlen(name), hash, &found); - if (!found) { - return JSONFailure; - } - - item_ix = object->cells[cell]; - if (free_value) { - val = object->values[item_ix]; - json_value_free(val); - val = NULL; - } - - parson_free(object->names[item_ix]); - last_item_ix = object->count - 1; - if (item_ix < last_item_ix) { - object->names[item_ix] = object->names[last_item_ix]; - object->values[item_ix] = object->values[last_item_ix]; - object->cell_ixs[item_ix] = object->cell_ixs[last_item_ix]; - object->hashes[item_ix] = object->hashes[last_item_ix]; - object->cells[object->cell_ixs[item_ix]] = item_ix; - } - object->count--; - - i = cell; - j = i; - for (x = 0; x < (object->cell_capacity - 1); x++) { - j = (j + 1) & (object->cell_capacity - 1); - if (object->cells[j] == OBJECT_INVALID_IX) { - break; - } - k = object->hashes[object->cells[j]] & (object->cell_capacity - 1); - if ((j > i && (k <= i || k > j)) - || (j < i && (k <= i && k > j))) { - object->cell_ixs[object->cells[j]] = i; - object->cells[i] = object->cells[j]; - i = j; - } - } - object->cells[i] = OBJECT_INVALID_IX; - return JSONSuccess; -} - -static JSON_Status json_object_dotremove_internal(JSON_Object *object, const char *name, parson_bool_t free_value) { - JSON_Value *temp_value = NULL; - JSON_Object *temp_object = NULL; - const char *dot_pos = strchr(name, '.'); - if (!dot_pos) { - return json_object_remove_internal(object, name, free_value); - } - temp_value = json_object_getn_value(object, name, dot_pos - name); - if (json_value_get_type(temp_value) != JSONObject) { - return JSONFailure; - } - temp_object = json_value_get_object(temp_value); - return json_object_dotremove_internal(temp_object, dot_pos + 1, free_value); -} - -static void json_object_free(JSON_Object *object) { - json_object_deinit(object, PARSON_TRUE, PARSON_TRUE); - parson_free(object); -} - -/* JSON Array */ -static JSON_Array * json_array_make(JSON_Value *wrapping_value) { - JSON_Array *new_array = (JSON_Array*)parson_malloc(sizeof(JSON_Array)); - if (new_array == NULL) { - return NULL; - } - new_array->wrapping_value = wrapping_value; - new_array->items = (JSON_Value**)NULL; - new_array->capacity = 0; - new_array->count = 0; - return new_array; -} - -static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value) { - if (array->count >= array->capacity) { - size_t new_capacity = MAX(array->capacity * 2, STARTING_CAPACITY); - if (json_array_resize(array, new_capacity) != JSONSuccess) { - return JSONFailure; - } - } - value->parent = json_array_get_wrapping_value(array); - array->items[array->count] = value; - array->count++; - return JSONSuccess; -} - -static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity) { - JSON_Value **new_items = NULL; - if (new_capacity == 0) { - return JSONFailure; - } - new_items = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*)); - if (new_items == NULL) { - return JSONFailure; - } - if (array->items != NULL && array->count > 0) { - memcpy(new_items, array->items, array->count * sizeof(JSON_Value*)); - } - parson_free(array->items); - array->items = new_items; - array->capacity = new_capacity; - return JSONSuccess; -} - -static void json_array_free(JSON_Array *array) { - size_t i; - for (i = 0; i < array->count; i++) { - json_value_free(array->items[i]); - } - parson_free(array->items); - parson_free(array); -} - -/* JSON Value */ -static JSON_Value * json_value_init_string_no_copy(char *string, size_t length) { - JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); - if (!new_value) { - return NULL; - } - new_value->parent = NULL; - new_value->type = JSONString; - new_value->value.string.chars = string; - new_value->value.string.length = length; - return new_value; -} - -/* Parser */ -static JSON_Status skip_quotes(const char **string) { - if (**string != '\"') { - return JSONFailure; - } - SKIP_CHAR(string); - while (**string != '\"') { - if (**string == '\0') { - return JSONFailure; - } else if (**string == '\\') { - SKIP_CHAR(string); - if (**string == '\0') { - return JSONFailure; - } - } - SKIP_CHAR(string); - } - SKIP_CHAR(string); - return JSONSuccess; -} - -static JSON_Status parse_utf16(const char **unprocessed, char **processed) { - unsigned int cp, lead, trail; - char *processed_ptr = *processed; - const char *unprocessed_ptr = *unprocessed; - JSON_Status status = JSONFailure; - unprocessed_ptr++; /* skips u */ - status = parse_utf16_hex(unprocessed_ptr, &cp); - if (status != JSONSuccess) { - return JSONFailure; - } - if (cp < 0x80) { - processed_ptr[0] = (char)cp; /* 0xxxxxxx */ - } else if (cp < 0x800) { - processed_ptr[0] = ((cp >> 6) & 0x1F) | 0xC0; /* 110xxxxx */ - processed_ptr[1] = ((cp) & 0x3F) | 0x80; /* 10xxxxxx */ - processed_ptr += 1; - } else if (cp < 0xD800 || cp > 0xDFFF) { - processed_ptr[0] = ((cp >> 12) & 0x0F) | 0xE0; /* 1110xxxx */ - processed_ptr[1] = ((cp >> 6) & 0x3F) | 0x80; /* 10xxxxxx */ - processed_ptr[2] = ((cp) & 0x3F) | 0x80; /* 10xxxxxx */ - processed_ptr += 2; - } else if (cp >= 0xD800 && cp <= 0xDBFF) { /* lead surrogate (0xD800..0xDBFF) */ - lead = cp; - unprocessed_ptr += 4; /* should always be within the buffer, otherwise previous sscanf would fail */ - if (*unprocessed_ptr++ != '\\' || *unprocessed_ptr++ != 'u') { - return JSONFailure; - } - status = parse_utf16_hex(unprocessed_ptr, &trail); - if (status != JSONSuccess || trail < 0xDC00 || trail > 0xDFFF) { /* valid trail surrogate? (0xDC00..0xDFFF) */ - return JSONFailure; - } - cp = ((((lead - 0xD800) & 0x3FF) << 10) | ((trail - 0xDC00) & 0x3FF)) + 0x010000; - processed_ptr[0] = (((cp >> 18) & 0x07) | 0xF0); /* 11110xxx */ - processed_ptr[1] = (((cp >> 12) & 0x3F) | 0x80); /* 10xxxxxx */ - processed_ptr[2] = (((cp >> 6) & 0x3F) | 0x80); /* 10xxxxxx */ - processed_ptr[3] = (((cp) & 0x3F) | 0x80); /* 10xxxxxx */ - processed_ptr += 3; - } else { /* trail surrogate before lead surrogate */ - return JSONFailure; - } - unprocessed_ptr += 3; - *processed = processed_ptr; - *unprocessed = unprocessed_ptr; - return JSONSuccess; -} - - -/* Copies and processes passed string up to supplied length. -Example: "\u006Corem ipsum" -> lorem ipsum */ -static char* process_string(const char *input, size_t input_len, size_t *output_len) { - const char *input_ptr = input; - size_t initial_size = (input_len + 1) * sizeof(char); - size_t final_size = 0; - char *output = NULL, *output_ptr = NULL, *resized_output = NULL; - output = (char*)parson_malloc(initial_size); - if (output == NULL) { - goto error; - } - output_ptr = output; - while ((*input_ptr != '\0') && (size_t)(input_ptr - input) < input_len) { - if (*input_ptr == '\\') { - input_ptr++; - switch (*input_ptr) { - case '\"': *output_ptr = '\"'; break; - case '\\': *output_ptr = '\\'; break; - case '/': *output_ptr = '/'; break; - case 'b': *output_ptr = '\b'; break; - case 'f': *output_ptr = '\f'; break; - case 'n': *output_ptr = '\n'; break; - case 'r': *output_ptr = '\r'; break; - case 't': *output_ptr = '\t'; break; - case 'u': - if (parse_utf16(&input_ptr, &output_ptr) != JSONSuccess) { - goto error; - } - break; - default: - goto error; - } - } else if ((unsigned char)*input_ptr < 0x20) { - goto error; /* 0x00-0x19 are invalid characters for json string (http://www.ietf.org/rfc/rfc4627.txt) */ - } else { - *output_ptr = *input_ptr; - } - output_ptr++; - input_ptr++; - } - *output_ptr = '\0'; - /* resize to new length */ - final_size = (size_t)(output_ptr-output) + 1; - /* todo: don't resize if final_size == initial_size */ - resized_output = (char*)parson_malloc(final_size); - if (resized_output == NULL) { - goto error; - } - memcpy(resized_output, output, final_size); - *output_len = final_size - 1; - parson_free(output); - return resized_output; -error: - parson_free(output); - return NULL; -} - -/* Return processed contents of a string between quotes and - skips passed argument to a matching quote. */ -static char * get_quoted_string(const char **string, size_t *output_string_len) { - const char *string_start = *string; - size_t input_string_len = 0; - JSON_Status status = skip_quotes(string); - if (status != JSONSuccess) { - return NULL; - } - input_string_len = *string - string_start - 2; /* length without quotes */ - return process_string(string_start + 1, input_string_len, output_string_len); -} - -static JSON_Value * parse_value(const char **string, size_t nesting) { - if (nesting > MAX_NESTING) { - return NULL; - } - SKIP_WHITESPACES(string); - switch (**string) { - case '{': - return parse_object_value(string, nesting + 1); - case '[': - return parse_array_value(string, nesting + 1); - case '\"': - return parse_string_value(string); - case 'f': case 't': - return parse_boolean_value(string); - case '-': - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - return parse_number_value(string); - case 'n': - return parse_null_value(string); - default: - return NULL; - } -} - -static JSON_Value * parse_object_value(const char **string, size_t nesting) { - JSON_Status status = JSONFailure; - JSON_Value *output_value = NULL, *new_value = NULL; - JSON_Object *output_object = NULL; - char *new_key = NULL; - - output_value = json_value_init_object(); - if (output_value == NULL) { - return NULL; - } - if (**string != '{') { - json_value_free(output_value); - return NULL; - } - output_object = json_value_get_object(output_value); - SKIP_CHAR(string); - SKIP_WHITESPACES(string); - if (**string == '}') { /* empty object */ - SKIP_CHAR(string); - return output_value; - } - while (**string != '\0') { - size_t key_len = 0; - new_key = get_quoted_string(string, &key_len); - /* We do not support key names with embedded \0 chars */ - if (!new_key) { - json_value_free(output_value); - return NULL; - } - if (key_len != strlen(new_key)) { - parson_free(new_key); - json_value_free(output_value); - return NULL; - } - SKIP_WHITESPACES(string); - if (**string != ':') { - parson_free(new_key); - json_value_free(output_value); - return NULL; - } - SKIP_CHAR(string); - new_value = parse_value(string, nesting); - if (new_value == NULL) { - parson_free(new_key); - json_value_free(output_value); - return NULL; - } - status = json_object_add(output_object, new_key, new_value); - if (status != JSONSuccess) { - parson_free(new_key); - json_value_free(new_value); - json_value_free(output_value); - return NULL; - } - SKIP_WHITESPACES(string); - if (**string != ',') { - break; - } - SKIP_CHAR(string); - SKIP_WHITESPACES(string); - if (**string == '}') { - break; - } - } - SKIP_WHITESPACES(string); - if (**string != '}') { - json_value_free(output_value); - return NULL; - } - SKIP_CHAR(string); - return output_value; -} - -static JSON_Value * parse_array_value(const char **string, size_t nesting) { - JSON_Value *output_value = NULL, *new_array_value = NULL; - JSON_Array *output_array = NULL; - output_value = json_value_init_array(); - if (output_value == NULL) { - return NULL; - } - if (**string != '[') { - json_value_free(output_value); - return NULL; - } - output_array = json_value_get_array(output_value); - SKIP_CHAR(string); - SKIP_WHITESPACES(string); - if (**string == ']') { /* empty array */ - SKIP_CHAR(string); - return output_value; - } - while (**string != '\0') { - new_array_value = parse_value(string, nesting); - if (new_array_value == NULL) { - json_value_free(output_value); - return NULL; - } - if (json_array_add(output_array, new_array_value) != JSONSuccess) { - json_value_free(new_array_value); - json_value_free(output_value); - return NULL; - } - SKIP_WHITESPACES(string); - if (**string != ',') { - break; - } - SKIP_CHAR(string); - SKIP_WHITESPACES(string); - if (**string == ']') { - break; - } - } - SKIP_WHITESPACES(string); - if (**string != ']' || /* Trim array after parsing is over */ - json_array_resize(output_array, json_array_get_count(output_array)) != JSONSuccess) { - json_value_free(output_value); - return NULL; - } - SKIP_CHAR(string); - return output_value; -} - -static JSON_Value * parse_string_value(const char **string) { - JSON_Value *value = NULL; - size_t new_string_len = 0; - char *new_string = get_quoted_string(string, &new_string_len); - if (new_string == NULL) { - return NULL; - } - value = json_value_init_string_no_copy(new_string, new_string_len); - if (value == NULL) { - parson_free(new_string); - return NULL; - } - return value; -} - -static JSON_Value * parse_boolean_value(const char **string) { - size_t true_token_size = SIZEOF_TOKEN("true"); - size_t false_token_size = SIZEOF_TOKEN("false"); - if (strncmp("true", *string, true_token_size) == 0) { - *string += true_token_size; - return json_value_init_boolean(1); - } else if (strncmp("false", *string, false_token_size) == 0) { - *string += false_token_size; - return json_value_init_boolean(0); - } - return NULL; -} - -static JSON_Value * parse_number_value(const char **string) { - char *end; - double number = 0; - errno = 0; - number = strtod(*string, &end); - if (errno == ERANGE && (number <= -HUGE_VAL || number >= HUGE_VAL)) { - return NULL; - } - if ((errno && errno != ERANGE) || !is_decimal(*string, end - *string)) { - return NULL; - } - *string = end; - return json_value_init_number(number); -} - -static JSON_Value * parse_null_value(const char **string) { - size_t token_size = SIZEOF_TOKEN("null"); - if (strncmp("null", *string, token_size) == 0) { - *string += token_size; - return json_value_init_null(); - } - return NULL; -} - -/* Serialization */ - -/* APPEND_STRING() is only called on string literals. - It's a bit hacky because it makes plenty of assumptions about the external state - and should eventually be tidied up into a function (same goes for APPEND_INDENT) - */ -#define APPEND_STRING(str) do {\ - written = SIZEOF_TOKEN((str));\ - if (buf != NULL) {\ - memcpy(buf, (str), written);\ - buf[written] = '\0';\ - buf += written;\ - }\ - written_total += written;\ - } while (0) - -#define APPEND_INDENT(level) do {\ - int level_i = 0;\ - for (level_i = 0; level_i < (level); level_i++) {\ - APPEND_STRING(PARSON_INDENT_STR);\ - }\ - } while (0) - -static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, parson_bool_t is_pretty, char *num_buf) -{ - const char *key = NULL, *string = NULL; - JSON_Value *temp_value = NULL; - JSON_Array *array = NULL; - JSON_Object *object = NULL; - size_t i = 0, count = 0; - double num = 0.0; - int written = -1, written_total = 0; - size_t len = 0; - - switch (json_value_get_type(value)) { - case JSONArray: - array = json_value_get_array(value); - count = json_array_get_count(array); - APPEND_STRING("["); - if (count > 0 && is_pretty) { - APPEND_STRING("\n"); - } - for (i = 0; i < count; i++) { - if (is_pretty) { - APPEND_INDENT(level+1); - } - temp_value = json_array_get_value(array, i); - written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf); - if (written < 0) { - return -1; - } - if (buf != NULL) { - buf += written; - } - written_total += written; - if (i < (count - 1)) { - APPEND_STRING(","); - } - if (is_pretty) { - APPEND_STRING("\n"); - } - } - if (count > 0 && is_pretty) { - APPEND_INDENT(level); - } - APPEND_STRING("]"); - return written_total; - case JSONObject: - object = json_value_get_object(value); - count = json_object_get_count(object); - APPEND_STRING("{"); - if (count > 0 && is_pretty) { - APPEND_STRING("\n"); - } - for (i = 0; i < count; i++) { - key = json_object_get_name(object, i); - if (key == NULL) { - return -1; - } - if (is_pretty) { - APPEND_INDENT(level+1); - } - /* We do not support key names with embedded \0 chars */ - written = json_serialize_string(key, strlen(key), buf); - if (written < 0) { - return -1; - } - if (buf != NULL) { - buf += written; - } - written_total += written; - APPEND_STRING(":"); - if (is_pretty) { - APPEND_STRING(" "); - } - temp_value = json_object_get_value_at(object, i); - written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf); - if (written < 0) { - return -1; - } - if (buf != NULL) { - buf += written; - } - written_total += written; - if (i < (count - 1)) { - APPEND_STRING(","); - } - if (is_pretty) { - APPEND_STRING("\n"); - } - } - if (count > 0 && is_pretty) { - APPEND_INDENT(level); - } - APPEND_STRING("}"); - return written_total; - case JSONString: - string = json_value_get_string(value); - if (string == NULL) { - return -1; - } - len = json_value_get_string_len(value); - written = json_serialize_string(string, len, buf); - if (written < 0) { - return -1; - } - if (buf != NULL) { - buf += written; - } - written_total += written; - return written_total; - case JSONBoolean: - if (json_value_get_boolean(value)) { - APPEND_STRING("true"); - } else { - APPEND_STRING("false"); - } - return written_total; - case JSONNumber: - num = json_value_get_number(value); - if (buf != NULL) { - num_buf = buf; - } - if (parson_number_serialization_function) { - written = parson_number_serialization_function(num, num_buf); - } else { - const char *float_format = parson_float_format ? parson_float_format : PARSON_DEFAULT_FLOAT_FORMAT; - written = parson_sprintf(num_buf, float_format, num); - } - if (written < 0) { - return -1; - } - if (buf != NULL) { - buf += written; - } - written_total += written; - return written_total; - case JSONNull: - APPEND_STRING("null"); - return written_total; - case JSONError: - return -1; - default: - return -1; - } -} - -static int json_serialize_string(const char *string, size_t len, char *buf) { - size_t i = 0; - char c = '\0'; - int written = -1, written_total = 0; - APPEND_STRING("\""); - for (i = 0; i < len; i++) { - c = string[i]; - switch (c) { - case '\"': APPEND_STRING("\\\""); break; - case '\\': APPEND_STRING("\\\\"); break; - case '\b': APPEND_STRING("\\b"); break; - case '\f': APPEND_STRING("\\f"); break; - case '\n': APPEND_STRING("\\n"); break; - case '\r': APPEND_STRING("\\r"); break; - case '\t': APPEND_STRING("\\t"); break; - case '\x00': APPEND_STRING("\\u0000"); break; - case '\x01': APPEND_STRING("\\u0001"); break; - case '\x02': APPEND_STRING("\\u0002"); break; - case '\x03': APPEND_STRING("\\u0003"); break; - case '\x04': APPEND_STRING("\\u0004"); break; - case '\x05': APPEND_STRING("\\u0005"); break; - case '\x06': APPEND_STRING("\\u0006"); break; - case '\x07': APPEND_STRING("\\u0007"); break; - /* '\x08' duplicate: '\b' */ - /* '\x09' duplicate: '\t' */ - /* '\x0a' duplicate: '\n' */ - case '\x0b': APPEND_STRING("\\u000b"); break; - /* '\x0c' duplicate: '\f' */ - /* '\x0d' duplicate: '\r' */ - case '\x0e': APPEND_STRING("\\u000e"); break; - case '\x0f': APPEND_STRING("\\u000f"); break; - case '\x10': APPEND_STRING("\\u0010"); break; - case '\x11': APPEND_STRING("\\u0011"); break; - case '\x12': APPEND_STRING("\\u0012"); break; - case '\x13': APPEND_STRING("\\u0013"); break; - case '\x14': APPEND_STRING("\\u0014"); break; - case '\x15': APPEND_STRING("\\u0015"); break; - case '\x16': APPEND_STRING("\\u0016"); break; - case '\x17': APPEND_STRING("\\u0017"); break; - case '\x18': APPEND_STRING("\\u0018"); break; - case '\x19': APPEND_STRING("\\u0019"); break; - case '\x1a': APPEND_STRING("\\u001a"); break; - case '\x1b': APPEND_STRING("\\u001b"); break; - case '\x1c': APPEND_STRING("\\u001c"); break; - case '\x1d': APPEND_STRING("\\u001d"); break; - case '\x1e': APPEND_STRING("\\u001e"); break; - case '\x1f': APPEND_STRING("\\u001f"); break; - case '/': - if (parson_escape_slashes) { - APPEND_STRING("\\/"); /* to make json embeddable in xml\/html */ - } else { - APPEND_STRING("/"); - } - break; - default: - if (buf != NULL) { - buf[0] = c; - buf += 1; - } - written_total += 1; - break; - } - } - APPEND_STRING("\""); - return written_total; -} - -#undef APPEND_STRING -#undef APPEND_INDENT - -/* Parser API */ -JSON_Value * json_parse_file(const char *filename) { - char *file_contents = read_file(filename); - JSON_Value *output_value = NULL; - if (file_contents == NULL) { - return NULL; - } - output_value = json_parse_string(file_contents); - parson_free(file_contents); - return output_value; -} - -JSON_Value * json_parse_file_with_comments(const char *filename) { - char *file_contents = read_file(filename); - JSON_Value *output_value = NULL; - if (file_contents == NULL) { - return NULL; - } - output_value = json_parse_string_with_comments(file_contents); - parson_free(file_contents); - return output_value; -} - -JSON_Value * json_parse_string(const char *string) { - if (string == NULL) { - return NULL; - } - if (string[0] == '\xEF' && string[1] == '\xBB' && string[2] == '\xBF') { - string = string + 3; /* Support for UTF-8 BOM */ - } - return parse_value((const char**)&string, 0); -} - -JSON_Value * json_parse_string_with_comments(const char *string) { - JSON_Value *result = NULL; - char *string_mutable_copy = NULL, *string_mutable_copy_ptr = NULL; - string_mutable_copy = parson_strdup(string); - if (string_mutable_copy == NULL) { - return NULL; - } - remove_comments(string_mutable_copy, "/*", "*/"); - remove_comments(string_mutable_copy, "//", "\n"); - string_mutable_copy_ptr = string_mutable_copy; - result = parse_value((const char**)&string_mutable_copy_ptr, 0); - parson_free(string_mutable_copy); - return result; -} - -/* JSON Object API */ - -JSON_Value * json_object_get_value(const JSON_Object *object, const char *name) { - if (object == NULL || name == NULL) { - return NULL; - } - return json_object_getn_value(object, name, strlen(name)); -} - -const char * json_object_get_string(const JSON_Object *object, const char *name) { - return json_value_get_string(json_object_get_value(object, name)); -} - -size_t json_object_get_string_len(const JSON_Object *object, const char *name) { - return json_value_get_string_len(json_object_get_value(object, name)); -} - -double json_object_get_number(const JSON_Object *object, const char *name) { - return json_value_get_number(json_object_get_value(object, name)); -} - -JSON_Object * json_object_get_object(const JSON_Object *object, const char *name) { - return json_value_get_object(json_object_get_value(object, name)); -} - -JSON_Array * json_object_get_array(const JSON_Object *object, const char *name) { - return json_value_get_array(json_object_get_value(object, name)); -} - -int json_object_get_boolean(const JSON_Object *object, const char *name) { - return json_value_get_boolean(json_object_get_value(object, name)); -} - -JSON_Value * json_object_dotget_value(const JSON_Object *object, const char *name) { - const char *dot_position = strchr(name, '.'); - if (!dot_position) { - return json_object_get_value(object, name); - } - object = json_value_get_object(json_object_getn_value(object, name, dot_position - name)); - return json_object_dotget_value(object, dot_position + 1); -} - -const char * json_object_dotget_string(const JSON_Object *object, const char *name) { - return json_value_get_string(json_object_dotget_value(object, name)); -} - -size_t json_object_dotget_string_len(const JSON_Object *object, const char *name) { - return json_value_get_string_len(json_object_dotget_value(object, name)); -} - -double json_object_dotget_number(const JSON_Object *object, const char *name) { - return json_value_get_number(json_object_dotget_value(object, name)); -} - -JSON_Object * json_object_dotget_object(const JSON_Object *object, const char *name) { - return json_value_get_object(json_object_dotget_value(object, name)); -} - -JSON_Array * json_object_dotget_array(const JSON_Object *object, const char *name) { - return json_value_get_array(json_object_dotget_value(object, name)); -} - -int json_object_dotget_boolean(const JSON_Object *object, const char *name) { - return json_value_get_boolean(json_object_dotget_value(object, name)); -} - -size_t json_object_get_count(const JSON_Object *object) { - return object ? object->count : 0; -} - -const char * json_object_get_name(const JSON_Object *object, size_t index) { - if (object == NULL || index >= json_object_get_count(object)) { - return NULL; - } - return object->names[index]; -} - -JSON_Value * json_object_get_value_at(const JSON_Object *object, size_t index) { - if (object == NULL || index >= json_object_get_count(object)) { - return NULL; - } - return object->values[index]; -} - -JSON_Value *json_object_get_wrapping_value(const JSON_Object *object) { - if (!object) { - return NULL; - } - return object->wrapping_value; -} - -int json_object_has_value (const JSON_Object *object, const char *name) { - return json_object_get_value(object, name) != NULL; -} - -int json_object_has_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type) { - JSON_Value *val = json_object_get_value(object, name); - return val != NULL && json_value_get_type(val) == type; -} - -int json_object_dothas_value (const JSON_Object *object, const char *name) { - return json_object_dotget_value(object, name) != NULL; -} - -int json_object_dothas_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type) { - JSON_Value *val = json_object_dotget_value(object, name); - return val != NULL && json_value_get_type(val) == type; -} - -/* JSON Array API */ -JSON_Value * json_array_get_value(const JSON_Array *array, size_t index) { - if (array == NULL || index >= json_array_get_count(array)) { - return NULL; - } - return array->items[index]; -} - -const char * json_array_get_string(const JSON_Array *array, size_t index) { - return json_value_get_string(json_array_get_value(array, index)); -} - -size_t json_array_get_string_len(const JSON_Array *array, size_t index) { - return json_value_get_string_len(json_array_get_value(array, index)); -} - -double json_array_get_number(const JSON_Array *array, size_t index) { - return json_value_get_number(json_array_get_value(array, index)); -} - -JSON_Object * json_array_get_object(const JSON_Array *array, size_t index) { - return json_value_get_object(json_array_get_value(array, index)); -} - -JSON_Array * json_array_get_array(const JSON_Array *array, size_t index) { - return json_value_get_array(json_array_get_value(array, index)); -} - -int json_array_get_boolean(const JSON_Array *array, size_t index) { - return json_value_get_boolean(json_array_get_value(array, index)); -} - -size_t json_array_get_count(const JSON_Array *array) { - return array ? array->count : 0; -} - -JSON_Value * json_array_get_wrapping_value(const JSON_Array *array) { - if (!array) { - return NULL; - } - return array->wrapping_value; -} - -/* JSON Value API */ -JSON_Value_Type json_value_get_type(const JSON_Value *value) { - return value ? value->type : JSONError; -} - -JSON_Object * json_value_get_object(const JSON_Value *value) { - return json_value_get_type(value) == JSONObject ? value->value.object : NULL; -} - -JSON_Array * json_value_get_array(const JSON_Value *value) { - return json_value_get_type(value) == JSONArray ? value->value.array : NULL; -} - -static const JSON_String * json_value_get_string_desc(const JSON_Value *value) { - return json_value_get_type(value) == JSONString ? &value->value.string : NULL; -} - -const char * json_value_get_string(const JSON_Value *value) { - const JSON_String *str = json_value_get_string_desc(value); - return str ? str->chars : NULL; -} - -size_t json_value_get_string_len(const JSON_Value *value) { - const JSON_String *str = json_value_get_string_desc(value); - return str ? str->length : 0; -} - -double json_value_get_number(const JSON_Value *value) { - return json_value_get_type(value) == JSONNumber ? value->value.number : 0; -} - -int json_value_get_boolean(const JSON_Value *value) { - return json_value_get_type(value) == JSONBoolean ? value->value.boolean : -1; -} - -JSON_Value * json_value_get_parent (const JSON_Value *value) { - return value ? value->parent : NULL; -} - -void json_value_free(JSON_Value *value) { - switch (json_value_get_type(value)) { - case JSONObject: - json_object_free(value->value.object); - break; - case JSONString: - parson_free(value->value.string.chars); - break; - case JSONArray: - json_array_free(value->value.array); - break; - default: - break; - } - parson_free(value); -} - -JSON_Value * json_value_init_object(void) { - JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); - if (!new_value) { - return NULL; - } - new_value->parent = NULL; - new_value->type = JSONObject; - new_value->value.object = json_object_make(new_value); - if (!new_value->value.object) { - parson_free(new_value); - return NULL; - } - return new_value; -} - -JSON_Value * json_value_init_array(void) { - JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); - if (!new_value) { - return NULL; - } - new_value->parent = NULL; - new_value->type = JSONArray; - new_value->value.array = json_array_make(new_value); - if (!new_value->value.array) { - parson_free(new_value); - return NULL; - } - return new_value; -} - -JSON_Value * json_value_init_string(const char *string) { - if (string == NULL) { - return NULL; - } - return json_value_init_string_with_len(string, strlen(string)); -} - -JSON_Value * json_value_init_string_with_len(const char *string, size_t length) { - char *copy = NULL; - JSON_Value *value; - if (string == NULL) { - return NULL; - } - if (!is_valid_utf8(string, length)) { - return NULL; - } - copy = parson_strndup(string, length); - if (copy == NULL) { - return NULL; - } - value = json_value_init_string_no_copy(copy, length); - if (value == NULL) { - parson_free(copy); - } - return value; -} - -JSON_Value * json_value_init_number(double number) { - JSON_Value *new_value = NULL; - if (IS_NUMBER_INVALID(number)) { - return NULL; - } - new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); - if (new_value == NULL) { - return NULL; - } - new_value->parent = NULL; - new_value->type = JSONNumber; - new_value->value.number = number; - return new_value; -} - -JSON_Value * json_value_init_boolean(int boolean) { - JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); - if (!new_value) { - return NULL; - } - new_value->parent = NULL; - new_value->type = JSONBoolean; - new_value->value.boolean = boolean ? 1 : 0; - return new_value; -} - -JSON_Value * json_value_init_null(void) { - JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); - if (!new_value) { - return NULL; - } - new_value->parent = NULL; - new_value->type = JSONNull; - return new_value; -} - -JSON_Value * json_value_deep_copy(const JSON_Value *value) { - size_t i = 0; - JSON_Value *return_value = NULL, *temp_value_copy = NULL, *temp_value = NULL; - const JSON_String *temp_string = NULL; - const char *temp_key = NULL; - char *temp_string_copy = NULL; - JSON_Array *temp_array = NULL, *temp_array_copy = NULL; - JSON_Object *temp_object = NULL, *temp_object_copy = NULL; - JSON_Status res = JSONFailure; - char *key_copy = NULL; - - switch (json_value_get_type(value)) { - case JSONArray: - temp_array = json_value_get_array(value); - return_value = json_value_init_array(); - if (return_value == NULL) { - return NULL; - } - temp_array_copy = json_value_get_array(return_value); - for (i = 0; i < json_array_get_count(temp_array); i++) { - temp_value = json_array_get_value(temp_array, i); - temp_value_copy = json_value_deep_copy(temp_value); - if (temp_value_copy == NULL) { - json_value_free(return_value); - return NULL; - } - if (json_array_add(temp_array_copy, temp_value_copy) != JSONSuccess) { - json_value_free(return_value); - json_value_free(temp_value_copy); - return NULL; - } - } - return return_value; - case JSONObject: - temp_object = json_value_get_object(value); - return_value = json_value_init_object(); - if (!return_value) { - return NULL; - } - temp_object_copy = json_value_get_object(return_value); - for (i = 0; i < json_object_get_count(temp_object); i++) { - temp_key = json_object_get_name(temp_object, i); - temp_value = json_object_get_value(temp_object, temp_key); - temp_value_copy = json_value_deep_copy(temp_value); - if (!temp_value_copy) { - json_value_free(return_value); - return NULL; - } - key_copy = parson_strdup(temp_key); - if (!key_copy) { - json_value_free(temp_value_copy); - json_value_free(return_value); - return NULL; - } - res = json_object_add(temp_object_copy, key_copy, temp_value_copy); - if (res != JSONSuccess) { - parson_free(key_copy); - json_value_free(temp_value_copy); - json_value_free(return_value); - return NULL; - } - } - return return_value; - case JSONBoolean: - return json_value_init_boolean(json_value_get_boolean(value)); - case JSONNumber: - return json_value_init_number(json_value_get_number(value)); - case JSONString: - temp_string = json_value_get_string_desc(value); - if (temp_string == NULL) { - return NULL; - } - temp_string_copy = parson_strndup(temp_string->chars, temp_string->length); - if (temp_string_copy == NULL) { - return NULL; - } - return_value = json_value_init_string_no_copy(temp_string_copy, temp_string->length); - if (return_value == NULL) { - parson_free(temp_string_copy); - } - return return_value; - case JSONNull: - return json_value_init_null(); - case JSONError: - return NULL; - default: - return NULL; - } -} - -size_t json_serialization_size(const JSON_Value *value) { - char num_buf[PARSON_NUM_BUF_SIZE]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */ - int res = json_serialize_to_buffer_r(value, NULL, 0, PARSON_FALSE, num_buf); - return res < 0 ? 0 : (size_t)(res) + 1; -} - -JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) { - int written = -1; - size_t needed_size_in_bytes = json_serialization_size(value); - if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) { - return JSONFailure; - } - written = json_serialize_to_buffer_r(value, buf, 0, PARSON_FALSE, NULL); - if (written < 0) { - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename) { - JSON_Status return_code = JSONSuccess; - FILE *fp = NULL; - char *serialized_string = json_serialize_to_string(value); - if (serialized_string == NULL) { - return JSONFailure; - } - fp = fopen(filename, "w"); - if (fp == NULL) { - json_free_serialized_string(serialized_string); - return JSONFailure; - } - if (fputs(serialized_string, fp) == EOF) { - return_code = JSONFailure; - } - if (fclose(fp) == EOF) { - return_code = JSONFailure; - } - json_free_serialized_string(serialized_string); - return return_code; -} - -char * json_serialize_to_string(const JSON_Value *value) { - JSON_Status serialization_result = JSONFailure; - size_t buf_size_bytes = json_serialization_size(value); - char *buf = NULL; - if (buf_size_bytes == 0) { - return NULL; - } - buf = (char*)parson_malloc(buf_size_bytes); - if (buf == NULL) { - return NULL; - } - serialization_result = json_serialize_to_buffer(value, buf, buf_size_bytes); - if (serialization_result != JSONSuccess) { - json_free_serialized_string(buf); - return NULL; - } - return buf; -} - -size_t json_serialization_size_pretty(const JSON_Value *value) { - char num_buf[PARSON_NUM_BUF_SIZE]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */ - int res = json_serialize_to_buffer_r(value, NULL, 0, PARSON_TRUE, num_buf); - return res < 0 ? 0 : (size_t)(res) + 1; -} - -JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) { - int written = -1; - size_t needed_size_in_bytes = json_serialization_size_pretty(value); - if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) { - return JSONFailure; - } - written = json_serialize_to_buffer_r(value, buf, 0, PARSON_TRUE, NULL); - if (written < 0) { - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename) { - JSON_Status return_code = JSONSuccess; - FILE *fp = NULL; - char *serialized_string = json_serialize_to_string_pretty(value); - if (serialized_string == NULL) { - return JSONFailure; - } - fp = fopen(filename, "w"); - if (fp == NULL) { - json_free_serialized_string(serialized_string); - return JSONFailure; - } - if (fputs(serialized_string, fp) == EOF) { - return_code = JSONFailure; - } - if (fclose(fp) == EOF) { - return_code = JSONFailure; - } - json_free_serialized_string(serialized_string); - return return_code; -} - -char * json_serialize_to_string_pretty(const JSON_Value *value) { - JSON_Status serialization_result = JSONFailure; - size_t buf_size_bytes = json_serialization_size_pretty(value); - char *buf = NULL; - if (buf_size_bytes == 0) { - return NULL; - } - buf = (char*)parson_malloc(buf_size_bytes); - if (buf == NULL) { - return NULL; - } - serialization_result = json_serialize_to_buffer_pretty(value, buf, buf_size_bytes); - if (serialization_result != JSONSuccess) { - json_free_serialized_string(buf); - return NULL; - } - return buf; -} - -void json_free_serialized_string(char *string) { - parson_free(string); -} - -JSON_Status json_array_remove(JSON_Array *array, size_t ix) { - size_t to_move_bytes = 0; - if (array == NULL || ix >= json_array_get_count(array)) { - return JSONFailure; - } - json_value_free(json_array_get_value(array, ix)); - to_move_bytes = (json_array_get_count(array) - 1 - ix) * sizeof(JSON_Value*); - memmove(array->items + ix, array->items + ix + 1, to_move_bytes); - array->count -= 1; - return JSONSuccess; -} - -JSON_Status json_array_replace_value(JSON_Array *array, size_t ix, JSON_Value *value) { - if (array == NULL || value == NULL || value->parent != NULL || ix >= json_array_get_count(array)) { - return JSONFailure; - } - json_value_free(json_array_get_value(array, ix)); - value->parent = json_array_get_wrapping_value(array); - array->items[ix] = value; - return JSONSuccess; -} - -JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string) { - JSON_Value *value = json_value_init_string(string); - if (value == NULL) { - return JSONFailure; - } - if (json_array_replace_value(array, i, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_replace_string_with_len(JSON_Array *array, size_t i, const char *string, size_t len) { - JSON_Value *value = json_value_init_string_with_len(string, len); - if (value == NULL) { - return JSONFailure; - } - if (json_array_replace_value(array, i, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number) { - JSON_Value *value = json_value_init_number(number); - if (value == NULL) { - return JSONFailure; - } - if (json_array_replace_value(array, i, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean) { - JSON_Value *value = json_value_init_boolean(boolean); - if (value == NULL) { - return JSONFailure; - } - if (json_array_replace_value(array, i, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_replace_null(JSON_Array *array, size_t i) { - JSON_Value *value = json_value_init_null(); - if (value == NULL) { - return JSONFailure; - } - if (json_array_replace_value(array, i, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_clear(JSON_Array *array) { - size_t i = 0; - if (array == NULL) { - return JSONFailure; - } - for (i = 0; i < json_array_get_count(array); i++) { - json_value_free(json_array_get_value(array, i)); - } - array->count = 0; - return JSONSuccess; -} - -JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value) { - if (array == NULL || value == NULL || value->parent != NULL) { - return JSONFailure; - } - return json_array_add(array, value); -} - -JSON_Status json_array_append_string(JSON_Array *array, const char *string) { - JSON_Value *value = json_value_init_string(string); - if (value == NULL) { - return JSONFailure; - } - if (json_array_append_value(array, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_append_string_with_len(JSON_Array *array, const char *string, size_t len) { - JSON_Value *value = json_value_init_string_with_len(string, len); - if (value == NULL) { - return JSONFailure; - } - if (json_array_append_value(array, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_append_number(JSON_Array *array, double number) { - JSON_Value *value = json_value_init_number(number); - if (value == NULL) { - return JSONFailure; - } - if (json_array_append_value(array, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_append_boolean(JSON_Array *array, int boolean) { - JSON_Value *value = json_value_init_boolean(boolean); - if (value == NULL) { - return JSONFailure; - } - if (json_array_append_value(array, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_array_append_null(JSON_Array *array) { - JSON_Value *value = json_value_init_null(); - if (value == NULL) { - return JSONFailure; - } - if (json_array_append_value(array, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value) { - unsigned long hash = 0; - parson_bool_t found = PARSON_FALSE; - size_t cell_ix = 0; - size_t item_ix = 0; - JSON_Value *old_value = NULL; - char *key_copy = NULL; - - if (!object || !name || !value || value->parent) { - return JSONFailure; - } - hash = hash_string(name, strlen(name)); - found = PARSON_FALSE; - cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); - if (found) { - item_ix = object->cells[cell_ix]; - old_value = object->values[item_ix]; - json_value_free(old_value); - object->values[item_ix] = value; - value->parent = json_object_get_wrapping_value(object); - return JSONSuccess; - } - if (object->count >= object->item_capacity) { - JSON_Status res = json_object_grow_and_rehash(object); - if (res != JSONSuccess) { - return JSONFailure; - } - cell_ix = json_object_get_cell_ix(object, name, strlen(name), hash, &found); - } - key_copy = parson_strdup(name); - if (!key_copy) { - return JSONFailure; - } - object->names[object->count] = key_copy; - object->cells[cell_ix] = object->count; - object->values[object->count] = value; - object->cell_ixs[object->count] = cell_ix; - object->hashes[object->count] = hash; - object->count++; - value->parent = json_object_get_wrapping_value(object); - return JSONSuccess; -} - -JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string) { - JSON_Value *value = json_value_init_string(string); - JSON_Status status = json_object_set_value(object, name, value); - if (status != JSONSuccess) { - json_value_free(value); - } - return status; -} - -JSON_Status json_object_set_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len) { - JSON_Value *value = json_value_init_string_with_len(string, len); - JSON_Status status = json_object_set_value(object, name, value); - if (status != JSONSuccess) { - json_value_free(value); - } - return status; -} - -JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number) { - JSON_Value *value = json_value_init_number(number); - JSON_Status status = json_object_set_value(object, name, value); - if (status != JSONSuccess) { - json_value_free(value); - } - return status; -} - -JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean) { - JSON_Value *value = json_value_init_boolean(boolean); - JSON_Status status = json_object_set_value(object, name, value); - if (status != JSONSuccess) { - json_value_free(value); - } - return status; -} - -JSON_Status json_object_set_null(JSON_Object *object, const char *name) { - JSON_Value *value = json_value_init_null(); - JSON_Status status = json_object_set_value(object, name, value); - if (status != JSONSuccess) { - json_value_free(value); - } - return status; -} - -JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value) { - const char *dot_pos = NULL; - JSON_Value *temp_value = NULL, *new_value = NULL; - JSON_Object *temp_object = NULL, *new_object = NULL; - JSON_Status status = JSONFailure; - size_t name_len = 0; - char *name_copy = NULL; - - if (object == NULL || name == NULL || value == NULL) { - return JSONFailure; - } - dot_pos = strchr(name, '.'); - if (dot_pos == NULL) { - return json_object_set_value(object, name, value); - } - name_len = dot_pos - name; - temp_value = json_object_getn_value(object, name, name_len); - if (temp_value) { - /* Don't overwrite existing non-object (unlike json_object_set_value, but it shouldn't be changed at this point) */ - if (json_value_get_type(temp_value) != JSONObject) { - return JSONFailure; - } - temp_object = json_value_get_object(temp_value); - return json_object_dotset_value(temp_object, dot_pos + 1, value); - } - new_value = json_value_init_object(); - if (new_value == NULL) { - return JSONFailure; - } - new_object = json_value_get_object(new_value); - status = json_object_dotset_value(new_object, dot_pos + 1, value); - if (status != JSONSuccess) { - json_value_free(new_value); - return JSONFailure; - } - name_copy = parson_strndup(name, name_len); - if (!name_copy) { - json_object_dotremove_internal(new_object, dot_pos + 1, 0); - json_value_free(new_value); - return JSONFailure; - } - status = json_object_add(object, name_copy, new_value); - if (status != JSONSuccess) { - parson_free(name_copy); - json_object_dotremove_internal(new_object, dot_pos + 1, 0); - json_value_free(new_value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string) { - JSON_Value *value = json_value_init_string(string); - if (value == NULL) { - return JSONFailure; - } - if (json_object_dotset_value(object, name, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_dotset_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len) { - JSON_Value *value = json_value_init_string_with_len(string, len); - if (value == NULL) { - return JSONFailure; - } - if (json_object_dotset_value(object, name, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number) { - JSON_Value *value = json_value_init_number(number); - if (value == NULL) { - return JSONFailure; - } - if (json_object_dotset_value(object, name, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean) { - JSON_Value *value = json_value_init_boolean(boolean); - if (value == NULL) { - return JSONFailure; - } - if (json_object_dotset_value(object, name, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_dotset_null(JSON_Object *object, const char *name) { - JSON_Value *value = json_value_init_null(); - if (value == NULL) { - return JSONFailure; - } - if (json_object_dotset_value(object, name, value) != JSONSuccess) { - json_value_free(value); - return JSONFailure; - } - return JSONSuccess; -} - -JSON_Status json_object_remove(JSON_Object *object, const char *name) { - return json_object_remove_internal(object, name, PARSON_TRUE); -} - -JSON_Status json_object_dotremove(JSON_Object *object, const char *name) { - return json_object_dotremove_internal(object, name, PARSON_TRUE); -} - -JSON_Status json_object_clear(JSON_Object *object) { - size_t i = 0; - if (object == NULL) { - return JSONFailure; - } - for (i = 0; i < json_object_get_count(object); i++) { - parson_free(object->names[i]); - object->names[i] = NULL; - - json_value_free(object->values[i]); - object->values[i] = NULL; - } - object->count = 0; - for (i = 0; i < object->cell_capacity; i++) { - object->cells[i] = OBJECT_INVALID_IX; - } - return JSONSuccess; -} - -JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value) { - JSON_Value *temp_schema_value = NULL, *temp_value = NULL; - JSON_Array *schema_array = NULL, *value_array = NULL; - JSON_Object *schema_object = NULL, *value_object = NULL; - JSON_Value_Type schema_type = JSONError, value_type = JSONError; - const char *key = NULL; - size_t i = 0, count = 0; - if (schema == NULL || value == NULL) { - return JSONFailure; - } - schema_type = json_value_get_type(schema); - value_type = json_value_get_type(value); - if (schema_type != value_type && schema_type != JSONNull) { /* null represents all values */ - return JSONFailure; - } - switch (schema_type) { - case JSONArray: - schema_array = json_value_get_array(schema); - value_array = json_value_get_array(value); - count = json_array_get_count(schema_array); - if (count == 0) { - return JSONSuccess; /* Empty array allows all types */ - } - /* Get first value from array, rest is ignored */ - temp_schema_value = json_array_get_value(schema_array, 0); - for (i = 0; i < json_array_get_count(value_array); i++) { - temp_value = json_array_get_value(value_array, i); - if (json_validate(temp_schema_value, temp_value) != JSONSuccess) { - return JSONFailure; - } - } - return JSONSuccess; - case JSONObject: - schema_object = json_value_get_object(schema); - value_object = json_value_get_object(value); - count = json_object_get_count(schema_object); - if (count == 0) { - return JSONSuccess; /* Empty object allows all objects */ - } else if (json_object_get_count(value_object) < count) { - return JSONFailure; /* Tested object mustn't have less name-value pairs than schema */ - } - for (i = 0; i < count; i++) { - key = json_object_get_name(schema_object, i); - temp_schema_value = json_object_get_value(schema_object, key); - temp_value = json_object_get_value(value_object, key); - if (temp_value == NULL) { - return JSONFailure; - } - if (json_validate(temp_schema_value, temp_value) != JSONSuccess) { - return JSONFailure; - } - } - return JSONSuccess; - case JSONString: case JSONNumber: case JSONBoolean: case JSONNull: - return JSONSuccess; /* equality already tested before switch */ - case JSONError: default: - return JSONFailure; - } -} - -int json_value_equals(const JSON_Value *a, const JSON_Value *b) { - JSON_Object *a_object = NULL, *b_object = NULL; - JSON_Array *a_array = NULL, *b_array = NULL; - const JSON_String *a_string = NULL, *b_string = NULL; - const char *key = NULL; - size_t a_count = 0, b_count = 0, i = 0; - JSON_Value_Type a_type, b_type; - a_type = json_value_get_type(a); - b_type = json_value_get_type(b); - if (a_type != b_type) { - return PARSON_FALSE; - } - switch (a_type) { - case JSONArray: - a_array = json_value_get_array(a); - b_array = json_value_get_array(b); - a_count = json_array_get_count(a_array); - b_count = json_array_get_count(b_array); - if (a_count != b_count) { - return PARSON_FALSE; - } - for (i = 0; i < a_count; i++) { - if (!json_value_equals(json_array_get_value(a_array, i), - json_array_get_value(b_array, i))) { - return PARSON_FALSE; - } - } - return PARSON_TRUE; - case JSONObject: - a_object = json_value_get_object(a); - b_object = json_value_get_object(b); - a_count = json_object_get_count(a_object); - b_count = json_object_get_count(b_object); - if (a_count != b_count) { - return PARSON_FALSE; - } - for (i = 0; i < a_count; i++) { - key = json_object_get_name(a_object, i); - if (!json_value_equals(json_object_get_value(a_object, key), - json_object_get_value(b_object, key))) { - return PARSON_FALSE; - } - } - return PARSON_TRUE; - case JSONString: - a_string = json_value_get_string_desc(a); - b_string = json_value_get_string_desc(b); - if (a_string == NULL || b_string == NULL) { - return PARSON_FALSE; /* shouldn't happen */ - } - return a_string->length == b_string->length && - memcmp(a_string->chars, b_string->chars, a_string->length) == 0; - case JSONBoolean: - return json_value_get_boolean(a) == json_value_get_boolean(b); - case JSONNumber: - return fabs(json_value_get_number(a) - json_value_get_number(b)) < 0.000001; /* EPSILON */ - case JSONError: - return PARSON_TRUE; - case JSONNull: - return PARSON_TRUE; - default: - return PARSON_TRUE; - } -} - -JSON_Value_Type json_type(const JSON_Value *value) { - return json_value_get_type(value); -} - -JSON_Object * json_object (const JSON_Value *value) { - return json_value_get_object(value); -} - -JSON_Array * json_array(const JSON_Value *value) { - return json_value_get_array(value); -} - -const char * json_string(const JSON_Value *value) { - return json_value_get_string(value); -} - -size_t json_string_len(const JSON_Value *value) { - return json_value_get_string_len(value); -} - -double json_number(const JSON_Value *value) { - return json_value_get_number(value); -} - -int json_boolean(const JSON_Value *value) { - return json_value_get_boolean(value); -} - -void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun) { - parson_malloc = malloc_fun; - parson_free = free_fun; -} - -void json_set_escape_slashes(int escape_slashes) { - parson_escape_slashes = escape_slashes; -} - -void json_set_float_serialization_format(const char *format) { - if (parson_float_format) { - parson_free(parson_float_format); - parson_float_format = NULL; - } - if (!format) { - parson_float_format = NULL; - return; - } - parson_float_format = parson_strdup(format); -} - -void json_set_number_serialization_function(JSON_Number_Serialization_Function func) { - parson_number_serialization_function = func; -} diff --git a/Lib/parson/parson.h b/Lib/parson/parson.h deleted file mode 100644 index 40be490..0000000 --- a/Lib/parson/parson.h +++ /dev/null @@ -1,274 +0,0 @@ -/* - SPDX-License-Identifier: MIT - - Parson 1.5.3 (https://github.com/kgabis/parson) - Copyright (c) 2012 - 2023 Krzysztof Gabis - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -#ifndef parson_parson_h -#define parson_parson_h - -#ifdef __cplusplus -extern "C" -{ -#endif -#if 0 -} /* unconfuse xcode */ -#endif - -#define PARSON_VERSION_MAJOR 1 -#define PARSON_VERSION_MINOR 5 -#define PARSON_VERSION_PATCH 3 - -#define PARSON_VERSION_STRING "1.5.3" - -#include /* size_t */ - -/* Types and enums */ -typedef struct json_object_t JSON_Object; -typedef struct json_array_t JSON_Array; -typedef struct json_value_t JSON_Value; - -enum json_value_type { - JSONError = -1, - JSONNull = 1, - JSONString = 2, - JSONNumber = 3, - JSONObject = 4, - JSONArray = 5, - JSONBoolean = 6 -}; -typedef int JSON_Value_Type; - -enum json_result_t { - JSONSuccess = 0, - JSONFailure = -1 -}; -typedef int JSON_Status; - -typedef void * (*JSON_Malloc_Function)(size_t); -typedef void (*JSON_Free_Function)(void *); - -/* A function used for serializing numbers (see json_set_number_serialization_function). - If 'buf' is null then it should return number of bytes that would've been written - (but not more than PARSON_NUM_BUF_SIZE). -*/ -typedef int (*JSON_Number_Serialization_Function)(double num, char *buf); - -/* Call only once, before calling any other function from parson API. If not called, malloc and free - from stdlib will be used for all allocations */ -void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun); - -/* Sets if slashes should be escaped or not when serializing JSON. By default slashes are escaped. - This function sets a global setting and is not thread safe. */ -void json_set_escape_slashes(int escape_slashes); - -/* Sets float format used for serialization of numbers. - Make sure it can't serialize to a string longer than PARSON_NUM_BUF_SIZE. - If format is null then the default format is used. */ -void json_set_float_serialization_format(const char *format); - -/* Sets a function that will be used for serialization of numbers. - If function is null then the default serialization function is used. */ -void json_set_number_serialization_function(JSON_Number_Serialization_Function fun); - -/* Parses first JSON value in a file, returns NULL in case of error */ -JSON_Value * json_parse_file(const char *filename); - -/* Parses first JSON value in a file and ignores comments (/ * * / and //), - returns NULL in case of error */ -JSON_Value * json_parse_file_with_comments(const char *filename); - -/* Parses first JSON value in a string, returns NULL in case of error */ -JSON_Value * json_parse_string(const char *string); - -/* Parses first JSON value in a string and ignores comments (/ * * / and //), - returns NULL in case of error */ -JSON_Value * json_parse_string_with_comments(const char *string); - -/* Serialization */ -size_t json_serialization_size(const JSON_Value *value); /* returns 0 on fail */ -JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes); -JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename); -char * json_serialize_to_string(const JSON_Value *value); - -/* Pretty serialization */ -size_t json_serialization_size_pretty(const JSON_Value *value); /* returns 0 on fail */ -JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes); -JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename); -char * json_serialize_to_string_pretty(const JSON_Value *value); - -void json_free_serialized_string(char *string); /* frees string from json_serialize_to_string and json_serialize_to_string_pretty */ - -/* Comparing */ -int json_value_equals(const JSON_Value *a, const JSON_Value *b); - -/* Validation - This is *NOT* JSON Schema. It validates json by checking if object have identically - named fields with matching types. - For example schema {"name":"", "age":0} will validate - {"name":"Joe", "age":25} and {"name":"Joe", "age":25, "gender":"m"}, - but not {"name":"Joe"} or {"name":"Joe", "age":"Cucumber"}. - In case of arrays, only first value in schema is checked against all values in tested array. - Empty objects ({}) validate all objects, empty arrays ([]) validate all arrays, - null validates values of every type. - */ -JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value); - -/* - * JSON Object - */ -JSON_Value * json_object_get_value (const JSON_Object *object, const char *name); -const char * json_object_get_string (const JSON_Object *object, const char *name); -size_t json_object_get_string_len(const JSON_Object *object, const char *name); /* doesn't account for last null character */ -JSON_Object * json_object_get_object (const JSON_Object *object, const char *name); -JSON_Array * json_object_get_array (const JSON_Object *object, const char *name); -double json_object_get_number (const JSON_Object *object, const char *name); /* returns 0 on fail */ -int json_object_get_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */ - -/* dotget functions enable addressing values with dot notation in nested objects, - just like in structs or c++/java/c# objects (e.g. objectA.objectB.value). - Because valid names in JSON can contain dots, some values may be inaccessible - this way. */ -JSON_Value * json_object_dotget_value (const JSON_Object *object, const char *name); -const char * json_object_dotget_string (const JSON_Object *object, const char *name); -size_t json_object_dotget_string_len(const JSON_Object *object, const char *name); /* doesn't account for last null character */ -JSON_Object * json_object_dotget_object (const JSON_Object *object, const char *name); -JSON_Array * json_object_dotget_array (const JSON_Object *object, const char *name); -double json_object_dotget_number (const JSON_Object *object, const char *name); /* returns 0 on fail */ -int json_object_dotget_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */ - -/* Functions to get available names */ -size_t json_object_get_count (const JSON_Object *object); -const char * json_object_get_name (const JSON_Object *object, size_t index); -JSON_Value * json_object_get_value_at(const JSON_Object *object, size_t index); -JSON_Value * json_object_get_wrapping_value(const JSON_Object *object); - -/* Functions to check if object has a value with a specific name. Returned value is 1 if object has - * a value and 0 if it doesn't. dothas functions behave exactly like dotget functions. */ -int json_object_has_value (const JSON_Object *object, const char *name); -int json_object_has_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type); - -int json_object_dothas_value (const JSON_Object *object, const char *name); -int json_object_dothas_value_of_type(const JSON_Object *object, const char *name, JSON_Value_Type type); - -/* Creates new name-value pair or frees and replaces old value with a new one. - * json_object_set_value does not copy passed value so it shouldn't be freed afterwards. */ -JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value); -JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string); -JSON_Status json_object_set_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len); /* length shouldn't include last null character */ -JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number); -JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean); -JSON_Status json_object_set_null(JSON_Object *object, const char *name); - -/* Works like dotget functions, but creates whole hierarchy if necessary. - * json_object_dotset_value does not copy passed value so it shouldn't be freed afterwards. */ -JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value); -JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string); -JSON_Status json_object_dotset_string_with_len(JSON_Object *object, const char *name, const char *string, size_t len); /* length shouldn't include last null character */ -JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number); -JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean); -JSON_Status json_object_dotset_null(JSON_Object *object, const char *name); - -/* Frees and removes name-value pair */ -JSON_Status json_object_remove(JSON_Object *object, const char *name); - -/* Works like dotget function, but removes name-value pair only on exact match. */ -JSON_Status json_object_dotremove(JSON_Object *object, const char *key); - -/* Removes all name-value pairs in object */ -JSON_Status json_object_clear(JSON_Object *object); - -/* - *JSON Array - */ -JSON_Value * json_array_get_value (const JSON_Array *array, size_t index); -const char * json_array_get_string (const JSON_Array *array, size_t index); -size_t json_array_get_string_len(const JSON_Array *array, size_t index); /* doesn't account for last null character */ -JSON_Object * json_array_get_object (const JSON_Array *array, size_t index); -JSON_Array * json_array_get_array (const JSON_Array *array, size_t index); -double json_array_get_number (const JSON_Array *array, size_t index); /* returns 0 on fail */ -int json_array_get_boolean(const JSON_Array *array, size_t index); /* returns -1 on fail */ -size_t json_array_get_count (const JSON_Array *array); -JSON_Value * json_array_get_wrapping_value(const JSON_Array *array); - -/* Frees and removes value at given index, does nothing and returns JSONFailure if index doesn't exist. - * Order of values in array may change during execution. */ -JSON_Status json_array_remove(JSON_Array *array, size_t i); - -/* Frees and removes from array value at given index and replaces it with given one. - * Does nothing and returns JSONFailure if index doesn't exist. - * json_array_replace_value does not copy passed value so it shouldn't be freed afterwards. */ -JSON_Status json_array_replace_value(JSON_Array *array, size_t i, JSON_Value *value); -JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string); -JSON_Status json_array_replace_string_with_len(JSON_Array *array, size_t i, const char *string, size_t len); /* length shouldn't include last null character */ -JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number); -JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean); -JSON_Status json_array_replace_null(JSON_Array *array, size_t i); - -/* Frees and removes all values from array */ -JSON_Status json_array_clear(JSON_Array *array); - -/* Appends new value at the end of array. - * json_array_append_value does not copy passed value so it shouldn't be freed afterwards. */ -JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value); -JSON_Status json_array_append_string(JSON_Array *array, const char *string); -JSON_Status json_array_append_string_with_len(JSON_Array *array, const char *string, size_t len); /* length shouldn't include last null character */ -JSON_Status json_array_append_number(JSON_Array *array, double number); -JSON_Status json_array_append_boolean(JSON_Array *array, int boolean); -JSON_Status json_array_append_null(JSON_Array *array); - -/* - *JSON Value - */ -JSON_Value * json_value_init_object (void); -JSON_Value * json_value_init_array (void); -JSON_Value * json_value_init_string (const char *string); /* copies passed string */ -JSON_Value * json_value_init_string_with_len(const char *string, size_t length); /* copies passed string, length shouldn't include last null character */ -JSON_Value * json_value_init_number (double number); -JSON_Value * json_value_init_boolean(int boolean); -JSON_Value * json_value_init_null (void); -JSON_Value * json_value_deep_copy (const JSON_Value *value); -void json_value_free (JSON_Value *value); - -JSON_Value_Type json_value_get_type (const JSON_Value *value); -JSON_Object * json_value_get_object (const JSON_Value *value); -JSON_Array * json_value_get_array (const JSON_Value *value); -const char * json_value_get_string (const JSON_Value *value); -size_t json_value_get_string_len(const JSON_Value *value); /* doesn't account for last null character */ -double json_value_get_number (const JSON_Value *value); -int json_value_get_boolean(const JSON_Value *value); -JSON_Value * json_value_get_parent (const JSON_Value *value); - -/* Same as above, but shorter */ -JSON_Value_Type json_type (const JSON_Value *value); -JSON_Object * json_object (const JSON_Value *value); -JSON_Array * json_array (const JSON_Value *value); -const char * json_string (const JSON_Value *value); -size_t json_string_len(const JSON_Value *value); /* doesn't account for last null character */ -double json_number (const JSON_Value *value); -int json_boolean(const JSON_Value *value); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/LiveTraffic.xcodeproj/project.pbxproj b/LiveTraffic.xcodeproj/project.pbxproj index b27da5a..4d57e7a 100755 --- a/LiveTraffic.xcodeproj/project.pbxproj +++ b/LiveTraffic.xcodeproj/project.pbxproj @@ -179,6 +179,7 @@ 25EF90052B497C7900D7C805 /* LTSynthetic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LTSynthetic.h; sourceTree = ""; }; 25EFC76B249567BF005DB0A6 /* IconsFontAwesome5.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IconsFontAwesome5.h; sourceTree = ""; }; 25EFC76C249567BF005DB0A6 /* fa-solid-900.inc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.pascal; path = "fa-solid-900.inc"; sourceTree = ""; }; + 25F108C22C49A79A0021EDD6 /* metaf */ = {isa = PBXFileReference; lastKnownFileType = folder; path = metaf; sourceTree = ""; }; 25F3471A24D2C7DE004574E7 /* parson.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = parson.h; sourceTree = ""; }; 25F3471B24D2C7DE004574E7 /* parson.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = parson.c; sourceTree = ""; }; 25F3471D24D2CB15004574E7 /* ACTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ACTable.h; sourceTree = ""; }; @@ -451,6 +452,7 @@ 25A095CA2203B0C500658AA8 /* Lib */ = { isa = PBXGroup; children = ( + 25F108C22C49A79A0021EDD6 /* metaf */, 25BF009E290DCBF400317CAE /* XPMP2.xcodeproj */, 258117B5291AF24D00D05428 /* FMOD_Logo.cpp */, 251284AC274D829500B0F51A /* base64 */, @@ -582,10 +584,11 @@ D607B16109A5563100699BC3 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; KnownAssetTags = ( New, ); - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1540; }; buildConfigurationList = D607B16209A5563100699BC3 /* Build configuration list for PBXProject "LiveTraffic" */; compatibilityVersion = "Xcode 12.0"; @@ -723,17 +726,19 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Developer ID Application: Birger Hoppe (W5UR4ZV2BP)"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 2.05; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = W5UR4ZV2BP; DSTROOT = "$(XPLANE11_ROOT)/Resources/plugins/$(PROJECT)"; DYLIB_COMPATIBILITY_VERSION = ""; DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; EXECUTABLE_EXTENSION = xpl; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -784,13 +789,14 @@ "Lib/ImGui/**", Lib/ImgWindow, Lib/Font, + Lib/metaf/include, "$(HEADER_SEARCH_PATHS)", ); LIBRARY_SEARCH_PATHS = Lib/fmod; - LIVETRAFFIC_VERSION_BETA = 0; + LIVETRAFFIC_VERSION_BETA = 1; LIVETRAFFIC_VER_MAJOR = 3; LIVETRAFFIC_VER_MINOR = 6; - LIVETRAFFIC_VER_PATCH = 0; + LIVETRAFFIC_VER_PATCH = 9; LLVM_LTO = NO; MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; @@ -831,15 +837,17 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Developer ID Application: Birger Hoppe (W5UR4ZV2BP)"; + CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 2.05; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = W5UR4ZV2BP; DSTROOT = "$(XPLANE11_ROOT)/Resources/plugins/$(PROJECT)"; DYLIB_COMPATIBILITY_VERSION = ""; DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; EXECUTABLE_EXTENSION = xpl; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -890,13 +898,14 @@ "Lib/ImGui/**", Lib/ImgWindow, Lib/Font, + Lib/metaf/include, "$(HEADER_SEARCH_PATHS)", ); LIBRARY_SEARCH_PATHS = Lib/fmod; - LIVETRAFFIC_VERSION_BETA = 0; + LIVETRAFFIC_VERSION_BETA = 1; LIVETRAFFIC_VER_MAJOR = 3; LIVETRAFFIC_VER_MINOR = 6; - LIVETRAFFIC_VER_PATCH = 0; + LIVETRAFFIC_VER_PATCH = 9; LLVM_LTO = YES; MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; @@ -911,6 +920,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = "${LIVETRAFFIC_VER_MAJOR}.${LIVETRAFFIC_VER_MINOR}.${LIVETRAFFIC_VER_PATCH}"; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_LOCATION = YES; @@ -962,6 +972,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = "${LIVETRAFFIC_VER_MAJOR}.${LIVETRAFFIC_VER_MINOR}.${LIVETRAFFIC_VER_PATCH}"; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_LOCATION = 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 33fe5d6..ee7d8fc 100644 --- a/LiveTraffic.xcodeproj/project.xcworkspace/xcuserdata/birger.xcuserdatad/xcdebugger/Expressions.xcexplist +++ b/LiveTraffic.xcodeproj/project.xcworkspace/xcuserdata/birger.xcuserdatad/xcdebugger/Expressions.xcexplist @@ -76,13 +76,13 @@ contextName = "Apt::FindEdgesForHeading(double, double, std::__1::vector<unsigned long, std::__1::allocator<unsigned long> >&, TaxiEdge::edgeTy) const:LTApt.cpp"> + value = "vecTaxiEdges[222]"> + value = "lst[1385]"> + value = "vecTaxiEdgesIdxHead[3321]"> @@ -129,10 +129,10 @@ contextName = "LTFlightData::AddDynData(LTFlightData::FDDynamicData const&, int, int, positionTy*):LTFlightData.cpp"> + value = "posDeque"> + value = "dynDataDeque"> @@ -258,6 +258,9 @@ + + @@ -268,10 +271,10 @@ value = "cull"> + value = "iter->second"> + value = "tcas"> @@ -476,7 +479,7 @@ contextName = "LTAircraft::CalcPPos():LTAircraft.cpp"> + value = "from"> @@ -488,7 +491,7 @@ value = "ppos"> + value = "turn"> @@ -586,10 +589,10 @@ value = "mapFd"> + value = "sizeof(bool)"> + value = "dataRefs"> @@ -1109,6 +1112,14 @@ + + + + + + @@ -1171,10 +1182,10 @@ value = "_startTime"> + value = "_targetTime"> + value = "_deltaDist"> diff --git a/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme b/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme index b275fe9..3e7e3ce 100644 --- a/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme +++ b/LiveTraffic.xcodeproj/xcuserdata/birger.xcuserdatad/xcschemes/LiveTraffic.xcscheme @@ -1,6 +1,6 @@ diff --git a/README.md b/README.md index 54c6554..dda4ce2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ LiveTraffic is based on a number of other great libraries and APIs, most notably - [X-Plane SDK](https://developer.x-plane.com/sdk/plugin-sdk-documents/) to integrate with X-Plane - [XPMP2](https://github.com/TwinFan/XPMP2) for CSL model processing - [CURL](https://curl.haxx.se/libcurl/) for network protocol support +- [FMOD](https://www.fmod.com/) Audio Engine: FMOD Core API by Firelight Technologies Pty Ltd. - [parson](https://github.com/kgabis/parson) as JSON parser +- [metaf](https://github.com/nnaumenko/metaf) as METAR parser - [libz/zlib](https://zlib.net) as compression library (used by CURL) - [ImGui](https://github.com/ocornut/imgui) for user interfaces - [ImgWindow](https://github.com/xsquawkbox/xsb_public) for integrating ImGui into X-Plane windows @@ -40,8 +42,8 @@ Thanks go to ## Build -Please note that LiveTraffic includes the XPMP2 lib as a GitHub submodule. -To properly build, you need to also checkout the XPMP2 submodule, +Please note that LiveTraffic includes XPMP2, parson, and metaf libs as a GitHub submodules. +To properly build, you need to also checkout the submodules, e.g. on the command line by doing ``` git clone --recurse-submodules https://github.com/TwinFan/LiveTraffic diff --git a/Src/ACInfoWnd.cpp b/Src/ACInfoWnd.cpp index 15e6f9a..eb2c943 100644 --- a/Src/ACInfoWnd.cpp +++ b/Src/ACInfoWnd.cpp @@ -445,7 +445,18 @@ void ACIWnd::buildInterface() if (bOpen) { CollSecClear(ACI_SB_SIMULATION); - buildRow("Simulated Time", dataRefs.GetSimTimeString().c_str(), true); + + std::string s; + // in case of historic RealTraffic data we adjust timestamp to the historic time + if (const RealTrafficConnection* pRTChn = dynamic_cast(pChannel)) + { + if (pRTChn->isHistoric()) + s = ts2string(dataRefs.GetSimTime() - pRTChn->GetTSAdjust()); + } + // otherwise show general simulated time + if (s.empty()) + s = dataRefs.GetSimTimeString(); + buildRow("Simulated Time", s.c_str(), true); // last received tracking data const double lstDat = pFD ? (pFD->GetYoungestTS() - ts) : -99999.9; diff --git a/Src/DataRefs.cpp b/Src/DataRefs.cpp index da46605..6410043 100644 --- a/Src/DataRefs.cpp +++ b/Src/DataRefs.cpp @@ -540,6 +540,9 @@ DataRefs::dataRefDefinitionT DATA_REFS_LT[CNT_DATAREFS_LT] = { {"livetraffic/cfg/contrail_max_alt", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true }, {"livetraffic/cfg/contrail_life_time", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true }, {"livetraffic/cfg/contrail_multiple", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true }, + {"livetraffic/cfg/weather_control", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true, true }, + {"livetraffic/cfg/weather_metar_agl", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true }, + {"livetraffic/cfg/weather_metar_dist", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true }, {"livetraffic/cfg/remote_support", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true }, {"livetraffic/cfg/external_camera_tool", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true, true }, {"livetraffic/cfg/last_check_new_ver", DataRefs::LTGetInt, DataRefs::LTSetCfgValue, GET_VAR, true }, @@ -548,6 +551,7 @@ DataRefs::dataRefDefinitionT DATA_REFS_LT[CNT_DATAREFS_LT] = { {"livetraffic/dbg/ac_filter", DataRefs::LTGetInt, DataRefs::LTSetDebugAcFilter, GET_VAR, false }, {"livetraffic/dbg/ac_pos", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true }, {"livetraffic/dbg/log_raw_fd", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, false }, + {"livetraffic/dbg/log_weather", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, false }, {"livetraffic/dbg/model_matching", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, true }, {"livetraffic/dbg/export_fd", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, false }, {"livetraffic/dbg/export_user_ac", DataRefs::LTGetInt, DataRefs::LTSetBool, GET_VAR, false }, @@ -632,6 +636,9 @@ void* DataRefs::getVarAddr (dataRefsLT dr) case DR_CFG_CONTRAIL_MAX_ALT: return &contrailAltMax_ft; case DR_CFG_CONTRAIL_LIFE_TIME: return &contrailLifeTime; case DR_CFG_CONTRAIL_MULTIPLE: return &contrailMulti; + case DR_CFG_WEATHER_CONTROL: return &weatherCtl; + case DR_CFG_WEATHER_MAX_METAR_AGL: return &weatherMaxMETARheight_ft; + case DR_CFG_WEATHER_MAX_METAR_DIST: return &weatherMaxMETARdist_nm; case DR_CFG_REMOTE_SUPPORT: return &remoteSupport; case DR_CFG_EXTERNAL_CAMERA: return &bUseExternalCamera; case DR_CFG_LAST_CHECK_NEW_VER: return &lastCheckNewVer; @@ -640,6 +647,7 @@ void* DataRefs::getVarAddr (dataRefsLT dr) case DR_DBG_AC_FILTER: return &uDebugAcFilter; case DR_DBG_AC_POS: return &bDebugAcPos; case DR_DBG_LOG_RAW_FD: return &bDebugLogRawFd; + case DR_DBG_LOG_WEATHER: return &bDebugWeather; case DR_DBG_MODEL_MATCHING: return &bDebugModelMatching; case DR_DBG_EXPORT_FD: return &bDebugExportFd; case DR_DBG_EXPORT_USER_AC: return &bDebugExportUserAc; @@ -731,9 +739,6 @@ static bool gbIgnoreItsMe = false; DataRefs::DataRefs ( logLevelTy initLogLevel ) : iLogLevel (initLogLevel), -#ifdef DEBUG -bDebugAcPos (true), -#endif MsgRect(0, 0, WIN_WIDTH, 0), SUIrect (0, 500, 690, 0), // (left=bottom=0 means: initially centered) ACIrect (0, 530, 320, 0), @@ -752,8 +757,10 @@ ILWrect (0, 400, 965, 0) for ( int& i: bChannel ) i = false; - // enable adsb.fi, OpenSky Master Data, OGN, and Synthetic by default + // enable all public/free channels by default: + // adsb.fi, OpenSky Tracking & Master Data, OGN, and Synthetic by default bChannel[DR_CHANNEL_ADSB_FI_ONLINE - DR_CHANNEL_FIRST] = true; + bChannel[DR_CHANNEL_OPEN_SKY_ONLINE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERDATA - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_SKY_AC_MASTERFILE - DR_CHANNEL_FIRST] = true; bChannel[DR_CHANNEL_OPEN_GLIDER_NET - DR_CHANNEL_FIRST] = true; @@ -1125,22 +1132,20 @@ void DataRefs::SetViewType(XPViewTypes vt) // return user's plane pos -positionTy DataRefs::GetUsersPlanePos(double& trueAirspeed_m, double& track ) const +positionTy DataRefs::GetUsersPlanePos(double* pTrueAirspeed_m, + double* pTrack, + double* pHeightAGL_m) const { - if (IsXPThread()) { - // running in XP's main thread we can just return the values - trueAirspeed_m = lastUsersTrueAirspeed; - track = lastUsersTrack; - return lastUsersPlanePos; - } else { - // in a worker thread, we need to have the lock, and copy before release - std::unique_lock lock(mutexDrUpdate); - positionTy ret = lastUsersPlanePos; - trueAirspeed_m = lastUsersTrueAirspeed; - track = lastUsersTrack; - lock.unlock(); - return ret; - } + // access guarded by a lock + std::lock_guard lock(mutexDrUpdate); + + // Copy the values + positionTy ret = lastUsersPlanePos; + if (pTrueAirspeed_m) *pTrueAirspeed_m = lastUsersTrueAirspeed; + if (pTrack) *pTrack = lastUsersTrack; + if (pHeightAGL_m) *pHeightAGL_m = lastUsersAGL_ft * M_per_FT; + + return ret; } void DataRefs::UpdateUsersPlanePos () @@ -1669,7 +1674,6 @@ 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 - const int oldFdSnapTaxiDist = fdSnapTaxiDist; const int oldRefreshInvtl = fdRefreshIntvl; const int oldLongRefreshIntvl = fdLongRefrIntvl; @@ -1703,7 +1707,7 @@ bool DataRefs::SetCfgValue (void* p, int val) if (contrailAltMax_ft < contrailAltMin_ft + 1000) contrailAltMax_ft = std::min(90000, contrailAltMin_ft + 1000); } - + // any configuration value invalid? if (labelColor < 0 || labelColor > 0xFFFFFF || #ifdef DEBUG @@ -1722,6 +1726,8 @@ bool DataRefs::SetCfgValue (void* p, int val) hideBelowAGL < 0 || hideBelowAGL > MDL_ALT_MAX || hideNearbyGnd < 0 || hideNearbyGnd > 500 || hideNearbyAir < 0 || hideNearbyAir > 5000 || + weatherMaxMETARheight_ft < 1000 || weatherMaxMETARheight_ft > 10000 || + weatherMaxMETARdist_nm < 5 || weatherMaxMETARdist_nm > 100 || rtListenPort < 1024 || rtListenPort > 65535 || rtTrafficPort < 1024 || rtTrafficPort > 65535 || rtWeatherPort < 1024 || rtWeatherPort > 65535 || @@ -1739,17 +1745,6 @@ 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(); - } - // If label draw distance changes we need to tell XPMP2 if (p == &labelMaxDist) XPMPSetAircraftLabelDist(float(labelMaxDist), bLabelVisibilityCUtOff); @@ -1769,6 +1764,21 @@ bool DataRefs::SetCfgValue (void* p, int val) } } + // If weather is... + if (p == &weatherCtl) { + switch (weatherCtl) { + case WC_INIT: + case WC_NONE: // ...switched off do so immediately + WeatherReset(); + break; + case WC_METAR_XP: // ...by METAR pass on the last METAR now + WeatherSet(lastWeatherMETAR, lastWeatherStationId); + break; + case WC_REAL_TRAFFIC: // ...by RealTraffic do nothing...RT will do + break; + } + } + // success LogCfgSetting(p, val); return true; @@ -2020,7 +2030,7 @@ bool DataRefs::LoadConfigFile() // which conversion to do with the (older) version of the config file? unsigned long cfgFileVer = 0; - enum cfgFileConvE { CFG_NO_CONV=0, CFG_V3, CFG_V31, CFG_V331, CFG_V342, CFG_V350, CFG_V360 } conv = CFG_NO_CONV; + enum cfgFileConvE { CFG_NO_CONV=0, CFG_V3, CFG_V31, CFG_V331, CFG_V342, CFG_V350 } conv = CFG_NO_CONV; // open a config file std::string sFileName (LTCalcFullPath(PATH_CONFIG_FILE)); @@ -2042,7 +2052,7 @@ bool DataRefs::LoadConfigFile() // first line is supposed to be the version, read entire line std::vector ln; std::string lnBuf; - if (!safeGetline(fIn, lnBuf)) { + if (!safeGetline(fIn, lnBuf) || lnBuf.empty()) { // this will trigger when the config file has size 0, // don't know why, but in some rare situations that does happen, // is actually the most often support question. @@ -2090,8 +2100,6 @@ bool DataRefs::LoadConfigFile() rtConnType = RT_CONN_APP; // Switch RealTraffic default to App as it was before conv = CFG_V350; } - if (cfgFileVer < 30600) - conv = CFG_V360; } } @@ -2163,11 +2171,6 @@ bool DataRefs::LoadConfigFile() // RealTraffic Sim Time Control: previous value 1 is re-purposed, switch instead to 2 if (*i == DATA_REFS_LT[DR_CFG_RT_SIM_TIME_CTRL] && sVal == "1") sVal = "2"; - [[fallthrough]]; - case CFG_V360: - // Disable OpenSky Network Online, simply too unreliable at the moment - if (*i == DATA_REFS_LT[DR_CHANNEL_OPEN_SKY_ONLINE]) - sVal = "0"; break; } @@ -2676,19 +2679,16 @@ bool DataRefs::ToggleLabelDraw() // constexpr float WEATHER_TRY_PERIOD = 120.0f; ///< [s] Don't _try_ to read weather more often than this -constexpr float WEATHER_UPD_PERIOD = 600.0f; ///< [s] Weather to be updated at leas this often -constexpr double WEATHER_UPD_DIST_M = 25.0 * M_per_NM; ///< [m] Weather to be updated if moved more than this far from last weather update position -constexpr float WEATHER_SEARCH_RADIUS_NM = 25; ///< [nm] Search for latest weather reports in this radius +constexpr float WEATHER_UPD_PERIOD = 600.0f; ///< [s] Weather to be updated at least this often // check if weather updated needed, then do -bool DataRefs::WeatherUpdate () +bool DataRefs::WeatherFetchMETAR () { // protected against updates from the weather thread std::lock_guard lock(mutexDrUpdate); - // Our current camera position - positionTy camPos = GetViewPos(); - camPos.LocalToWorld(); + // User's position + positionTy posUser = GetUsersPlanePos(); // So...do we need an update? if (// never try more often than TRY_PERIOD says to avoid flooding @@ -2696,22 +2696,22 @@ bool DataRefs::WeatherUpdate () ( // had no weather yet at all? std::isnan(lastWeatherPos.lat()) || // moved far away from last weather pos? - camPos.dist(lastWeatherPos) > WEATHER_UPD_DIST_M || + posUser.dist(lastWeatherPos) > double(GetWeatherMaxMetarDist_m())/2.0 || // enough time passed since last weather update? lastWeatherUpd + WEATHER_UPD_PERIOD < GetMiscNetwTime() )) { // Trigger a weather update; this is an asynch operation lastWeatherAttempt = GetMiscNetwTime(); - return ::WeatherUpdate(camPos, WEATHER_SEARCH_RADIUS_NM); // travel distances [m] doubles as weather search distance [nm] + return ::WeatherFetchUpdate(posUser, GetWeatherMaxMetarDist_nm()); } return false; } // Called by the asynch process spawned by ::WeatherUpdate to inform us of the weather -void DataRefs::SetWeather (float hPa, float lat, float lon, - const std::string& stationId, - const std::string& METAR) +float DataRefs::SetWeather (float hPa, float lat, float lon, + const std::string& stationId, + const std::string& METAR) { // protected against reads from the main thread std::lock_guard lock(mutexDrUpdate); @@ -2727,6 +2727,11 @@ void DataRefs::SetWeather (float hPa, float lat, float lon, lastWeatherStationId = GetNearestAirportId(lat, lon); } + // Let's see if we can quickly find the QNH from the metar, which we prefer + const float qnh = WeatherQNHfromMETAR(METAR); + if (!std::isnan(qnh)) + hPa = qnh; + // Did weather change? if (!dequal(lastWeatherHPA, hPa)) { LOG_MSG(logINFO, INFO_WEATHER_UPDATED, hPa, @@ -2734,9 +2739,15 @@ void DataRefs::SetWeather (float hPa, float lat, float lon, lastWeatherPos.lat(), lastWeatherPos.lon()); } - // Finally: Save the new pressure and potentially export it with the tracking data + // Save the new pressure and potentially export it with the tracking data lastWeatherHPA = hPa; LTFlightData::ExportLastWeather(); + + // If we are to set weather based on METAR, then this is it + if (dataRefs.GetWeatherControl() == WC_METAR_XP) + WeatherSet(lastWeatherMETAR, lastWeatherStationId); + + return lastWeatherHPA; } // Thread-safely gets current weather info diff --git a/Src/InfoListWnd.cpp b/Src/InfoListWnd.cpp index 41b6f2d..58ca9f3 100644 --- a/Src/InfoListWnd.cpp +++ b/Src/InfoListWnd.cpp @@ -39,6 +39,7 @@ static const CreditTy CREDITS[] = { { "CURL", "for network protocol support", "https://curl.haxx.se/libcurl/" }, { "FMOD", "Audio Engine: FMOD Core API by Firelight Technologies Pty Ltd.", "https://www.fmod.com/"}, { "parson", "as JSON parser", "https://github.com/kgabis/parson" }, + { "metaf", "for parsing METARs", "https://github.com/nnaumenko/metaf" }, { "libz/zlib", "as compression library (used by CURL)", "https://zlib.net/" }, { "ImGui", "for user interfaces", "https://github.com/ocornut/imgui" }, { "ImgWindow", "for integrating ImGui into X-Plane windows", "https://github.com/xsquawkbox/xsb_public" }, @@ -322,8 +323,8 @@ void InfoListWnd::buildInterface() if (ImGui::TreeNodeEx("Aircraft / Channel Status", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginTable("StatusInfo", 2, ImGuiTableFlags_SizingPolicyFixedX)) { - - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed); + static float xCol1 = ImGui::CalcTextSize("Number of available CSL Models").x; + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, xCol1); ImGui::TableSetupColumn("Info", ImGuiTableColumnFlags_WidthStretch); // Are we active at all? @@ -372,6 +373,19 @@ void InfoListWnd::buildInterface() } // Weather + ImGui::TableNextRow(); + if (ImGui::TableSetColumnIndex(0)) ImGui::TextUnformatted("Weather Source"); + if (ImGui::TableSetColumnIndex(1)) ImGui::TextUnformatted(WeatherGetSource().c_str()); + + // If generated weather's METAR deviates from live weather, then display the generation source, too + const std::string& MetarForWeatherGeneration = WeatherGetMETAR(); + if (!MetarForWeatherGeneration.empty() && + MetarForWeatherGeneration != weatherMETAR) { + ImGui::TableNextRow(); + if (ImGui::TableSetColumnIndex(0)) ImGui::TextUnformatted("Weather METAR"); + if (ImGui::TableSetColumnIndex(1)) ImGui::TextUnformatted(MetarForWeatherGeneration.c_str()); + } + ImGui::TableNextRow(); if (ImGui::TableSetColumnIndex(0)) ImGui::TextUnformatted("Live Weather"); if (ImGui::TableSetColumnIndex(1)) { @@ -444,7 +458,14 @@ void InfoListWnd::buildInterface() else { ImGui::TableNextRow(); if (ImGui::TableSetColumnIndex(0)) ImGui::TextUnformatted(ICON_FA_EXCLAMATION_TRIANGLE " " LIVE_TRAFFIC " is"); - if (ImGui::TableSetColumnIndex(1)) ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "INACTIVE"); + if (ImGui::TableSetColumnIndex(1)) { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "INACTIVE"); + if (ImGui::Button(ICON_FA_POWER_OFF " Start Showing Aircraft")) { + ImGuiContext* pCtxt = ImGui::GetCurrentContext(); + dataRefs.SetAircraftDisplayed(true); + ImGui::SetCurrentContext(pCtxt); + } + } // Additional warning if there's no CSL model if (numCSLModels == 0) { diff --git a/Src/LTAircraft.cpp b/Src/LTAircraft.cpp index 1d4a363..c63fba7 100644 --- a/Src/LTAircraft.cpp +++ b/Src/LTAircraft.cpp @@ -2563,8 +2563,7 @@ bool LTAircraft::CalcVisible () const int hideDist = dataRefs.GetHideNearby(IsOnGrnd()); if (hideDist > 0) { // We need the distance to the user's aircraft - double d1, d2; - const positionTy userPos = dataRefs.GetUsersPlanePos(d1, d2); + const positionTy userPos = dataRefs.GetUsersPlanePos(); const double dist = ppos.dist(userPos); XPMP2::Aircraft::SetVisible(dist > double(hideDist)); } @@ -2599,8 +2598,8 @@ void LTAircraft::CalcAIPrio () } // user's plane's position and bearing from user's plane to this aircraft - double userSpeed, userTrack; - positionTy posUser = dataRefs.GetUsersPlanePos(userSpeed, userTrack); + double userTrack = 0.0; + positionTy posUser = dataRefs.GetUsersPlanePos(nullptr, &userTrack); if (posUser.IsOnGnd()) // if on the ground userTrack = posUser.heading(); // heading is more reliable const double bearing = posUser.angle(ppos); diff --git a/Src/LTApt.cpp b/Src/LTApt.cpp index afe92f7..7ee885f 100644 --- a/Src/LTApt.cpp +++ b/Src/LTApt.cpp @@ -2511,9 +2511,12 @@ static std::future futRefreshing; /// Last position for which airports have been read static positionTy lastCameraPos; -/// New airports added, so that a call to LTAptUpdateRwyAltitude(9 is necessary? +/// New airports added, so that a call to LTAptUpdateRwyAltitude() is necessary? static bool bAptsAdded = false; - + +/// Is data available? +static bool bAptAvailable = false; + // Start reading apt.dat file(s) bool LTAptEnable () { @@ -2535,25 +2538,21 @@ void LTAptUpdateRwyAltitudes () } // Update the airport data with airports around current camera position -void LTAptRefresh () +bool 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() && // but status is not yet ready? futRefreshing.wait_for(std::chrono::seconds(0)) != std::future_status::ready) // then stop here - return; + return false; // Distance since last read not far enough? // Must have travelled at least as far as standard search radius for planes: const positionTy camera = DataRefs::GetViewPos(); if (!camera.isNormal(true)) // have no good camery position (yet) - return; + return false; double radius = dataRefs.GetFdStdDistance_m(); if (lastCameraPos.dist(camera) < radius) // is false if lastCameraPos is NAN @@ -2562,9 +2561,11 @@ void LTAptRefresh () // But do we need to check for rwy altitudes after last scan of apt.dat file? if (bAptsAdded) { LTAptUpdateRwyAltitudes(); + bAptsAdded = false; + bAptAvailable = true; // now airport data is reliably available + return true; // fresh data available } - bAptsAdded = false; - return; + return false; } else lastCameraPos = camera; @@ -2575,10 +2576,18 @@ void LTAptRefresh () LOG_MSG(logINFO, "Starting thread to read apt.dat for airports %.1fnm around %s", radius / M_per_NM, std::string(lastCameraPos).c_str()); bStopThread = false; + bAptAvailable = false; futRefreshing = std::async(std::launch::async, AsyncReadApt, lastCameraPos, radius); // need to check for rwy altitudes soon! bAptsAdded = true; + return false; +} + +// Has LTAptRefresh finished, ie. do we have airport data? +bool LTAptAvailable () +{ + return bAptAvailable; } // Return the best possible runway to auto-land at diff --git a/Src/LTChannel.cpp b/Src/LTChannel.cpp index 8c78d21..0cc7634 100644 --- a/Src/LTChannel.cpp +++ b/Src/LTChannel.cpp @@ -127,6 +127,18 @@ double jag_n_nan (const JSON_Array *array, size_t idx) return NAN; } +// return an entire JSON array as float vector +std::vector jag_f_vector (const JSON_Array* array) +{ + std::vector v; + const size_t n = json_array_get_count(array); + for (size_t idx = 0; idx < n; ++idx) { + JSON_Value* pJSONVal = NULL; + v.push_back(jag_is_null(array, idx, &pJSONVal) ? NAN : float(json_value_get_number (pJSONVal))); + } + return v; +} + // Find first non-Null value in several JSON array fields JSON_Value* jag_FindFirstNonNull(const JSON_Array* pArr, std::initializer_list aIdx) { @@ -890,7 +902,7 @@ void LTFlightDataStop() { /// @see https://github.com/Homebrew/homebrew-core/issues/158759#issuecomment-1874091015 /// To be able to reload plugins we don't properly call global cleanup -#if not APL +#if APL==0 // cleanup global CURL stuff curl_global_cleanup(); #endif diff --git a/Src/LTFlightData.cpp b/Src/LTFlightData.cpp index 78ae974..92087f5 100644 --- a/Src/LTFlightData.cpp +++ b/Src/LTFlightData.cpp @@ -2197,7 +2197,7 @@ void LTFlightData::AddDynData (const FDDynamicData& inDyn, // new position must be significantly _after_ current 'to' pos // so that current channel _really_ had its chance to sent an update: - const double tsCutOff = pAc->GetToPos().ts() + dataRefs.GetFdRefreshIntvl()*3/2; + const double tsCutOff = GetYoungestTS() + dataRefs.GetFdRefreshIntvl()*3/2; if (inDyn.ts < tsCutOff) return; } diff --git a/Src/LTForeFlight.cpp b/Src/LTForeFlight.cpp index a7405a9..220be99 100644 --- a/Src/LTForeFlight.cpp +++ b/Src/LTForeFlight.cpp @@ -207,7 +207,7 @@ void ForeFlightSender::Main () // time for GPS? if (now >= nextGPS) { - pos = dataRefs.GetUsersPlanePos(airSpeed_m, track); + pos = dataRefs.GetUsersPlanePos(&airSpeed_m, &track); SendGPS(pos, airSpeed_m, track); nextGPS = now + FF_INTVL_GPS; bDidSendSomething = true; @@ -217,7 +217,7 @@ void ForeFlightSender::Main () if (now >= nextAtt) { if (!pos.isNormal()) - pos = dataRefs.GetUsersPlanePos(airSpeed_m, track); + pos = dataRefs.GetUsersPlanePos(&airSpeed_m, &track); SendAtt(pos, airSpeed_m, track); nextAtt = now + FF_INTVL_ATT; bDidSendSomething = true; diff --git a/Src/LTImgWindow.cpp b/Src/LTImgWindow.cpp index 8b1e8c7..c989e81 100644 --- a/Src/LTImgWindow.cpp +++ b/Src/LTImgWindow.cpp @@ -401,7 +401,7 @@ IMGUI_API bool FilteredInputText(const char* label, const char* filter, // Next cell: Draw the checkbox with a value linked to the dataRef PushID(label); if (between(width, -0.1f, 0.1f)) // with == 0.0f - width = GetContentRegionAvail().x - GetWidthIconBtn(); + width = GetContentRegionAvail().x - 3*GetWidthIconBtn(); SetNextItemWidth(width); if (hint) InputTextWithHint("", hint, &s, flags); @@ -946,6 +946,7 @@ bool LTImgWindowInit () ICON_FA_FOLDER_OPEN ICON_FA_INFO_CIRCLE ICON_FA_LEVEL_UP_ALT + ICON_FA_POWER_OFF ICON_FA_PLANE ICON_FA_QUESTION_CIRCLE ICON_FA_ROUTE diff --git a/Src/LTMain.cpp b/Src/LTMain.cpp index add3afb..8e78a1a 100644 --- a/Src/LTMain.cpp +++ b/Src/LTMain.cpp @@ -760,7 +760,7 @@ std::string GetNearestAirportId (const positionTy& _pos, if (outApPos) { outApPos->lat() = lat; outApPos->lon() = lon; - outApPos->SetAltFt((double)alt); + outApPos->alt_m() = alt; } } @@ -768,6 +768,33 @@ std::string GetNearestAirportId (const positionTy& _pos, return airportId; } +// Fetch specific airport location/altitude +/// @note Can't use `XPLMFindNavAid` because XP only searches for _parts_ +/// of the ID, so searching for "EDDL" effectively finds "XEDDL" +/// and returns the wrong position. +/// Instead, we iterate all airports and do an _exact_ comparison of the ID. +positionTy GetAirportLoc (const std::string sICAO) +{ + char sId[32]; + float lat=NAN, lon=NAN, alt=NAN; + + // Loop all airorts + for (XPLMNavRef navRef = XPLMFindFirstNavAidOfType(xplm_Nav_Airport); + navRef != XPLM_NAV_NOT_FOUND; + navRef = XPLMGetNextNavAid(navRef)) + { + // Get info and check if this is the one + XPLMGetNavAidInfo(navRef, nullptr, + &lat, &lon, &alt, + nullptr, nullptr, sId, nullptr, nullptr); + if (sICAO == sId) + return positionTy(lat, lon, alt); + } + // not found + return positionTy(); +} + + // Convert ADS-B Emitter Category to text const char* GetADSBEmitterCat (const std::string& cat) { @@ -827,6 +854,37 @@ bool dequal ( const double d1, const double d2 ) ((d1 + epsilon) > d2); } +// Find an interpolated value +float interpolate (const std::vector& scale, + const std::vector& values, + float pos_in_scale) +{ + LOG_ASSERT(!scale.empty()); + LOG_ASSERT(scale.size() == values.size()); + + // Border Cases + if (values.size() == 1) return values.front(); + if (pos_in_scale <= scale.front()) return values.front(); + if (pos_in_scale >= scale.back()) return values.back(); + + // We now know that `pos_in_scale` is between front and back + // Search for pos_in_scale in `scale`, find where it would fit inbetween + // (as border cases are covered above must find something) + const auto iter = std::adjacent_find(scale.begin(), scale.end(), + [pos_in_scale](const float& a, const float& b) + { return a <= pos_in_scale && pos_in_scale <= b; }); + LOG_ASSERT(iter != scale.end()); + + // 'left' index and weight for 'left' value + const size_t idx = (size_t)std::distance(scale.begin(), iter); + const float weight = float(1) - (pos_in_scale - *iter)/(*(iter+1) - *iter); + + return // interpolate between values of those positions we found + values[idx] * weight + // 'left'-hand part + values[idx+1] * (float(1) - weight); // 'right'-hand part +} + + // // MARK: Thread Handling // @@ -963,9 +1021,21 @@ float LoopCBAircraftMaintenance (float inElapsedSinceLastCall, float, int, void* // LiveTraffic Top Level Exception handling: catch all, reinit if something happens try { // Potentially refresh weather information - dataRefs.WeatherUpdate(); + dataRefs.WeatherFetchMETAR(); + // Update the weather (short-cuts if nothing to do) + WeatherUpdate(); + // Refresh airport data from apt.dat (in case camera moved far) - LTAptRefresh(); + if (LTAptRefresh()) { // fresh airport data available? + // If we are configured to keep parked aircraft, then we can ask RT to give us some + if (dataRefs.ShallKeepParkedAircraft()) { + // Trigger RealTraffic to refresh parked aircraft + RealTrafficConnection* pRTConn = + dynamic_cast(LTFlightDataGetCh(DR_CHANNEL_REAL_TRAFFIC_ONLINE)); + if (pRTConn) + pRTConn->DoReadParkedTraffic(); + } + } // maintenance (add/remove) LTFlightDataAcMaintenance(); // updates to menu item status @@ -991,23 +1061,13 @@ float LoopCBAircraftMaintenance (float inElapsedSinceLastCall, float, int, void* int MPIntPrefsFunc (const char*, const char* key, int iDefault) { // debug XPMP's CSL model matching if requested - if (!strcmp(key, XPMP_CFG_ITM_MODELMATCHING)) { - if constexpr (LIVETRAFFIC_VERSION_BETA) // force logging of model-matching in BETA versions - return true; - else - return dataRefs.GetDebugModelMatching(); - } + if (!strcmp(key, XPMP_CFG_ITM_MODELMATCHING)) return dataRefs.GetDebugModelMatching(); // logging level to match ours - if (!strcmp(key, XPMP_CFG_ITM_LOGLEVEL)) { - if constexpr (LIVETRAFFIC_VERSION_BETA) // force DEBUG-level logging in BETA versions - return logDEBUG; - else - return dataRefs.GetLogLevel(); - } + if (!strcmp(key, XPMP_CFG_ITM_LOGLEVEL)) return dataRefs.GetLogLevel(); // We don't want clamping to the ground, we take care of the ground ourselves - if (!strcmp(key, XPMP_CFG_ITM_CLAMPALL)) return 0; + if (!strcmp(key, XPMP_CFG_ITM_CLAMPALL)) return 0; // We want XPMP2 to assign unique modeS_ids if we feed duplicates (which can happen due to different id systems in use, especially ICAO vs FLARM) - if (!strcmp(key, XPMP_CFG_ITM_HANDLE_DUP_ID)) return 1; + if (!strcmp(key, XPMP_CFG_ITM_HANDLE_DUP_ID)) return 1; // Copying .obj files is an advanced setting if (!strcmp(key, XPMP_CFG_ITM_REPLDATAREFS) || !strcmp(key, XPMP_CFG_ITM_REPLTEXTURE)) diff --git a/Src/LTRealTraffic.cpp b/Src/LTRealTraffic.cpp index fd96f03..59f9306 100644 --- a/Src/LTRealTraffic.cpp +++ b/Src/LTRealTraffic.cpp @@ -34,16 +34,22 @@ #include #endif +// RealTraffic's atmospheric layers in meters +const std::vector RT_ATMOS_LAYERS = { + 111, 323, 762, 988, 1457, 1948, 2465, 3011, 3589, 4205, + 4863, 5572, 6341, 7182, 8114, 9160, 10359, 11770, 13503, 15790 +}; + + // // MARK: RealTraffic Connection // // Set all relevant values -void RealTrafficConnection::WxTy::set(double qnh, const CurrTy& o, bool bResetErr) +void RealTrafficConnection::WxTy::set(double qnh, long _tOff, bool bResetErr) { QNH = qnh; - pos = o.pos; - tOff = o.tOff; + tOff = _tOff; next = std::chrono::steady_clock::now() + RT_DRCT_WX_WAIT; if (bResetErr) nErr = 0; @@ -97,17 +103,20 @@ std::string RealTrafficConnection::GetStatusText () const // --- Direct Connection? --- if (eConnType == RT_CONN_REQU_REPL) { std::string s = - curr.eRequType == CurrTy::RT_REQU_AUTH ? "Authenticating..." : - curr.eRequType == CurrTy::RT_REQU_WEATHER ? "Fetching weather..." : + curr.eRequType == CurrTy::RT_REQU_AUTH ? "Authenticating..." : + curr.eRequType == CurrTy::RT_REQU_DEAUTH ? "De-authenticating..." : + curr.eRequType == CurrTy::RT_REQU_PARKED ? "Fetching parked aircraft..." : + curr.eRequType == CurrTy::RT_REQU_NEAREST_METAR ? "Fetching weather..." : + curr.eRequType == CurrTy::RT_REQU_WEATHER ? "Fetching weather..." : LTChannel::GetStatusText(); - if (tsAdjust > 1.0) { // historic data? + if (isHistoric()) { // historic data? snprintf(sIntvl, sizeof(sIntvl), MSG_RT_ADJUST, GetAdjustTSText().c_str()); s += sIntvl; } if (lTotalFlights == 0) { // RealTraffic has no data at all??? s += " | RealTraffic has no traffic at all! "; - s += (curr.tOff > 0 ? "Maybe requested historic data too far in the past?" : "(full_count=0)"); + s += (isHistoric() ? "Maybe requested historic data too far in the past?" : "(full_count=0)"); } return s; } @@ -188,32 +197,40 @@ void RealTrafficConnection::MainDirect () rtWx.QNH = NAN; rtWx.nErr = 0; lTotalFlights = -1; + // can right away read parked traffic if parked aircraft enabled and airport data is already available, otherwise we'll be triggered later when airport data has been processed + bDoParkedTraffic = dataRefs.ShallKeepParkedAircraft() && LTAptAvailable(); + // If we could theoretically set weather we prepare the interpolation settings + if (WeatherCanSet()) { + rtWx.interp = LTWeather::ComputeInterpol(RT_ATMOS_LAYERS, + rtWx.w.atmosphere_alt_levels_m); + } while ( shallRun() ) { // LiveTraffic Top Level Exception Handling try { // where are we right now? const positionTy pos (dataRefs.GetViewPos()); - rrlWait = RT_DRCT_ERR_WAIT; // Standard is: retry in 5s // If the camera position is valid we can request data around it if (pos.isNormal()) { - // determine the type of request, fetch data and process it - SetRequType(pos); + // Fetch data and process it + curr.pos = pos; if (FetchAllData(pos) && ProcessFetchedData()) // reduce error count if processed successfully // as a chance to appear OK in the long run DecErrCnt(); + + // Determine next action and wait time + tNextWakeup = SetRequType(pos); } else { // Camera position is yet invalid, retry in a second - rrlWait = std::chrono::seconds(1); + tNextWakeup = std::chrono::steady_clock::now() + std::chrono::seconds(1); } // sleep for a bit or if woken up for termination // by condition variable trigger { - tNextWakeup = std::chrono::steady_clock::now() + rrlWait; std::unique_lock lk(FDThreadSynchMutex); FDThreadSynchCV.wait_until(lk, tNextWakeup, [this]{return !shallRun();}); @@ -227,10 +244,30 @@ void RealTrafficConnection::MainDirect () IncErrCnt(); } } + + // Close the session with RealTraffic + try { + const positionTy pos (dataRefs.GetViewPos()); + SetRequType(pos); + if (FetchAllData(pos) && ProcessFetchedData()) + // reduce error count if processed successfully + // as a chance to appear OK in the long run + DecErrCnt(); + } catch (const std::exception& e) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, e.what()); + IncErrCnt(); + } catch (...) { + LOG_MSG(logERR, ERR_TOP_LEVEL_EXCEPTION, "(unknown type)"); + IncErrCnt(); + } + + // Reset weather control (this assumes noone else can control weather and + // would need to change once any other source in LiveTraffic can do so) + WeatherReset(); } -// Which request do we need now? -void RealTrafficConnection::SetRequType (const positionTy& _pos) +// Which request do we need next and when? +std::chrono::time_point RealTrafficConnection::SetRequType (const positionTy& _pos) { // Position as passed in curr.pos = _pos; @@ -261,20 +298,46 @@ void RealTrafficConnection::SetRequType (const positionTy& _pos) break; } - if (curr.sGUID.empty()) // have no GUID? Need authentication + if (!shallRun()) { // end the session? + curr.eRequType = CurrTy::RT_REQU_DEAUTH; + return std::chrono::steady_clock::now(); + } + if (curr.sGUID.empty()) { // have no GUID? Need authentication curr.eRequType = CurrTy::RT_REQU_AUTH; - else if ((std::isnan(rtWx.QNH) || // no Weather, or wrong time offset, or outdated, or moved too far away? - std::labs(curr.tOff - rtWx.tOff) > 120 || - std::chrono::steady_clock::now() >= rtWx.next || - rtWx.pos.distRoughSqr(curr.pos) > (RT_DRCT_WX_DIST*RT_DRCT_WX_DIST))) - { + return std::chrono::steady_clock::now(); + } + if (curr.eRequType == CurrTy::RT_REQU_NEAREST_METAR) { // previous request was METAR location? curr.eRequType = CurrTy::RT_REQU_WEATHER; - if (std::labs(curr.tOff - rtWx.tOff) > 120) // if changing the timeoffset (request other historic data) then we must have new weather before proceeding + return tNextWeather; + } + const positionTy posUser = dataRefs.GetUsersPlanePos(); // Where is the user's plane just now= + if (rtWx.nErr < RT_DRCT_MAX_WX_ERR && // not yet seen too many weather request errors? _AND_ + (std::isnan(rtWx.QNH) || // no Weather, or wrong time offset, or outdated, or moved too far away? + std::labs(curr.tOff - rtWx.tOff) > 120 || + // too far? (we use half the max. METAR distance + rtWx.pos.distRoughSqr(posUser) > (sqr(dataRefs.GetWeatherMaxMetarDist_m()/2.0)) || + // or turned by at least 45 degrees? (Heading into a different direction can mean to select a different METAR) + std::abs(HeadingDiff(rtWx.pos.heading(), posUser.heading())) > 45.0)) + { + curr.eRequType = CurrTy::RT_REQU_NEAREST_METAR; + if (std::labs(curr.tOff - rtWx.tOff) > 120) // if changing the timeoffset (request other historic data) then we must have new weather before proceeding rtWx.QNH = NAN; + return tNextWeather; } - else - // in all other cases we ask for traffic data - curr.eRequType = CurrTy::RT_REQU_TRAFFIC; + if (rtWx.nErr < RT_DRCT_MAX_WX_ERR && // not yet seen too many weather request errors? _AND_ + std::chrono::steady_clock::now() >= rtWx.next) // just time for a weather update + { + curr.eRequType = CurrTy::RT_REQU_WEATHER; + return tNextWeather; + } + if (bDoParkedTraffic && LTAptAvailable()) { // Do the parked traffic now, and only when airport details are available so we can place the aircraft correctly + curr.eRequType = CurrTy::RT_REQU_PARKED; + return tNextTraffic; + } + + // in all other cases we ask for traffic data + curr.eRequType = CurrTy::RT_REQU_TRAFFIC; + return tNextTraffic; } @@ -293,8 +356,13 @@ std::string RealTrafficConnection::GetURL (const positionTy&) switch (curr.eRequType) { case CurrTy::RT_REQU_AUTH: return RT_AUTH_URL; + case CurrTy::RT_REQU_DEAUTH: + return RT_DEAUTH_URL; + case CurrTy::RT_REQU_NEAREST_METAR: + return RT_NEAREST_METAR_URL; case CurrTy::RT_REQU_WEATHER: return RT_WEATHER_URL; + case CurrTy::RT_REQU_PARKED: case CurrTy::RT_REQU_TRAFFIC: return RT_TRAFFIC_URL; } @@ -313,22 +381,49 @@ void RealTrafficConnection::ComputeBody (const positionTy&) dataRefs.GetRTLicense().c_str(), HTTP_USER_AGENT); break; + case CurrTy::RT_REQU_DEAUTH: + snprintf(s, sizeof(s), RT_DEAUTH_POST, + curr.sGUID.c_str()); + break; + case CurrTy::RT_REQU_NEAREST_METAR: + rtWx.pos = dataRefs.GetUsersPlanePos(); + snprintf(s, sizeof(s), RT_NEAREST_METAR_POST, + curr.sGUID.c_str(), + rtWx.pos.lat(), rtWx.pos.lon(), + curr.tOff); + break; case CurrTy::RT_REQU_WEATHER: + rtWx.pos = dataRefs.GetUsersPlanePos(); snprintf(s, sizeof(s), RT_WEATHER_POST, curr.sGUID.c_str(), - curr.pos.lat(), curr.pos.lon(), 0L, + rtWx.pos.lat(), rtWx.pos.lon(), std::lround(rtWx.pos.alt_ft()), + rtWx.nearestMETAR.ICAO.c_str(), curr.tOff); break; + case CurrTy::RT_REQU_PARKED: case CurrTy::RT_REQU_TRAFFIC: { // we add 10% to the bounding box to have some data ready once the plane is close enough for display const boundingBoxTy box (curr.pos, double(dataRefs.GetFdStdDistance_m()) * 1.10); - snprintf(s,sizeof(s), RT_TRAFFIC_POST, - curr.sGUID.c_str(), - box.nw.lat(), box.se.lat(), - box.nw.lon(), box.se.lon(), - curr.tOff); + // If we request traffic for the very first time, then we ask for some buffer into the past for faster plane display + if ((curr.eRequType == CurrTy::RT_REQU_TRAFFIC) && IsFirstRequ()) { + snprintf(s,sizeof(s), RT_TRAFFIC_POST_BUFFER, + curr.sGUID.c_str(), + box.nw.lat(), box.se.lat(), + box.nw.lon(), box.se.lon(), + curr.tOff, + dataRefs.GetFdBufPeriod() / 10); // One buffer per 10s of buffering time + } + // normal un-buffered request for traffic or parked aircraft + else { + snprintf(s,sizeof(s), + curr.eRequType == CurrTy::RT_REQU_TRAFFIC ? RT_TRAFFIC_POST : RT_TRAFFIC_POST_PARKED, + curr.sGUID.c_str(), + box.nw.lat(), box.se.lat(), + box.nw.lon(), box.se.lon(), + curr.tOff); + } break; } } @@ -352,7 +447,7 @@ bool RealTrafficConnection::ProcessFetchedData () if (!pRoot) { LOG_MSG(logERR,ERR_JSON_PARSE); IncErrCnt(); return false; } JSON_Object* pObj = json_object(pRoot.get()); if (!pObj) { LOG_MSG(logERR,ERR_JSON_MAIN_OBJECT); IncErrCnt(); return false; } - + // Try the error fields first long rStatus = jog_l(pObj, "status"); if (!rStatus) { LOG_MSG(logERR,"Response has no 'status'"); IncErrCnt(); return false; } @@ -360,7 +455,6 @@ bool RealTrafficConnection::ProcessFetchedData () std::string rMsg = jog_s(pObj, "message"); // --- Error processing --- - rrlWait = RT_DRCT_ERR_WAIT; // Standard is: retry in 5s // For failed weather requests keep a separate counter if (curr.eRequType == CurrTy::RT_REQU_WEATHER && rStatus != HTTP_OK) { @@ -382,50 +476,61 @@ bool RealTrafficConnection::ProcessFetchedData () } else { LOG_MSG(logWARN, "RealTraffic returned: %s", rMsg.c_str()); IncErrCnt(); + tNextTraffic = tNextWeather = std::chrono::steady_clock::now() + RT_DRCT_ERR_WAIT; return false; } case HTTP_METH_NOT_ALLWD: // Send for "too many sessions" / "request rate violation" LOG_MSG(logERR, "RealTraffic: %s", rMsg.c_str()); IncErrCnt(); - rrlWait = std::chrono::seconds(10); // documentation says "wait 10 seconds" + // documentation says "wait 10 seconds" + tNextTraffic = tNextWeather = std::chrono::steady_clock::now() + RT_DRCT_ERR_RATE; curr.sGUID.clear(); // force re-login return false; case HTTP_UNAUTHORIZED: // means our GUID expired LOG_MSG(logDEBUG, "Session expired"); curr.sGUID.clear(); // re-login immediately - rrlWait = std::chrono::milliseconds(0); + tNextTraffic = tNextWeather = std::chrono::steady_clock::now(); return false; case HTTP_FORBIDDEN: LOG_MSG(logWARN, "RealTraffic forbidden: %s", rMsg.c_str()); IncErrCnt(); + tNextTraffic = tNextWeather = std::chrono::steady_clock::now() + RT_DRCT_ERR_WAIT; return false; case HTTP_INTERNAL_ERR: default: SHOW_MSG(logERR, "RealTraffic returned an error: %s", rMsg.c_str()); IncErrCnt(); + tNextTraffic = tNextWeather = std::chrono::steady_clock::now() + RT_DRCT_ERR_WAIT; return false; } // All good, process the request - + // Wait till next request? - long l = jog_l(pObj, "rrl"); // Wait time till next request + long rrl = jog_l(pObj, "rrl"); // Wait time till next request, separately for traffic and weather + long wrrl = jog_l(pObj, "wrrl"); switch (curr.eRequType) { - case CurrTy::RT_REQU_AUTH: // after an AUTH request we take the rrl unchanged, ie. as quickly as possible + case CurrTy::RT_REQU_AUTH: // in most cases we continue as quickly as possible + case CurrTy::RT_REQU_DEAUTH: + case CurrTy::RT_REQU_PARKED: + case CurrTy::RT_REQU_WEATHER: break; - case CurrTy::RT_REQU_WEATHER: // unfortunately, no `rrl` in weather requests... - l = 300; // we just continue 300ms later + case CurrTy::RT_REQU_NEAREST_METAR: // after learning the META we continue quickly with the weather request + wrrl = 50; break; case CurrTy::RT_REQU_TRAFFIC: // By default we wait at least 8s, or more if RealTraffic instructs us so - if (l < RT_DRCT_DEFAULT_WAIT) - l = RT_DRCT_DEFAULT_WAIT; + if (rrl < RT_DRCT_DEFAULT_WAIT) + rrl = RT_DRCT_DEFAULT_WAIT; break; } - rrlWait = std::chrono::milliseconds(l); + if (rrl > 0) + tNextTraffic = std::chrono::steady_clock::now() + std::chrono::milliseconds(rrl); + if (wrrl > 0) + tNextWeather = std::chrono::steady_clock::now() + std::chrono::milliseconds(wrrl); // --- Authorization --- if (curr.eRequType == CurrTy::RT_REQU_AUTH) { @@ -441,17 +546,38 @@ bool RealTrafficConnection::ProcessFetchedData () return true; } + // --- De-authentication (closing the session) --- + if (curr.eRequType == CurrTy::RT_REQU_DEAUTH) { + curr.sGUID.clear(); + LOG_MSG(logDEBUG, "De-authenticated"); + return true; + } + + // --- Nearest METAR location --- + if (curr.eRequType == CurrTy::RT_REQU_NEAREST_METAR) { + ProcessNearestMETAR(json_object_get_array(pObj, "data")); + return true; + } + // --- Weather --- if (curr.eRequType == CurrTy::RT_REQU_WEATHER) { - // We are interested in just a single value: local Pressure - const double wxSLP = jog_n_nan(pObj, "data.locWX.SLP"); + // Here, we are interested in just a single value: local Pressure + double wxQNH = jog_n_nan(pObj, "data.QNH"); // ideally QNH + if (std::isnan(wxQNH)) + wxQNH = jog_n_nan(pObj, "data.locWX.SLP"); // of not given then SLP + // Error in locWX data? std::string s = jog_s(pObj, "data.locWX.Error"); // sometimes errors are given in a specific field - if (s.empty() && - !strcmp(jog_s(pObj, "data.locWX.Info"), "TinyDelta")) // if we request too often then Info is 'TinyDelta' - s = "TinyDelta"; + if (s.empty()) { // and at other times there is something in the 'Info' field...not very consistent + s = jog_s(pObj, "data.locWX.Info"); + if (!s.empty() && + s != "TinyDelta" && // if we request too often then Info is 'TinyDelta', and we let it sit in 's' + s.substr(0,6) != "error:") // any error starts with "error:" and we let it sit in 's' + s.clear(); + } + // Any error, either explicitely or because local pressure is bogus? - if (!s.empty() || std::isnan(wxSLP) || wxSLP < 800.0) + if (!s.empty() || std::isnan(wxQNH) || wxQNH < 800.0) { if (s == "File requested") { // Error "File requested" often occurs when requesting historic weather that isn't cached on the server, so we only issue debug-level message @@ -463,7 +589,7 @@ bool RealTrafficConnection::ProcessFetchedData () s.c_str(), netData); } else { LOG_MSG(logERR, "RealTraffic returned no or invalid local pressure %.1f:\n%s", - wxSLP, netData); + wxQNH, netData); } } // one more error @@ -473,33 +599,69 @@ bool RealTrafficConnection::ProcessFetchedData () // Too many WX errors? We give up and just use standard pressure if (rtWx.nErr >= RT_DRCT_MAX_WX_ERR) { SHOW_MSG(logERR, "Too many errors trying to fetch RealTraffic weather, will continue without; planes may appear at slightly wrong altitude."); - rtWx.set(HPA_STANDARD, curr, false); + rtWx.set(HPA_STANDARD, curr.tOff, false); + rtWx.pos = positionTy(); } else { // We will request weather directly again, but need to wait 60s for it - rrlWait = std::chrono::seconds(60); + tNextWeather = std::chrono::steady_clock::now() + std::chrono::seconds(60); } } return false; } - - // Successfully received weather information - LOG_MSG(logDEBUG, "Received RealTraffic locWX.SLP = %.1f", wxSLP); - rtWx.set(wxSLP, curr); // Save new QNH + + // If we have METAR info pass that on, too + s = jog_s(pObj, "data.ICAO"); + std::string metar = jog_s(pObj, "data.METAR"); + + if (s.empty() || s == "UNKN") { // ignore no/unknown METAR + s.clear(); + metar.clear(); + } + + // If this is live data, not historic, then we can use it instead of separately querying METAR + if (!isHistoric()) { + rtWx.w.qnh_pas = dataRefs.SetWeather((float)wxQNH, + (float)rtWx.pos.lat(), (float)rtWx.pos.lon(), + s, metar); + } + // historic data + else { + // Try reading QNH from METAR + rtWx.w.qnh_pas = WeatherQNHfromMETAR(metar); + } + + // Successfully received local pressure information + rtWx.set(std::isnan(rtWx.w.qnh_pas) ? wxQNH : double(rtWx.w.qnh_pas), curr.tOff); // Save new QNH + LOG_MSG(logDEBUG, "Received RealTraffic Weather with QNH = %.1f", rtWx.QNH); + + // If requested to set X-Plane's weather based on detailed weather data + if (dataRefs.GetWeatherControl() == WC_REAL_TRAFFIC) { + ProcessWeather (json_object_get_object(pObj, "data")); + if (std::isnan(rtWx.w.qnh_pas)) + rtWx.w.qnh_pas = float(rtWx.QNH); + } + return true; } + // --- Parked Aircraft --- + if (curr.eRequType == CurrTy::RT_REQU_PARKED) { + bDoParkedTraffic = false; // Repeat only when instructed + return ProcessParkedAcBuffer(json_object_get_object(pObj, "data")); + } + // --- Traffic data --- // In `dataepoch` RealTraffic delivers the point in time when the data was valid. // That is relevant especially for historic data, when `dataepoch` is in the past. - l = jog_l(pObj, "dataepoch"); - if (l > long(JAN_FIRST_2019)) + const long epoch = jog_l(pObj, "dataepoch"); + if (epoch > long(JAN_FIRST_2019)) { // "now" is the simulated time plus the buffering period const long simTime = long(dataRefs.GetSimTime()); const long bufTime = long(dataRefs.GetFdBufPeriod()); // As long as the timestamp is half the buffer time close to "now" we consider the data current, ie. non-historic - if (l > simTime + bufTime/2) { + if (epoch > simTime + bufTime/2) { if (tsAdjust > 0.0) { // is that a change from historic delivery? SHOW_MSG(logINFO, INFO_RT_REAL_TIME); } @@ -507,7 +669,7 @@ bool RealTrafficConnection::ProcessFetchedData () } // we have historic data else { - long diff = simTime + bufTime - l; // difference between "now" + long diff = simTime + bufTime - epoch; // difference between "now" diff -= 10; // rounding 10s above the minute down, diff += 60 - diff % 60; // everything else up to the next minute if (long(tsAdjust) != diff) { // is this actually a change? @@ -529,7 +691,49 @@ bool RealTrafficConnection::ProcessFetchedData () prevWarn = now; } } + + // The 'data' object holds the aircraft data in two different variants: + // - directly, then it holds a set of objects, each being an aircraft (essentially a fake array) + // - buffered, then it holds a set of objects, which in turn hold a set of aircraft objects + const JSON_Object* pData = json_object_get_object(pObj, "data"); + if (!pData) { + LOG_MSG(logERR, "Response is missing the 'data' object that would have the aircraft data!"); + IncErrCnt(); + return false; + } + // Has buffered data? Then we need to loop those buffers + bool bRet = true; + if (json_object_has_value_of_type(pData, "buffer_0", JSONObject)) { + // But we want to process them in correct chronological order, + // so we need to find out the last buffer and work our way up + for (int i = (int) json_object_get_count(pData) - 1; + i >= 0; --i) + { + char bufName[20]; + snprintf(bufName, sizeof(bufName), "buffer_%d", i); + if (!ProcessTrafficBuffer(json_object_get_object(pData, bufName))) { + if (i == 0) { // really important only is buffer_0, the one with the real-time data + LOG_MSG(logWARN, "Couldn't process 'buffer_0'!"); + IncErrCnt(); + bRet = false; + } + } + } + } + // no buffered data, just process the one set of data that is there + else { + bRet = ProcessTrafficBuffer(pData); + } + + return bRet; +} +// in direct mode process an object with aircraft data, essentially a fake array +bool RealTrafficConnection::ProcessTrafficBuffer (const JSON_Object* pBuf) +{ + // Quick exit if no data + if (!pBuf) return false; + // any a/c filter defined for debugging purposes? const std::string acFilter ( dataRefs.GetDebugAcFilter() ); @@ -549,12 +753,12 @@ bool RealTrafficConnection::ProcessFetchedData () // "a26738": ["a26738",33.671356,-117.867284, ...], // "full_count": 13501, "source": "MemoryDB", "rrl": 2000, "status": 200, "dataepoch": 1703885732 // } - const size_t numVals = json_object_get_count(pObj); + const size_t numVals = json_object_get_count(pBuf); for (size_t i = 0; i < numVals && shallRun(); ++i) { // Get the array 'behind' the i-th value, // will fail if it is no aircraft entry - const JSON_Value* pVal = json_object_get_value_at(pObj, i); + const JSON_Value* pVal = json_object_get_value_at(pBuf, i); if (!pVal) break; const JSON_Array* pJAc = json_value_get_array(pVal); if (!pJAc) continue; // probably not an aircraft line @@ -623,6 +827,9 @@ bool RealTrafficConnection::ProcessFetchedData () std::string s = jag_s(pJAc, RT_DRCT_Category); stat.catDescr = GetADSBEmitterCat(s); + // RealTraffic often sends ASW20 when it should be AS20, a glider + if (stat.acTypeIcao == "ASW20") stat.acTypeIcao = "AS20"; + // Static objects are all equally marked with a/c type TWR if ((s == "C3" || s == "C4" || s == "C5") || (stat.reg == STATIC_OBJECT_TYPE && stat.acTypeIcao == STATIC_OBJECT_TYPE)) @@ -695,6 +902,396 @@ bool RealTrafficConnection::ProcessFetchedData () return true; } + +// in direct mode process an object with parked aircraft data, essentially a fake array +bool RealTrafficConnection::ProcessParkedAcBuffer (const JSON_Object* pData) +{ + // Quick exit if no data + if (!pData) return false; + + // any a/c filter defined for debugging purposes? + const std::string acFilter ( dataRefs.GetDebugAcFilter() ); + + // Current camera position + const positionTy posView = dataRefs.GetViewPos(); + + // The data is delivered in many many values, + // ie. each plane is just a JSON_Value, its name being the hexid, + // its value being an array with the details. + // That means we need to traverse all values. + // { + // "7c4920": [-33.936407, 151.169229, "EGLL_569", "A388", "VH-OQA", 1721590016.01, "QFA2"], + // "7c5325": [-33.936333, 151.170109, "EGLL_569", "A333", "VH-QPJ", 1721597924.81, "QFA128"], + // "7c765a": [-33.935635, 151.17746, "EGLL_320", "A320", "VH-XNW", 1721650004.0, "JST825"] + // } + + // Additionally, at busy places like EGLL we receive duplicates, + // ie. different aircraft said to be parked at the same location. + // To reduce duplicates, we pre-process the data and pick the latest of duplicates + struct parkedAcData { + double lat, lon, ts; + std::string key, acType, reg, call; + }; + // We organize the data by parking position to be able to identify duplicates + std::map mapPData; + + const size_t numVals = json_object_get_count(pData); + for (size_t i = 0; i < numVals && shallRun(); ++i) + { + // Aircraft key (hexId) + std::string key = json_object_get_name(pData, i); + + // Get the array 'behind' the i-th value, + // will fail if it is no aircraft entry + const JSON_Value* pVal = json_object_get_value_at(pData, i); + if (!pVal) break; + const JSON_Array* pJAc = json_value_get_array(pVal); + if (!pJAc) continue; // probably not an aircraft line + + // Check for minimum number of fields + if (json_array_get_count(pJAc) < RT_PARK_NUM_FIELDS ) { + LOG_MSG(logWARN, "Received too few fields in parked a/c record %ld", (long)i); + IncErrCnt(); + continue; + } + + // Get the parking position and timestamp first to check for duplicates + std::string parkPos = jag_s(pJAc, RT_PARK_ParkPosName); + double ts = jag_n(pJAc, RT_PARK_LastTimeStamp); + if (mapPData.count(parkPos) > 0) { + // we know that parking position already! + parkedAcData& dat = mapPData.at(parkPos); + if (ts > dat.ts) { // but new data is newer -> replace + dat = { + jag_n_nan(pJAc, RT_PARK_Lat), + jag_n_nan(pJAc, RT_PARK_Lon), + ts, + std::move(key), + jag_s(pJAc, RT_PARK_AcType), + jag_s(pJAc, RT_PARK_Reg), + jag_s(pJAc, RT_PARK_CallSign) + }; + } + } else { + // We don't yet know that parking position, store data in new map record + mapPData.emplace(std::move(parkPos), + parkedAcData { + jag_n_nan(pJAc, RT_PARK_Lat), + jag_n_nan(pJAc, RT_PARK_Lon), + ts, + std::move(key), + jag_s(pJAc, RT_PARK_AcType), + jag_s(pJAc, RT_PARK_Reg), + jag_s(pJAc, RT_PARK_CallSign) + }); + } + } + + // Now mapPData is filled with parked aircraft that we really want to process + for (auto& p: mapPData) + { + parkedAcData& dat = p.second; + + // Get the name of the i-th value, that is the hex id + LTFlightData::FDKeyTy fdKey (LTFlightData::KEY_ICAO, dat.key); + + // not matching a/c filter? -> skip it + if ((!acFilter.empty() && (fdKey != acFilter)) ) + continue; + + // Check for duplicates with OGN/FLARM, potentially replaces the key type + if (fdKey.eKeyType == LTFlightData::KEY_ICAO) + LTFlightData::CheckDupKey(fdKey, LTFlightData::KEY_FLARM); + else + // Some codes are otherwise often duplicate with ADSBEx + LTFlightData::CheckDupKey(fdKey, LTFlightData::KEY_ADSBEX); + + // position + positionTy pos (dat.lat, dat.lon); + pos.heading() = 0.0; + pos.f.onGrnd = GND_ON; // parked aircraft are by definition on the ground + // see later how TS is used: we send 3 instances to make the a/c appear immediately + pos.ts() = dataRefs.GetSimTime() - 0.5 * double(dataRefs.GetFdBufPeriod()); + + // position is rather important, we check for validity + // (we do allow alt=NAN if on ground) + if ( !pos.isNormal(true) ) { + LOG_MSG(logDEBUG,ERR_POS_UNNORMAL,fdKey.c_str(),pos.dbgTxt().c_str()); + continue; + } + + // Static data + LTFlightData::FDStaticData stat; + stat.acTypeIcao = std::move(dat.acType); + stat.call = std::move(dat.call); + stat.reg = std::move(dat.reg); + + // RealTraffic often sends ASW20 when it should be AS20, a glider + if (stat.acTypeIcao == "ASW20") stat.acTypeIcao = "AS20"; + + // Dynamic data + LTFlightData::FDDynamicData dyn; + dyn.radar.mode = xpmpTransponderMode_Standby; + dyn.gnd = true; + dyn.heading = pos.heading(); + dyn.ts = pos.ts(); + dyn.spd = 0.0; + dyn.vsi = 0.0; + dyn.pChannel = this; + + // Try to find a matching "startup position" to perfectly put the aircraft in place + positionTy startupPos = LTAptFindStartupLoc(pos, + (double)dataRefs.GetFdSnapTaxiDist_m()); + if (startupPos.isNormal(true)) { + pos.lat() = startupPos.lat(); + pos.lon() = startupPos.lon(); + pos.heading() = startupPos.heading(); + } + + try { + // from here on access to fdMap guarded by a mutex + // until FD object is inserted and updated + std::unique_lock mapFdLock (mapFdMutex); + + // get the fd object from the map, key is the transpIcao + // this fetches an existing or, if not existing, creates a new one + LTFlightData& fd = mapFd[fdKey]; + + // also get the data access lock once and for all + // so following fetch/update calls only make quick recursive calls + std::lock_guard fdLock (fd.dataAccessMutex); + // now that we have the detail lock we can release the global one + mapFdLock.unlock(); + + // completely new? fill key fields + if ( fd.empty() ) + fd.SetKey(fdKey); + + // add the static data + fd.UpdateData(std::move(stat), pos.dist(posView)); + + // add the "dynamic" data + // We send in the position 3 times in enough of a time distance for the plane to appear directly + for (int k = 0; k < 4; ++k) { + fd.AddDynData(dyn, 0, 0, &pos); + pos.ts() = (dyn.ts += 0.5 * double(dataRefs.GetFdBufPeriod())); + } + + } catch(const std::system_error& e) { + LOG_MSG(logERR, ERR_LOCK_ERROR, "mapFd", e.what()); + IncErrCnt(); + } + } + + LOG_MSG(logINFO, "Received %d parked aircraft", int(numVals)); + + return true; +} +// +// MARK: Direct Connection, Weather processing +// + + +// parse RT's NearestMETAR response array entry +bool RealTrafficConnection::NearestMETAR::Parse (const JSON_Object* pObj) +{ + if (!pObj) { + LOG_MSG(logWARN, "Array entry 'data[]' empty!"); + return false; + } + + ICAO = jog_s(pObj, "ICAO"); + dist = jog_n_nan(pObj, "Dist"); + brgTo = jog_n_nan(pObj, "BrgTo"); + + return isValid(); +} + + +// in direct mode process NearestMETAR response, find a suitable METAR from the returned array +void RealTrafficConnection::ProcessNearestMETAR (const JSON_Array* pData) +{ + if (!pData) { + LOG_MSG(logWARN, "JSON response is missing 'data' array!"); + return; + } + + // Reset until we know better + rtWx.nearestMETAR.clear(); + + // Array must have at least one element + const size_t cntMetars = json_array_get_count(pData); + if (cntMetars < 1) { + LOG_MSG(logWARN, "Received no nearest METARs from RealTraffic ('data' array empty)"); + return; + } + + // The first METAR is the closest, so a priori best cadidate to be used + NearestMETAR m(json_array_get_object(pData, 0)); + if (!m.isValid()) { + LOG_MSG(logWARN, "Nearest METAR ('data[0]') isn't valid"); + return; + } + // even the nearest station is too far away for reliable weather? + if (m.dist > dataRefs.GetWeatherMaxMetarDist_nm()) { + LOG_MSG(logDEBUG, "Nearest METAR location too far away, using none"); + rtWx.nearestMETAR.clear(); + return; + } + + // Check for better matching station in direction of flight, + // but start with that one + rtWx.nearestMETAR = m; + + // Factor in bearing of station in relation to current plane's flight path: + // It's worth half the distance if it is straight ahead (0 deg difference) + // and the full distance if it is in my back (180 deg difference), + // so that position ahead of me appear "nearer" + double bestDist = m.dist * (180.0 + std::abs(HeadingDiff(rtWx.pos.heading(), double(m.brgTo)))) / 360.0; + + for (size_t i = 1; i < cntMetars; ++i) + { + m.Parse(json_array_get_object(pData, i)); // parse the next METAR info record + if (m.dist > dataRefs.GetWeatherMaxMetarDist_nm()) // stop processing once the stations are too far away + break; + // based on weighted distance, is this station "closer" than our current best? + const double weightedDist = m.dist * (180.0 + std::abs(HeadingDiff(rtWx.pos.heading(), double(m.brgTo)))) / 360.0; + if (weightedDist < bestDist) { + bestDist = weightedDist; // then use it! + rtWx.nearestMETAR = m; + } + } + + LOG_MSG(logDEBUG, "Using Nearest METAR location %s (%.1fnm, %.0fdeg)", + rtWx.nearestMETAR.ICAO.c_str(), + rtWx.nearestMETAR.dist, rtWx.nearestMETAR.brgTo); +} + + +// in direct mode process detailed weather information +void RealTrafficConnection::ProcessWeather(const JSON_Object* pData) +{ + rtWx.w = LTWeather(); // reset all values + if (!pData) { + LOG_MSG(logWARN, "JSON response is missing 'data' object!"); + return; + } + const JSON_Object* pLocWX = json_object_get_object(pData, "locWX"); + if (!pLocWX) { + LOG_MSG(logWARN, "JSON response is missing 'data.locWX' object!"); + return; + } + + // --- Process detailed weather data --- + const JSON_Array* pDPs = json_object_get_array(pLocWX, "DPs"); + const JSON_Array* pTEMPs = json_object_get_array(pLocWX, "TEMPs"); + const JSON_Array* pWDIRs = json_object_get_array(pLocWX, "WDIRs"); + const JSON_Array* pWSPDs = json_object_get_array(pLocWX, "WSPDs"); + const JSON_Array* pDZDTs = json_object_get_array(pLocWX, "DZDTs"); + std::vector aTEMPs; + + if (!pDPs || !pTEMPs || !pWDIRs || !pWSPDs || !pDZDTs) { + LOG_MSG(logWARN, "JSON response is missing one of the following arrays in data.locWX: DPs, TEMPs, WDIRs, WSPDs, DZDTs"); + } + + rtWx.w.pos = rtWx.pos; + + // METAR + rtWx.w.metar = jog_s(pData, "METAR"); + rtWx.w.metarFieldIcao = jog_s(pData, "ICAO"); + rtWx.w.posMetarField = positionTy(); // field's location is to be determined later in main thread + + rtWx.w.visibility_reported_sm = float(jog_n_nan(pLocWX, "SVis") / M_per_SM); + rtWx.w.sealevel_pressure_pas = float(jog_n_nan(pLocWX, "SLP") * 100.0); + if (pTEMPs && json_array_get_count(pTEMPs) >= 1) // use temperature of lowest level, adjusted per temperature lapse rate + rtWx.w.sealevel_temperature_c = float(jag_n_nan(pTEMPs, 0)) - RT_ATMOS_LAYERS.front() * float(TEMP_LAPS_R); + rtWx.w.rain_percent = std::min(float(jog_n_nan(pLocWX, "PRR")) / 9.0f, 1.0f); // RT sends mm/h and says ">7.5 is heavy", XP wants a 0..1 scale + + // Wind + rtWx.w.wind_altitude_msl_m = rtWx.w.atmosphere_alt_levels_m; // we just use the standard atmospheric layers of XP, need to interpolate anyway as RT sends more layers + if (pWSPDs) { + rtWx.w.Interpolate(rtWx.interp, jag_f_vector(pWSPDs), rtWx.w.wind_speed_msc); + std::for_each(rtWx.w.wind_speed_msc.begin(), rtWx.w.wind_speed_msc.end(), + [](float& f){ f *= float(NM_per_KM); }); // convert from km/h to kn=nm/h + } + if (pWDIRs) + rtWx.w.InterpolateDir(rtWx.interp, jag_f_vector(pWDIRs), rtWx.w.wind_direction_degt); + if (pDZDTs) { + rtWx.w.Interpolate(rtWx.interp, jag_f_vector(pDZDTs), rtWx.w.turbulence); + std::for_each(rtWx.w.turbulence.begin(), rtWx.w.turbulence.end(), + [](float& f){ f = std::clamp(f * 0.5f, 0.0f, 1.0f); }); // convert from RT's scale (">2 severe") to XP's of 0.0..1.0 + } + + // Temperature + if (pDPs) + rtWx.w.Interpolate(rtWx.interp, jag_f_vector(pDPs), rtWx.w.dewpoint_deg_c); + rtWx.w.temperature_altitude_msl_m = rtWx.w.atmosphere_alt_levels_m; + if (pTEMPs) + rtWx.w.Interpolate(rtWx.interp, aTEMPs = jag_f_vector(pTEMPs), rtWx.w.temperatures_aloft_deg_c); + + // Cloud layers + ProcessCloudLayer(json_object_get_object(pLocWX, "LLC"), 0); + ProcessCloudLayer(json_object_get_object(pLocWX, "MLC"), 1); + ProcessCloudLayer(json_object_get_object(pLocWX, "HLC"), 2); + + // Troposhere + rtWx.w.tropo_alt_m = float(jog_n_nan(pLocWX, "TPP")); + if (!aTEMPs.empty() && !std::isnan(rtWx.w.tropo_alt_m)) + rtWx.w.tropo_temp_c = interpolate(RT_ATMOS_LAYERS, aTEMPs, rtWx.w.tropo_alt_m); + + // Waves + rtWx.w.wave_dir = float(jog_n_nan(pLocWX, "SWDIR")); // we just use surface wind direction directly + if (std::isnan(rtWx.w.wave_dir)) + rtWx.w.wave_dir = rtWx.w.wind_direction_degt.front(); + float SWSPD = float(jog_n_nan(pLocWX, "SWSPD")); // we determine the wave amplitude based on surface wind speed [km/h] + if (!std::isnan(SWSPD)) { + // We take wave amplitudes from https://www.wpc.ncep.noaa.gov/html/beaufort.shtml + SWSPD *= (float)NM_per_KM; // convert to knots + if (SWSPD < 1) rtWx.w.wave_amplitude = 0.0f; + else if (SWSPD < 4) rtWx.w.wave_amplitude = 0.1f; + else if (SWSPD < 7) rtWx.w.wave_amplitude = 0.25f; + else if (SWSPD < 11) rtWx.w.wave_amplitude = 0.8f; + else if (SWSPD < 17) rtWx.w.wave_amplitude = 1.25f; + else if (SWSPD < 22) rtWx.w.wave_amplitude = 2.25f; + else if (SWSPD < 28) rtWx.w.wave_amplitude = 3.5f; + else if (SWSPD < 34) rtWx.w.wave_amplitude = 4.75f; + else if (SWSPD < 41) rtWx.w.wave_amplitude = 6.5f; + else if (SWSPD < 48) rtWx.w.wave_amplitude = 8.5f; + else if (SWSPD < 56) rtWx.w.wave_amplitude = 10.75f; + else if (SWSPD < 64) rtWx.w.wave_amplitude = 13.75f; + else rtWx.w.wave_amplitude = 17.0f; + } + + // Rwy_Friction + if (rtWx.w.metar.empty()) // ideally set later based on METAR + // Rain causes wet status [0..7] + rtWx.w.runway_friction = (int)std::lround(rtWx.w.rain_percent * 7.f); + + // Have the weather set (force immediate update on first weather data) + rtWx.w.update_immediately = IsFirstRequ(); + WeatherSet(rtWx.w); +} + +// in direct mode process one cloud layer +void RealTrafficConnection::ProcessCloudLayer(const JSON_Object* pCL, size_t i) +{ + if (!pCL) return; + const float cover = (float) jog_n_nan(pCL, "cover"); + const float base = (float) jog_n_nan(pCL, "base"); + const float tops = (float) jog_n_nan(pCL, "tops"); + const float type = (float) jog_n_nan(pCL, "type"); + const float conf = (float) jog_n_nan(pCL, "confidence"); + if (std::isnan(cover) || std::isnan(base) || std::isnan(tops) || std::isnan(type) || std::isnan(conf)) + return; + + rtWx.w.cloud_type[i] = type; + rtWx.w.cloud_coverage_percent[i] = cover/100.0f; + rtWx.w.cloud_base_msl_m[i] = base; + rtWx.w.cloud_tops_msl_m[i] = tops; +} + + + // // MARK: UDP/TCP via App // @@ -1134,8 +1731,7 @@ void RealTrafficConnection::SendPos (const positionTy& pos, double speed_m) void RealTrafficConnection::SendUsersPlanePos() { double airSpeed_m = 0.0; - double track = 0.0; - positionTy pos = dataRefs.GetUsersPlanePos(airSpeed_m,track); + positionTy pos = dataRefs.GetUsersPlanePos(&airSpeed_m); SendPos(pos, airSpeed_m); } diff --git a/Src/LTSynthetic.cpp b/Src/LTSynthetic.cpp index f53f2a2..3a68454 100644 --- a/Src/LTSynthetic.cpp +++ b/Src/LTSynthetic.cpp @@ -122,12 +122,19 @@ bool SyntheticConnection::FetchAllData(const positionTy&) else { // this a/c is _not_ or no longer parked, so we should forget about it in our stored data mapSynData.erase(p.first); + } - // next test if the aircraft came too close to any of ours on the ground - if (ac.IsOnGrnd() && !ac.IsGroundVehicle()) { - for (auto i = mapSynData.begin(); i != mapSynData.end(); ) { - // For the sake of performance we do the test very quick - if (i->second.pos.distRoughSqr(ac.GetPPos()) < GND_COLLISION_DIST*GND_COLLISION_DIST) { + // Test if the aircraft came too close to any other parked aircraft on the ground + if (ac.IsOnGrnd() && !ac.IsGroundVehicle()) { + for (auto i = mapSynData.begin(); i != mapSynData.end(); ) { + // Only compare to other aircraft (not myself) + if (i->first == key) { + ++i; + } else { + const double dist = i->second.pos.dist(ac.GetPPos()); + if (dist < GND_COLLISION_DIST) + { + // Remove the other parked aircraft LOG_MSG(logDEBUG, "%s came too close to parked %s, removing the parked aircraft", fd.keyDbg().c_str(), i->first.c_str()); i = mapSynData.erase(i); @@ -147,6 +154,10 @@ bool SyntheticConnection::ProcessFetchedData () { // Timestamp with which we send the data const double tNow = (double)std::time(nullptr); + // Camera pos + const positionTy posCam = dataRefs.GetViewPos(); + // Squared search distance for distance comparison + const double distSearchSqr = sqr(dataRefs.GetFdStdDistance_m()); // --- Parked Aircraft --- // For all stored aircraft @@ -156,11 +167,18 @@ bool SyntheticConnection::ProcessFetchedData () const LTFlightData::FDKeyTy& key = i->first; SynDataTy& parkDat = i->second; + // Only process planes in search distance + // We keep the data in memory, just in case we come back, but we don't feed data for unneeded planes + if (parkDat.pos.distRoughSqr(posCam) > distSearchSqr) { + ++i; // next plane + continue; + } + // Find the related flight data std::unique_lock mapLock (mapFdMutex); // lock the entire map LTFlightData& fd = mapFd[key]; // fetch or create flight data mapLock.unlock(); // release the map lock - + // Haven't yet looked up startup position's heading? if (std::isnan(parkDat.pos.heading())) { parkDat.pos.heading() = LTAptFindStartupLoc(parkDat.pos).heading(); diff --git a/Src/LTWeather.cpp b/Src/LTWeather.cpp index b55d234..a526c1a 100644 --- a/Src/LTWeather.cpp +++ b/Src/LTWeather.cpp @@ -1,5 +1,12 @@ /// @file LTWeather.cpp -/// @brief Fetch real weather information from AWC +/// @brief Set X-Plane weather / Fetch real weather information from AWC +/// +/// @details Set X-Plane weather based on weather information received from channels like RealTraffic +/// @note Available with X-Plane 12 only +/// @see https://developer.x-plane.com/article/weather-datarefs-in-x-plane-12/ +/// +/// @details Fetch METAR information from Aviation Weather, +/// mainly to learn about QNH for altitude correction /// @see https://aviationweather.gov/data/api/#/Dataserver/dataserverMetars /// @see Example request: Latest weather somewhere around EDDL, limited to the fields we are interested in: /// https://aviationweather.gov//cgi-bin/data/dataserver.php?requestType=retrieve&dataSource=metars&format=xml&boundingBox=45.5,0.5,54.5,9,5&hoursBeforeNow=2&mostRecent=true&fields=raw_text,station_id,latitude,longitude,altim_in_hg @@ -52,7 +59,7 @@ /// @endcode /// /// @author Birger Hoppe -/// @copyright (c) 2018-2023 Birger Hoppe +/// @copyright (c) 2018-2024 Birger Hoppe /// @copyright Permission is hereby granted, free of charge, to any person obtaining a /// copy of this software and associated documentation files (the "Software"), /// to deal in the Software without restriction, including without limitation @@ -71,100 +78,1183 @@ #include "LiveTraffic.h" +// metaf header-only library for parsing METAR +// see https://github.com/nnaumenko/metaf +#include "metaf.hpp" + // -// MARK: Weather Network Request handling +// MARK: Set X-Plane Weather // -/// The request URL, parameters are in this order: radius, longitude, latitude -const char* WEATHER_URL="https://aviationweather.gov/cgi-bin/data/dataserver.php?requestType=retrieve&dataSource=metars&format=xml&hoursBeforeNow=2&mostRecent=true&boundingBox=%.2f,%.2f,%.2f,%.2f&fields=raw_text,station_id,latitude,longitude,altim_in_hg"; +/// Represents access to dataRefs +struct XDR { + XPLMDataRef dr = NULL; ///< handle to the dataRef + bool find (const char* sDR) ///< find the dataRef, return if found + { return (dr = XPLMFindDataRef(sDR)) != NULL; } + bool good () const { return dr != NULL; } +}; -/// Weather search radius (increment) to use if the initial weather request came back empty -constexpr float ADD_WEATHER_RADIUS_NM = 100.0f; -/// How often to add up ADD_WEATHER_RADIUS_NM before giving up? -constexpr long MAX_WEATHER_RADIUS_FACTOR = 5; +/// Represents a float dataRef +struct XDR_float : public XDR { + float get () const { return XPLMGetDataf(dr); } ///< read the float value + operator float () const { return get(); } ///< read the float value + void set (float v) { if (std::isfinite(v)) XPLMSetDataf(dr, v); } ///< set the float value + XDR_float& operator = (float v) { set(v); return *this; } +}; -/// suppress further error message as we had enough already? -bool gbSuppressWeatherErrMsg = false; +/// Represents a float array dataRef +template +struct XDR_farr : public XDR { + void get (std::array& v) const { XPLMGetDatavf(dr,v.data(),0,N ); } ///< get data from X-Plane into local storage + void set (const std::array & v) ///< write data to X-Plane (if element 0 is finite, ie. not NAN) + { + // Set only if _any_ of the elements is finite + if (std::any_of(v.begin(), v.end(), [](float f){return std::isfinite(f);})) { + // but we must not set any non-finite values, that can crash XP or other plugins, + // so we make a copy that transforms all non-finite values to 0.0 + std::array cv; + std::transform(v.begin(), v.end(), cv.begin(), + [](const float f){ return std::isfinite(f) ? f : 0.0f; }); + // and then send that copy to XP: + XPLMSetDatavf(dr,cv.data(),0,N ); + } + } +}; -// Error messages -#define ERR_WEATHER_ERROR "Weather request returned with error: %s" -#define INFO_NO_NEAR_WEATHER "Found no nearby weather in a %.fnm radius" -#define ERR_NO_WEATHER "Found no weather in a %.fnm radius, giving up" -#define INFO_FOUND_WEATHER_AGAIN "Successfully updated weather again from %s" +/// Represents an int dataRef +struct XDR_int : public XDR { + int get () const { return XPLMGetDatai(dr); } ///< read the int value + operator int () const { return get(); } ///< read the int value + void set (int v) {if(v >= 0) XPLMSetDatai(dr, v);} ///< set the int value (assuming only non-negative are valid) + XDR_int& operator = (int v) { set(v); return *this; } +}; + + +// Weather Region dataRefs +XDR_float wdr_visibility_reported_sm; ///< float y statute_miles = 0. The reported visibility (e.g. what the METAR/weather window says). +XDR_float wdr_sealevel_pressure_pas; ///< float y pascals Pressure at sea level, current planet +XDR_float wdr_sealevel_temperature_c; ///< float y degreesC The temperature at sea level. +XDR_float wdr_qnh_base_elevation; ///< float y float Base elevation for QNH. Takes into account local physical variations from a spheroid. +XDR_float wdr_qnh_pas; ///< float y float Base elevation for QNH. Takes into account local physical variations from a spheroid. +XDR_float wdr_rain_percent; ///< float y ratio [0.0 - 1.0] The percentage of rain falling. +XDR_int wdr_change_mode; ///< int y enum How the weather is changing. 0 = Rapidly Improving, 1 = Improving, 2 = Gradually Improving, 3 = Static, 4 = Gradually Deteriorating, 5 = Deteriorating, 6 = Rapidly Deteriorating, 7 = Using Real Weather +XDR_int wdr_weather_source; ///< int n enum What system is currently controlling the weather. 0 = Preset, 1 = Real Weather, 2 = Controlpad, 3 = Plugin. +XDR_int wdr_update_immediately; ///< int y bool If this is true, any weather region changes EXCEPT CLOUDS will take place immediately instead of at the next update interval (currently 60 seconds). +XDR_farr<13> wdr_atmosphere_alt_levels_m; ///< float[13] n meters The altitudes for the thirteen atmospheric layers returned in other sim/weather/region datarefs. +XDR_farr<13> wdr_wind_altitude_msl_m; ///< float[13] y meters >= 0. The center altitude of this layer of wind in MSL meters. +XDR_farr<13> wdr_wind_speed_msc; ///< float[13] y kts >= 0. The wind speed in knots. +XDR_farr<13> wdr_wind_direction_degt; ///< float[13] y degrees [0 - 360] The direction the wind is blowing from in degrees from true north clockwise. +XDR_farr<13> wdr_shear_speed_msc; ///< float[13] y kts >= 0. The gain from the shear in knots. +XDR_farr<13> wdr_shear_direction_degt; ///< float[13] y degrees [0 - 360]. The direction for a wind shear, per above. +XDR_farr<13> wdr_turbulence; ///< float[13] y float [0.0 - 1.0] A turbulence factor, 0-10, the unit is just a scale. +XDR_farr<13> wdr_dewpoint_deg_c; ///< float[13] y degreesC The dew point at specified levels in the atmosphere. +XDR_farr<13> wdr_temperature_altitude_msl_m;///< float[13] y meters >= 0. Altitudes used for the temperatures_aloft_deg_c array. +XDR_farr<13> wdr_temperatures_aloft_deg_c; ///< float[13] y degreesC Temperature at pressure altitudes given in sim/weather/region/atmosphere_alt_levels. If the surface is at a higher elevation, the ISA difference at wherever the surface is is assumed to extend all the way down to sea level. +XDR_farr<3> wdr_cloud_type; ///< float[3] y float Blended cloud types per layer. 0 = Cirrus, 1 = Stratus, 2 = Cumulus, 3 = Cumulo-nimbus. Intermediate values are to be expected. +XDR_farr<3> wdr_cloud_coverage_percent; ///< float[3] y float Cloud coverage per layer, range 0 - 1. +XDR_farr<3> wdr_cloud_base_msl_m; ///< float[3] y meters MSL >= 0. The base altitude for this cloud layer. +XDR_farr<3> wdr_cloud_tops_msl_m; ///< float[3] y meters >= 0. The tops for this cloud layer. +XDR_float wdr_tropo_temp_c; ///< float y degreesC Temperature at the troposphere +XDR_float wdr_tropo_alt_m; ///< float y meters Altitude of the troposphere +XDR_float wdr_thermal_rate_ms; ///< float y m/s >= 0 The climb rate for thermals. +XDR_float wdr_wave_amplitude; ///< float y meters Amplitude of waves in the water (height of waves) +XDR_float wdr_wave_length; ///< float n meters Length of a single wave in the water - not writable starting in v12 +XDR_float wdr_wave_speed; ///< float n m/s Speed of water waves - not writable starting in v12 +XDR_float wdr_wave_dir; ///< float y degrees Direction of waves. +XDR_float wdr_runway_friction; ///< int/float y enum The friction constant for runways (how wet they are). Dry = 0, wet(1-3), puddly(4-6), snowy(7-9), icy(10-12), snowy/icy(13-15) +XDR_float wdr_variability_pct; ///< float y ratio How randomly variable the weather is over distance. Range 0 - 1. +XDR_int wdr_weather_preset; ///< int y enum Read the UI weather preset that is closest to the current conditions, or set an UI preset. Clear(0), VFR Few(1), VFR Scattered(2), VFR Broken(3), VFR Marginal(4), IFR Non-precision(5), IFR Precision(6), Convective(7), Large-cell Storms(8) + +/// @brief XP's weather change modes +/// @details 0 = Rapidly Improving, 1 = Improving, 2 = Gradually Improving, 3 = Static, 4 = Gradually Deteriorating, 5 = Deteriorating, 6 = Rapidly Deteriorating, 7 = Using Real Weather +enum wdrChangeModeTy : int { + WDR_CM_RAPIDLY_IMPROVING = 0, ///< 0 = Rapidly Improving + WDR_CM_IMPROVING, ///< 1 = Improving + WDR_CM_GRADUALLY_IMPROVING, ///< 2 = Gradually Improving + WDR_CM_STATIC, ///< 3 = Static + WDR_CM_GRADUALLY_DETERIORATING, ///< 4 = Gradually Deteriorating + WDR_CM_DETERIORATING, ///< 5 = Deteriorating + WDR_CM_RAPIDLY_DETERIORATING, ///< 6 = Rapidly Deteriorating + WDR_CM_REAL_WEATHER, ///< 7 = Using Real Weather +}; + +/// @brief XP's cloud types +/// @details 0 = Cirrus, 1 = Stratus, 2 = Cumulus, 3 = Cumulo-nimbus. Intermediate values are to be expected. +constexpr float WDR_CT_CIRRUS = 0.0f; ///< 0 = Cirrus +constexpr float WDR_CT_STRATUS = 1.0f; ///< 1 = Stratus +constexpr float WDR_CT_CUMULUS = 2.0f; ///< 2 = Cumulus +constexpr float WDR_CT_CUMULONIMBUS = 3.0f; ///< 3 = Cumulo-nimbus + +bool WeatherInitDataRefs () +{ + return true + && wdr_visibility_reported_sm .find("sim/weather/region/visibility_reported_sm") + && wdr_sealevel_pressure_pas .find("sim/weather/region/sealevel_pressure_pas") + && wdr_sealevel_temperature_c .find("sim/weather/region/sealevel_temperature_c") + && wdr_qnh_base_elevation .find("sim/weather/region/qnh_base_elevation") + && wdr_qnh_pas .find("sim/weather/region/qnh_pas") + && wdr_rain_percent .find("sim/weather/region/rain_percent") + && wdr_change_mode .find("sim/weather/region/change_mode") + && wdr_weather_source .find("sim/weather/region/weather_source") + && wdr_update_immediately .find("sim/weather/region/update_immediately") + && wdr_atmosphere_alt_levels_m .find("sim/weather/region/atmosphere_alt_levels_m") + && wdr_wind_altitude_msl_m .find("sim/weather/region/wind_altitude_msl_m") + && wdr_wind_speed_msc .find("sim/weather/region/wind_speed_msc") + && wdr_wind_direction_degt .find("sim/weather/region/wind_direction_degt") + && wdr_shear_speed_msc .find("sim/weather/region/shear_speed_msc") + && wdr_shear_direction_degt .find("sim/weather/region/shear_direction_degt") + && wdr_turbulence .find("sim/weather/region/turbulence") + && wdr_dewpoint_deg_c .find("sim/weather/region/dewpoint_deg_c") + && wdr_temperature_altitude_msl_m .find("sim/weather/region/temperature_altitude_msl_m") + && wdr_temperatures_aloft_deg_c .find("sim/weather/region/temperatures_aloft_deg_c") + && wdr_cloud_type .find("sim/weather/region/cloud_type") + && wdr_cloud_coverage_percent .find("sim/weather/region/cloud_coverage_percent") + && wdr_cloud_base_msl_m .find("sim/weather/region/cloud_base_msl_m") + && wdr_cloud_tops_msl_m .find("sim/weather/region/cloud_tops_msl_m") + && wdr_tropo_temp_c .find("sim/weather/region/tropo_temp_c") + && wdr_tropo_alt_m .find("sim/weather/region/tropo_alt_m") + && wdr_thermal_rate_ms .find("sim/weather/region/thermal_rate_ms") + && wdr_wave_amplitude .find("sim/weather/region/wave_amplitude") + && wdr_wave_length .find("sim/weather/region/wave_length") + && wdr_wave_speed .find("sim/weather/region/wave_speed") + && wdr_wave_dir .find("sim/weather/region/wave_dir") + && wdr_runway_friction .find("sim/weather/region/runway_friction") + && wdr_variability_pct .find("sim/weather/region/variability_pct") + && wdr_weather_preset .find("sim/weather/region/weather_preset") + ; +} + +// Constructor sets all arrays to all `NAN` +LTWeather::LTWeather() +{ + wdr_atmosphere_alt_levels_m.get(atmosphere_alt_levels_m); + wind_altitude_msl_m.fill(NAN); + wind_speed_msc.fill(NAN); + wind_direction_degt.fill(NAN); + shear_speed_msc.fill(NAN); + shear_direction_degt.fill(NAN); + turbulence.fill(NAN); + dewpoint_deg_c.fill(NAN); + temperature_altitude_msl_m.fill(NAN); + temperatures_aloft_deg_c.fill(NAN); + cloud_type.fill(NAN); + cloud_coverage_percent.fill(NAN); + cloud_base_msl_m.fill(NAN); + cloud_tops_msl_m.fill(NAN); +} -/// return the value between two xml tags -std::string GetXMLValue (const std::string& _r, const std::string& _tag, - std::string::size_type& pos) +// Set the given weather in X-Plane +void LTWeather::Set () const { - // find the tag - std::string::size_type p = _r.find(_tag, pos); - if (p == std::string::npos) // didn't find it - return ""; + wdr_update_immediately.set(update_immediately); + + wdr_change_mode.set(WDR_CM_STATIC); // 3 - Static (this also switches off XP's real weather) + + wdr_visibility_reported_sm.set(visibility_reported_sm); + wdr_sealevel_temperature_c.set(sealevel_temperature_c); + wdr_qnh_base_elevation.set(qnh_base_elevation); + if (!std::isnan(qnh_pas)) // prefer QNH as it is typically as reported by METAR + wdr_qnh_pas.set(qnh_pas); + else + wdr_sealevel_pressure_pas.set(sealevel_pressure_pas); + wdr_rain_percent.set(rain_percent); + wdr_wind_altitude_msl_m.set(wind_altitude_msl_m); + wdr_wind_speed_msc.set(wind_speed_msc); + wdr_wind_direction_degt.set(wind_direction_degt); + wdr_shear_speed_msc.set(shear_speed_msc); + wdr_shear_direction_degt.set(shear_direction_degt); + wdr_turbulence.set(turbulence); + wdr_dewpoint_deg_c.set(dewpoint_deg_c); + wdr_temperature_altitude_msl_m.set(temperature_altitude_msl_m); + wdr_temperatures_aloft_deg_c.set(temperatures_aloft_deg_c); + wdr_cloud_type.set(cloud_type); + wdr_cloud_coverage_percent.set(cloud_coverage_percent); + wdr_cloud_base_msl_m.set(cloud_base_msl_m); + wdr_cloud_tops_msl_m.set(cloud_tops_msl_m); + wdr_tropo_temp_c.set(tropo_temp_c); + wdr_tropo_alt_m.set(tropo_alt_m); + wdr_thermal_rate_ms.set(thermal_rate_ms); + wdr_wave_amplitude.set(wave_amplitude); + wdr_wave_dir.set(wave_dir); + wdr_runway_friction.set(float(runway_friction)); + wdr_variability_pct.set(variability_pct); + + if (dataRefs.ShallLogWeather()) + Log(update_immediately ? "Set Weather immediately" : "Set Weather"); +} + +// Read weather from X-Plane +void LTWeather::Get (const std::string& logMsg) +{ + visibility_reported_sm = wdr_visibility_reported_sm.get(); + sealevel_pressure_pas = wdr_sealevel_pressure_pas.get(); + sealevel_temperature_c = wdr_sealevel_temperature_c.get(); + qnh_base_elevation = wdr_qnh_base_elevation.get(); + qnh_pas = wdr_qnh_pas.get(); + rain_percent = wdr_rain_percent.get(); + wdr_atmosphere_alt_levels_m.get(atmosphere_alt_levels_m); + wdr_wind_altitude_msl_m.get(wind_altitude_msl_m); + wdr_wind_speed_msc.get(wind_speed_msc); + wdr_wind_direction_degt.get(wind_direction_degt); + wdr_shear_speed_msc.get(shear_speed_msc); + wdr_shear_direction_degt.get(shear_direction_degt); + wdr_turbulence.get(turbulence); + wdr_dewpoint_deg_c.get(dewpoint_deg_c); + wdr_temperature_altitude_msl_m.get(temperature_altitude_msl_m); + wdr_temperatures_aloft_deg_c.get(temperatures_aloft_deg_c); + wdr_cloud_type.get(cloud_type); + wdr_cloud_coverage_percent.get(cloud_coverage_percent); + wdr_cloud_base_msl_m.get(cloud_base_msl_m); + wdr_cloud_tops_msl_m.get(cloud_tops_msl_m); + tropo_temp_c = wdr_tropo_temp_c.get(); + tropo_alt_m = wdr_tropo_alt_m.get(); + thermal_rate_ms = wdr_thermal_rate_ms.get(); + wave_amplitude = wdr_wave_amplitude.get(); + wave_dir = wdr_wave_dir.get(); + runway_friction = (int)std::lround(wdr_runway_friction.get()); + variability_pct = wdr_variability_pct.get(); + update_immediately = bool(wdr_update_immediately.get()); + change_mode = wdr_change_mode.get(); + weather_source = wdr_weather_source.get(); + weather_preset = wdr_weather_preset.get(); - // find the beginning of the _next_ tag (we don't validate any further) - const std::string::size_type startPos = p + _tag.size(); - pos = _r.find('<', startPos); // where the end tag begins - if (pos != std::string::npos) - return _r.substr(startPos, pos-startPos); - else { - pos = 0; // we overwrite pos with npos...reset to buffer's beginning for next search - return ""; + pos = dataRefs.GetUsersPlanePos(); + + if (!logMsg.empty()) + Log(logMsg); + else if (dataRefs.ShallLogWeather()) + Log("Got Weather"); +} + +// Log values to Log.txt +void LTWeather::Log (const std::string& msg) const +{ + std::ostringstream lOut; + lOut << std::fixed << std::setprecision(1); + lOut << "pos: " << std::string(pos) << "\n"; + lOut << "vis: " << visibility_reported_sm << "sm, "; + lOut << "sea_pressure: "<< sealevel_pressure_pas << "pas, "; + lOut << "sea_temp: " << sealevel_temperature_c << "C, "; + lOut << "qnh_base_elev: "<= 0 || weather_source >= 0 || weather_preset >= 0) { + lOut << "immediately: " << update_immediately << ", "; + lOut << "change mode: " << change_mode << ", "; + lOut << "source: " << weather_source << ", "; + lOut << "preset: " << weather_preset << "\n"; } + + lOut << "METAR: " << (metar.empty() ? "(nil)" : metar.c_str()) << "\n"; + + LOG_MSG(logDEBUG, "%s\n%s", msg.c_str(), lOut.str().c_str()); } +// Compute interpolation settings to fill one array from a differently sized one +std::array LTWeather::ComputeInterpol (const std::vector& from, + const std::array& to) +{ + std::array ret; + for (size_t i = 0; i < to.size(); ++i) // for each element of the destination + { + const float f = to[i]; + if (f <= from.front()) { // Smaller than even lowest `from` value->use first value only + ret[i].i = 0; + ret[i].w = 1.0f; + } + else if (f >= from.back()) { // Larger than even largest `from` value + ret[i].i = from.size()-2; // Use only last value + ret[i].w = 0.0f; // (Start with 2nd last index, but with weight 0, that means, use the next index, the last one, with weight 1) + } + else { + // Search for f in `from`, find where it would fit inbetween + // (as border cases are covered above must find something) + const auto iter = std::adjacent_find(from.begin(), from.end(), + [f](const float a, const float b) + { return a <= f && f <= b; }); + if(iter != from.end()) { + ret[i].i = (size_t)std::distance(from.begin(), iter); + ret[i].w = 1.0f - (f - *iter)/(*(iter+1) - *iter); + } + } + } + + return ret; +} + +// Fill values from a differently sized input vector based on interpolation +void LTWeather::Interpolate (const std::array& aInterpol, + const std::vector& from, + std::array& to) +{ + for (size_t i = 0; i < aInterpol.size(); ++i) { + const InterpolSet& is = aInterpol[i]; + if (is.i+1 < from.size()) { + to[i] = from[is.i] * is.w + from[is.i+1] * (1-is.w); + } + else to[i] = NAN; + } +} + +// Fill directions/headings from a differently sized input vector based on interpolation +// Headings need to be interpolated separately as the average of 359 and 001 is 000 rather than 180... +void LTWeather::InterpolateDir (const std::array& aInterpol, + const std::vector& from, + std::array& to) +{ + for (size_t i = 0; i < aInterpol.size(); ++i) { + const InterpolSet& is = aInterpol[i]; + if (is.i+1 < from.size()) { + double h1 = HeadingNormalize(from[is.i]); + double h2 = HeadingNormalize(from[is.i+1]); + const double hDiff = h1 - h2; + if (hDiff > 180.0) // case of 359 and 001 + h2 += 360.0; + else if (hDiff < -180.0) // case of 001 and 359 + h1 += 360.0; + to[i] = (float)HeadingNormalize(h1 * is.w + h2 * (1-is.w)); + } + else to[i] = NAN; + } +} + +// Get interpolated value for a given altitude +float LTWeather::GetInterpolated (const std::array& levels_m, + const std::array& vals, + float alt_m) +{ + // Smaller than lowest altitude? + if (alt_m <= levels_m.front()) return vals.front(); + // Find if it's something inbetween + for (size_t i = 0; i < levels_m.size() - 1; ++i) { + if (levels_m[i] <= alt_m && alt_m <= levels_m[i+1]) { + const float w = (alt_m - levels_m[i]) / (levels_m[i+1] - levels_m[i]); // Weight: How close are we to the i+1'th value? + return (1.0f-w) * vals[i] + w * vals[i+1]; + } + } + // must be larger than last + return vals.back(); +} + +// Fill value equally up to given altitude +void LTWeather::FillUp (const std::array& levels_m, + std::array& to, + float alt_m, + float val, + bool bInterpolateNext) +{ + std::size_t i = 0; + for (; i < levels_m.size() && levels_m[i] <= alt_m; ++i) // while level is less than alt_m set value + to[i] = val; + // i now points to first element _after_ those we set to 'val' + if (bInterpolateNext && i >= 1 && i+1 < levels_m.size()) { // to align with values above interpolate the next value + to[i] = to[i-1] + (to[i+1]-to[i-1])*(levels_m[i]-levels_m[i-1])/(levels_m[i+1]-levels_m[i-1]); + } +} + +// Fill value equally to the given minimum up to given altitude (ie. don't overwrite values that are already larger) +void LTWeather::FillUpMin (const std::array& levels_m, + std::array& to, + float alt_m, + float valMin, + bool bInterpolateNext) +{ + std::size_t i = 0; + bool bSetLastVal = false; // did we set/modify the last value? + for (; i < levels_m.size() && levels_m[i] <= alt_m; ++i) // while level is less than alt_m set value + if ((bSetLastVal = to[i] < valMin)) + to[i] = valMin; + // i now points to first element _after_ those we set to 'val' + if (bInterpolateNext && bSetLastVal && i >= 1 && i+1 < levels_m.size()) { // to align with values above interpolate the next value + to[i] = to[i-1] + (to[i+1]-to[i-1])*(levels_m[i]-levels_m[i-1])/(levels_m[i+1]-levels_m[i-1]); + } +} + +// +// MARK: Parse METAR +// + +using namespace metaf; + +constexpr float CAVOK_VISIBILITY_M = 10000.0f; ///< CAVOK minimum Visibility [m] +constexpr float CAVOK_MIN_CLOUD_BASE_M = 1500.0f; ///< CAVOK: no clouds below this height AGL [m] +constexpr float NSC_MIN_CLOUD_BASE_M = 6000.0f * float(M_per_FT); ///< NSC: no clouds below this height AGL [m] +constexpr float CLR_MIN_CLOUD_BASE_M = 12000.0f * float(M_per_FT); ///< CLR: no clouds below this height AGL [m] +constexpr float SKC_MIN_CLOUD_BASE_M = FLT_MAX; ///< SKC: no clouds at all +constexpr float CIRRUS_ABOVE_M = 7000.0f; ///< If unsure, use Cirrus above this altitude [m] +constexpr float CUMULUS_BELOW_M = 2000.0f; ///< If unsure, use Cumulus below this altitude [m] +constexpr float RAIN_DRIZZLE = 0.1f; ///< rain in case of DRIZZLE +constexpr int RAIN_DRIZZLE_FRIC = 1; ///< rwy friction in case of light rain +constexpr float RAIN_LIGHT = 0.3f; ///< light rain +constexpr int RAIN_LIGHT_FRIC = 2; ///< rwy friction in case of light rain +constexpr float RAIN_MODERATE = 0.5f; ///< moderate rain +constexpr int RAIN_MODERATE_FRIC = 3; ///< rwy friction in case of moderate rain +constexpr float RAIN_HEAVY = 0.9f; ///< heavy rain +constexpr int RAIN_HEAVY_FRIC = 5; ///< rwy friction in case of light rain +constexpr float SPRAY_HEIGHT_M = 10.0f; ///< height AGL up to which we simulate SPRAY +constexpr float MIST_MAX_VISIBILITY_SM = 7.0f; ///< max visibility in case of MIST +constexpr float FOG_MAX_VISIBILITY_SM = 5.f/8.f; ///< max visibility in case of FOG +constexpr float TEMP_RWY_ICED = -7.5f; ///< temperature under which we consider rwy icy + +/// @brief metaf visitor, ie. functions that are called when traversing the parsed METAR +/// @details Set of visit functions that perform the actual processing of the information in the METAR. +/// Each function returns a `bool`, which reads "Shall processing be stopped?" +class LTWeatherVisitor : public Visitor { +public: + LTWeather& w; ///< The weather we are to modify + float fieldAlt_m = 0.0f; ///< field altitude in meter + bool bCloseToGnd = false; ///< is position close enough to ground to weigh METAR higher than weather data? + size_t iCloud = 0; ///< which cloud layer is to be filled next? + int bThunderstorms = 0; ///< thunderstorms anywhere? (0-None, 1-Light, 2-Normal, 3-Heavy) -> PostProcessing() makes sure CB exists and adds turbulence + bool bRwyFrictionDefined = false; ///< Rwy friction defined based on Rwy State Group in METAR, don't touch again in PostProcessing() + +public: + /// Constructor sets the reference to the weather that we are to modify + LTWeatherVisitor (LTWeather& weather) : w(weather) {} + + /// @brief Convert cloud cover to X-Plane percentage + /// @note We aren't using metaf's CloudType::okta() function because it returns the _highest_ possible okta value, + /// we want the average. + static float toXPCloudCover (const CloudGroup& cg) + { + switch (cg.amount()) { + case CloudGroup::Amount::NOT_REPORTED: // various ways of saying 'none' + case CloudGroup::Amount::NCD: + case CloudGroup::Amount::NSC: + case CloudGroup::Amount::NONE_CLR: + case CloudGroup::Amount::NONE_SKC: + case CloudGroup::Amount::OBSCURED: // obscured, clouds aren't visible, so we don't know + return 0.0f; + case CloudGroup::Amount::FEW: // Few clouds (1/8 to 2/8 sky covered). + return 1.5f/8.0f; + case CloudGroup::Amount::VARIABLE_FEW_SCATTERED: // Cloud cover is variable between FEW and SCATTERED -> between 1/8 and 4/8 + return 2.5f/8.0f; + case CloudGroup::Amount::SCATTERED: // Scattered clouds (3/8 to 4/8 sky covered). + return 3.5f/8.0f; + case CloudGroup::Amount::VARIABLE_SCATTERED_BROKEN: // Cloud cover is variable between SCATTERED and BROKEN -> between 3/8 and 7/8 + return 5.0f/8.0f; + case CloudGroup::Amount::BROKEN: // Broken clouds (5/8 to 7/8 sky covered). + return 6.0f/8.0f; + case CloudGroup::Amount::VARIABLE_BROKEN_OVERCAST: // Cloud cover is variable between BROKEN and OVERCAST -> between 5/8 and 8/8 + return 6.5f/8.0f; + case CloudGroup::Amount::OVERCAST: // Overcast (8/8 sky covered) + return 8.0f/8.0f; + } + return 0.0f; + } + + /// @brief Guess cloud type by altitude + /// @details This isn't perfect, for sure, but cloud types differ high up from down below in general. + /// XP basically defines 0=Cirrus, 1=Stratus, 2=Cumulus. + /// We make use of that to say: + /// > 7000m -> Cirrus + /// < 2000m -> Cumulus + /// and inbetween we interpolate, which also mixes in Stratus in the middle layers + float cloudTypeByAlt (const CloudGroup& cg) + { + float base_m = cg.height().toUnit(Distance::Unit::METERS).value_or(NAN); + if (std::isnan(base_m)) return 2.0f; + base_m += fieldAlt_m; + if (base_m <= CUMULUS_BELOW_M) return WDR_CT_CUMULUS; // Cumulus below 2000m + if (base_m >= CIRRUS_ABOVE_M) return WDR_CT_CIRRUS; // Cirrus above 7000m + // inbetween interpolate in a linear fashion + base_m -= CUMULUS_BELOW_M; + base_m /= (CIRRUS_ABOVE_M - CUMULUS_BELOW_M) / (WDR_CT_CUMULUS - WDR_CT_CIRRUS); + return (WDR_CT_CUMULUS - WDR_CT_CIRRUS) - base_m; + } + + /// Convert a METAR cloud type to X-Plane + float toXPCloudType (const CloudGroup& cg) + { + const CloudGroup::ConvectiveType convTy = cg.convectiveType(); + if (convTy == CloudGroup::ConvectiveType::CUMULONIMBUS) + return 3.0f; + if (convTy == CloudGroup::ConvectiveType::TOWERING_CUMULUS) + return 2.5f; + + const std::optional& optClTy = cg.cloudType(); + if (!optClTy) // if nothing specified, go for Cirrus + return cloudTypeByAlt(cg); + switch (optClTy.value().type()) { + case CloudType::Type::NOT_REPORTED: return cloudTypeByAlt(cg); + //Low clouds + case CloudType::Type::CUMULONIMBUS: return WDR_CT_CUMULONIMBUS; + case CloudType::Type::TOWERING_CUMULUS: return 2.5f; + case CloudType::Type::CUMULUS: return WDR_CT_CUMULUS; + case CloudType::Type::CUMULUS_FRACTUS: return 1.8f; + case CloudType::Type::STRATOCUMULUS: return 1.5f; + case CloudType::Type::NIMBOSTRATUS: + case CloudType::Type::STRATUS: return WDR_CT_STRATUS; + case CloudType::Type::STRATUS_FRACTUS: return 0.8f; + //Med clouds + case CloudType::Type::ALTOSTRATUS: return WDR_CT_STRATUS; + case CloudType::Type::ALTOCUMULUS: return WDR_CT_CUMULUS; + case CloudType::Type::ALTOCUMULUS_CASTELLANUS: return 2.3f; + //High clouds + case CloudType::Type::CIRRUS: return WDR_CT_CIRRUS; + case CloudType::Type::CIRROSTRATUS: return 0.5; + case CloudType::Type::CIRROCUMULUS: return 1.5f; + //Obscurations + default: return WDR_CT_CIRRUS; + } + } + + /// Remove a given cloud layer + void RemoveClouds (size_t i) + { + LOG_ASSERT(i < w.cloud_type.size()); + w.cloud_type[i] = -1.0f; + w.cloud_base_msl_m[i] = -1.0f; + w.cloud_tops_msl_m[i] = -1.0f; + w.cloud_coverage_percent[i] = 0.0f; + } + + /// @brief Remove clouds up to given height AGL, remove thunderstorm clouds upon request + /// @details Called with varyiing parameters for SKC, CLR, CAVOK, NSC, NCD + void ReduceClouds (float maxHeight_m, bool bRemoveTSClouds) + { + // No cloud below `maxHeight_m` + maxHeight_m += fieldAlt_m; // add field altitude to get altitude + for (size_t i = 0; i < w.cloud_base_msl_m.size(); ++i) { + if (w.cloud_tops_msl_m[i] < maxHeight_m) // entire cloud layer too low -> remove + RemoveClouds(i); + else if (w.cloud_base_msl_m[i] < maxHeight_m) // only base too low? -> lift base, but keep tops (and hence the layer as such) + w.cloud_base_msl_m[i] = maxHeight_m; + } + // No cumulonimbus or towering cumulus clouds + if (bRemoveTSClouds) + for (float& ct: w.cloud_type) + if (ct > WDR_CT_CUMULUS) ct = WDR_CT_CUMULUS; // reduce Cumulo-nimbus to Cumulus + } + + // --- visit functions --- + + /// Location, ie. the ICAO code, for which we fetch position / altitude + bool visitLocationGroup(const LocationGroup & lg, + ReportPart, const std::string&) override + { + w.metarFieldIcao = lg.toString(); + w.posMetarField = GetAirportLoc(w.metarFieldIcao); // determin the field's location and especially altitude + fieldAlt_m = float(w.posMetarField.alt_m()); // save field's altitude for easier access in later visitor functions + if (std::isnan(fieldAlt_m)) { + fieldAlt_m = 0.0f; + LOG_MSG(logWARN, "Couldn't determine altitude of field '%s', clouds may appear too low", lg.toString().c_str()); + } + + // If we don't have a position, then use the field's position + if (!w.pos.hasPosAlt()) + w.pos = w.posMetarField; + + // Are we flying/viewing "close" to ground so that we prefer METAR data? + bCloseToGnd = float(w.pos.alt_m()) < fieldAlt_m + dataRefs.GetWeatherMaxMetarHeight_m(); + + return false; + } + + /// Keyword: If CAVOK then change weather accordingly + bool visitKeywordGroup(const KeywordGroup & kwg, + ReportPart, const std::string&) override + { + if (kwg.type() == KeywordGroup::Type::CAVOK) { + // Visibility 10 km or more in all directions. + if (std::isnan(w.visibility_reported_sm) || + w.visibility_reported_sm * float(M_per_SM) < CAVOK_VISIBILITY_M) + w.visibility_reported_sm = CAVOK_VISIBILITY_M / float(M_per_SM); + // No clouds below 1.500m AGL, no CB/TCU + ReduceClouds(CAVOK_MIN_CLOUD_BASE_M, true); + } + return false; + } + + /// Trend Groups: Whatever the details: As we are only interested in _current_ weather we stop processing once reaching any trend group + bool visitTrendGroup(const TrendGroup &, ReportPart, const std::string&) override + { return true; } + + /// Clouds + bool visitCloudGroup(const CloudGroup & cg, + ReportPart, const std::string&) override + { + switch (cg.type()) { + case CloudGroup::Type::NO_CLOUDS: { + // There are various ways of saying "no clouds" an they all mean a bit a different thing: + switch (cg.amount()) { + case CloudGroup::Amount::NSC: + ReduceClouds(NSC_MIN_CLOUD_BASE_M, true); + break; + case CloudGroup::Amount::NONE_CLR: + ReduceClouds(CLR_MIN_CLOUD_BASE_M, false); + break; + case CloudGroup::Amount::NONE_SKC: + ReduceClouds(SKC_MIN_CLOUD_BASE_M, true); + break; + default: + // not touching cloud information + break; + } + break; + } + case CloudGroup::Type::CLOUD_LAYER: { + float base_m = cg.height().toUnit(Distance::Unit::METERS).value_or(NAN); + if (std::isnan(base_m)) break; // can't work without cloud base height + + // XP can only do 3 layers...if there are more in the METAR then shift/add layers until _one_ is above camera + if (iCloud >= w.cloud_type.size()) { + // is last layer _below_ camera altitude? + if (w.cloud_base_msl_m.back() < w.pos.alt_m()) { + // shift layers down, so the top-most layer becomes available + std::move(std::next(w.cloud_type.begin()), w.cloud_type.end(), w.cloud_type.begin()); + std::move(std::next(w.cloud_coverage_percent.begin()), w.cloud_coverage_percent.end(), w.cloud_coverage_percent.begin()); + std::move(std::next(w.cloud_base_msl_m.begin()), w.cloud_base_msl_m.end(), w.cloud_base_msl_m.begin()); + std::move(std::next(w.cloud_tops_msl_m.begin()), w.cloud_tops_msl_m.end(), w.cloud_tops_msl_m.begin()); + iCloud = w.cloud_type.size()-1; + } + } + + // Save the cloud layer, if there is still room in the cloud array + if (iCloud < w.cloud_type.size()) { + // check first if we can make out any coverage, no need to waste a cloud layer for 0 coverage + const float cover = toXPCloudCover(cg); + if (cover > 0.0f) { + w.cloud_coverage_percent[iCloud] = cover; + w.cloud_type[iCloud] = toXPCloudType(cg); + w.cloud_base_msl_m[iCloud] = fieldAlt_m + cg.height().toUnit(Distance::Unit::METERS).value_or(0.0f); + if (w.cloud_type[iCloud] < 2.5f) // non-convective cloud + w.cloud_tops_msl_m[iCloud] = w.cloud_base_msl_m[iCloud] + WEATHER_METAR_CLOUD_HEIGHT_M; + else // Cumulo-nimbus are higher + w.cloud_tops_msl_m[iCloud] = w.cloud_base_msl_m[iCloud] + WEATHER_METAR_CB_CLOUD_HEIGHT_M; + ++iCloud; + } + } + break; + } + + case CloudGroup::Type::VERTICAL_VISIBILITY: + case CloudGroup::Type::CEILING: + case CloudGroup::Type::VARIABLE_CEILING: + case CloudGroup::Type::CHINO: + case CloudGroup::Type::CLD_MISG: + case CloudGroup::Type::OBSCURATION: + break; + } + return false; + } + + /// Visibility, use only if close to ground + bool visitVisibilityGroup(const VisibilityGroup & vg, + ReportPart, const std::string&) override + { + if (bCloseToGnd) { + switch (vg.type()) { + case VisibilityGroup::Type::PREVAILING: + case VisibilityGroup::Type::PREVAILING_NDV: + case VisibilityGroup::Type::SURFACE: + case VisibilityGroup::Type::TOWER: + { + const std::optional v = vg.visibility().toUnit(Distance::Unit::STATUTE_MILES); + if (v.has_value()) + w.visibility_reported_sm = v.value(); + break; + } + + case VisibilityGroup::Type::VARIABLE_PREVAILING: + { + const std::optional v1 = vg.minVisibility().toUnit(Distance::Unit::STATUTE_MILES); + const std::optional v2 = vg.maxVisibility().toUnit(Distance::Unit::STATUTE_MILES); + if (v1.has_value() && v2.has_value()) + w.visibility_reported_sm = (v1.value() + v2.value()) / 2.0f; + break; + } + + default: + break; + } + } + return false; + } + + /// Weather group, for weather phenomena like RA, TS etc. + bool visitWeatherGroup(const WeatherGroup& wg, + ReportPart, const std::string&) override + { + // Only interested in CURRENT reports + if (wg.type() == WeatherGroup::Type::CURRENT) { + for (const WeatherPhenomena& wp: wg.weatherPhenomena()) { + if (wp.qualifier() == WeatherPhenomena::Qualifier::RECENT) + continue; // skip of RECENT phenomena, only interested in current + + // Thunderstorms + if (wp.descriptor() == WeatherPhenomena::Descriptor::THUNDERSTORM) { + switch (wp.qualifier()) { + case WeatherPhenomena::Qualifier::NONE: + case WeatherPhenomena::Qualifier::MODERATE: + bThunderstorms = 2; + break; + case WeatherPhenomena::Qualifier::LIGHT: + case WeatherPhenomena::Qualifier::VICINITY: + case WeatherPhenomena::Qualifier::RECENT: + bThunderstorms = 1; + break; + case WeatherPhenomena::Qualifier::HEAVY: + bThunderstorms = 3; + break; + } + } + + // Weather Phenomena + for (WeatherPhenomena::Weather wpw: wp.weather()) { + switch (wpw) { + case WeatherPhenomena::Weather::SPRAY: // mimic spray only _very_ close to ground + if (w.pos.alt_m() - fieldAlt_m > SPRAY_HEIGHT_M) + break; + [[fallthrough]]; // otherwise treat the same as drizzle + case WeatherPhenomena::Weather::DRIZZLE: + w.rain_percent = RAIN_DRIZZLE; + w.runway_friction = RAIN_DRIZZLE_FRIC; + break; + case WeatherPhenomena::Weather::RAIN: + case WeatherPhenomena::Weather::SNOW: + case WeatherPhenomena::Weather::SNOW_GRAINS: + case WeatherPhenomena::Weather::ICE_CRYSTALS: + case WeatherPhenomena::Weather::ICE_PELLETS: + case WeatherPhenomena::Weather::HAIL: + case WeatherPhenomena::Weather::SMALL_HAIL: + case WeatherPhenomena::Weather::UNDETERMINED: + if (wp.qualifier() == WeatherPhenomena::Qualifier::LIGHT) { + w.rain_percent = RAIN_LIGHT; + w.runway_friction = RAIN_LIGHT_FRIC; + } + else if (wp.qualifier() == WeatherPhenomena::Qualifier::HEAVY) { + w.rain_percent = RAIN_HEAVY; + w.runway_friction = RAIN_HEAVY_FRIC; + } + else { + w.rain_percent = RAIN_MODERATE; + w.runway_friction = RAIN_MODERATE_FRIC; + } + // Snowy/icy rwy friction + switch (wpw) { + case WeatherPhenomena::Weather::SNOW: + case WeatherPhenomena::Weather::SNOW_GRAINS: + w.runway_friction = (w.runway_friction-1)/2 + 7; // convert from wet/puddly [1..6] to snowy [7..9] + break; + case WeatherPhenomena::Weather::ICE_CRYSTALS: + case WeatherPhenomena::Weather::ICE_PELLETS: + w.runway_friction = (w.runway_friction-1)/2 + 10; // convert from wet/puddly [1..6] to icy [10..12] + break; + default: // just to silence compiler warnings + break; + } + break; + + // Limit visibility in case of MIST/FOG + case WeatherPhenomena::Weather::MIST: + if (w.visibility_reported_sm > MIST_MAX_VISIBILITY_SM) + w.visibility_reported_sm = MIST_MAX_VISIBILITY_SM; + break; + case WeatherPhenomena::Weather::FOG: + if (w.visibility_reported_sm > FOG_MAX_VISIBILITY_SM) + w.visibility_reported_sm = FOG_MAX_VISIBILITY_SM; + break; + + // everything else we don't process + default: + break; + } + } + } + } + return false; + } + + /// Pressure group, for QNH and SLP + bool visitPressureGroup(const PressureGroup& pg, + ReportPart, const std::string&) override + { + switch (pg.type()) { + case PressureGroup::Type::OBSERVED_QNH: + w.qnh_base_elevation = fieldAlt_m; + w.qnh_pas = pg.atmosphericPressure().toUnit(Pressure::Unit::HECTOPASCAL).value_or(NAN) * 100.0f; + break; + case PressureGroup::Type::OBSERVED_SLP: + w.sealevel_pressure_pas = pg.atmosphericPressure().toUnit(Pressure::Unit::HECTOPASCAL).value_or(NAN) * 100.0f; + break; + default: + break; + } + return false; + } + + + /// Temperatur group for air and surface temp + bool visitTemperatureGroup(const TemperatureGroup& tg, + ReportPart, const std::string&) override + { + if (tg.type() == TemperatureGroup::Type::TEMPERATURE_AND_DEW_POINT) { + // Temperatur: Fill the same temp all the way up to field altitude, which wouldn't be quite right...but under us is ground anyway + if (tg.airTemperature().temperature().has_value()) + w.FillUp(w.temperature_altitude_msl_m, w.temperatures_aloft_deg_c, + fieldAlt_m, + tg.airTemperature().toUnit(Temperature::Unit::C).value_or(NAN), + true); + // Dew Point: Fill the same temp all the way up to field altitude, which wouldn't be quite right...but under us is ground anyway + if (tg.dewPoint().temperature().has_value()) + w.FillUp(w.temperature_altitude_msl_m, w.dewpoint_deg_c, + fieldAlt_m, + tg.dewPoint().toUnit(Temperature::Unit::C).value_or(NAN), + true); + } + return false; + } + + /// Wind group + bool visitWindGroup(const WindGroup& wg, + ReportPart, const std::string&) override + { + switch (wg.type()) { + // process wind information + case WindGroup::Type::SURFACE_WIND: + case WindGroup::Type::SURFACE_WIND_CALM: + case WindGroup::Type::VARIABLE_WIND_SECTOR: + case WindGroup::Type::SURFACE_WIND_WITH_VARIABLE_SECTOR: + case WindGroup::Type::WIND_SHEAR: + { + // up to which altitude will we set wind values? + const float alt_m = fieldAlt_m + dataRefs.GetWeatherMaxMetarHeight_m(); + // Standard wind speed (can be 0 for calm winds) and direction + const float speed = wg.windSpeed().toUnit(Speed::Unit::METERS_PER_SECOND).value_or(0.0f); + const float dir = wg.direction().degrees().value_or(0.0f); + w.FillUp(w.wind_altitude_msl_m, w.wind_speed_msc, // set wind speed up to preferred METAR height AGL + alt_m, speed, true); + w.FillUp(w.wind_altitude_msl_m, w.wind_direction_degt, // set wind direction up to preferred METAR height AGL + alt_m, dir, false); // no interpolation...that can go wrong with headings + + // Variable wind direction -> transform into +/- degrees as expected by XP + const float begDir = wg.varSectorBegin().degrees().value_or(NAN); + const float endDir = wg.varSectorEnd().degrees().value_or(NAN); + const float halfDiff = !std::isnan(begDir) && !std::isnan(endDir) ? + float(HeadingDiff(begDir, endDir) / 2.0) : + 0.0f; + w.FillUp(w.wind_altitude_msl_m, w.shear_direction_degt, // set wind shear up to preferred METAR height AGL + alt_m, halfDiff, false); + + // Gust speed if given + float gust = wg.gustSpeed().toUnit(Speed::Unit::METERS_PER_SECOND).value_or(NAN); + if (std::isnan(gust)) gust = 0.0f; // if not given set 'gain from gust' to zeri + else gust -= speed; // if given, subtract normal wind speed, we need 'gain from gust' + w.FillUp(w.wind_altitude_msl_m, w.shear_speed_msc, // set wind shear gain up to preferred METAR height AGL + alt_m, gust, true); + + break; + } + + // don't process + case WindGroup::Type::WIND_SHEAR_IN_LOWER_LAYERS: + case WindGroup::Type::WIND_SHIFT: + case WindGroup::Type::WIND_SHIFT_FROPA: + case WindGroup::Type::PEAK_WIND: + case WindGroup::Type::WSCONDS: + case WindGroup::Type::WND_MISG: + break; + } + return false; + } + + /// Rwy State Group + bool visitRunwayStateGroup(const RunwayStateGroup& rsg, + ReportPart, const std::string&) override + { + // XP provides just one `runway_friction` dataRef, not one per runway, + // so we just process the first runway state group, assuming that + // the states of different runways on the same airport won't be much different anyway. + if (!bRwyFrictionDefined && rsg.deposits() != RunwayStateGroup::Deposits::NOT_REPORTED) { + bRwyFrictionDefined = true; + const float depth = rsg.depositDepth().toUnit(Precipitation::Unit::MM).value_or(0.0f); + int extend = 0; + switch (rsg.contaminationExtent()) { + case RunwayStateGroup::Extent::FROM_26_TO_50_PERCENT: + extend = 1; + break; + case RunwayStateGroup::Extent::MORE_THAN_51_PERCENT: + extend = 2; + break; + default: + extend = 0; + } + + switch (rsg.deposits()) { + case RunwayStateGroup::Deposits::NOT_REPORTED: + case RunwayStateGroup::Deposits::CLEAR_AND_DRY: + w.runway_friction = 0; + break; + case RunwayStateGroup::Deposits::DAMP: + w.runway_friction = 1; + break; + case RunwayStateGroup::Deposits::WET_AND_WATER_PATCHES: + // depth decides between wet (<2mm) and puddly (>=2mm) + w.runway_friction = (depth < 2 ? 1 : 4) + extend; + break; + case RunwayStateGroup::Deposits::ICE: + case RunwayStateGroup::Deposits::RIME_AND_FROST_COVERED: + case RunwayStateGroup::Deposits::FROZEN_RUTS_OR_RIDGES: + w.runway_friction = 10 + extend; // icy + break; + case RunwayStateGroup::Deposits::DRY_SNOW: + case RunwayStateGroup::Deposits::WET_SNOW: + case RunwayStateGroup::Deposits::SLUSH: + w.runway_friction = 7 + extend; // snowy + break; + case RunwayStateGroup::Deposits::COMPACTED_OR_ROLLED_SNOW: + w.runway_friction = 13 + extend; // snowy/icy + break; + } + } + return false; + } + + /// After finishing the processing, perform some cleanup + void PostProcessing () + { + // Runway friction + if (!bRwyFrictionDefined) { + if (w.runway_friction < 0) // don't yet have a runway friction? + // Rain causes wet status [0..7] + w.runway_friction = (int)std::lround(w.rain_percent * 6.f); + // Rwy Friction: Consider freezing if there is something's on the rwy that is not yet ice + const float t = w.GetInterpolated(w.temperature_altitude_msl_m, + w.temperatures_aloft_deg_c, + fieldAlt_m); + if (t <= TEMP_RWY_ICED && + 0 < w.runway_friction && w.runway_friction < 10) + { + // From water + if (1 <= w.runway_friction && w.runway_friction <= 6) + w.runway_friction = (w.runway_friction-1)/2 + 10; // convert from wet/puddly [1..6] to icy [10..12] + else + w.runway_friction += 3; // convert from snowy [7..9] to icy [10..12] + } + } + + // Cleanup up cloud layers: + if (iCloud > 0) { + // Anything beyond iCloud, that is lower than the last METAR layer, is to be removed + const float highestMetarBase = w.cloud_base_msl_m[iCloud-1]; + for (std::size_t i = iCloud; i < w.cloud_type.size(); ++i) { + if (w.cloud_base_msl_m[i] <= highestMetarBase) + RemoveClouds(i); + } + // Avoid overlapping cloud layer + for (std::size_t i = 0; i < w.cloud_type.size()-1; ++i) { + if (w.cloud_tops_msl_m[i] > w.cloud_base_msl_m[i+1] - WEATHER_MIN_CLOUD_HEIGHT_M) + // Minimum 100m height, but otherwise we try to keep 100m vertical distance from next layer + w.cloud_tops_msl_m[i] = std::max(w.cloud_base_msl_m[i] + WEATHER_MIN_CLOUD_HEIGHT_M, + w.cloud_base_msl_m[i+1] - WEATHER_MIN_CLOUD_HEIGHT_M); + } + } + + // Thunderstorms require a cumulo-nimbus somewhere + if (bThunderstorms > 0) { + // a cumulo-nimbus anywhere? + size_t iCB = SIZE_MAX; + for (size_t i = 0; iCB == SIZE_MAX && i < w.cloud_type.size(); ++i) + if (w.cloud_type[i] >= 2.5) + iCB = i; + + // if no CB yet, Need to turn one cloud layer to cmulu-numbus + for (size_t i = 0; iCB == SIZE_MAX && i < w.cloud_type.size(); ++i) { + // search for coverage of 25%, 50%, 75% depending on thunderstorm intensity + if (w.cloud_coverage_percent[i] > float(bThunderstorms)/4.0f) { + // set to 2.5, 2.75, 3.0 depending on thunderstorm intensity + w.cloud_type[i] = 2.25f + float(bThunderstorms)*0.25f; + iCB = i; + } + } + // if still no CB yet, turn first cloud layer to cmulu-numbus + for (size_t i = 0; iCB == SIZE_MAX && i < w.cloud_type.size(); ++i) { + // search for coverage of at least 10% + if (w.cloud_coverage_percent[i] >= 0.1f) { + // set to 2.5, 2.75, 3.0 depending on thunderstorm intensity + w.cloud_type[i] = 2.25f + float(bThunderstorms)*0.25f; + iCB = i; + } + } + + // Add turbulence until under the top of the CB cloud + w.FillUpMin(w.wind_altitude_msl_m, w.turbulence, + w.cloud_tops_msl_m[iCB], + float(bThunderstorms)*0.25f, // 0.25, 0.5, 0.75 depending on TS intensity + true); + } + } +}; + +/// Parse the METAR into the global objects, avoids re-parsing the same METAR over again +const ParseResult& ParseMETAR (const std::string& raw) +{ + static std::mutex mutexMetar; // Mutex that safeguards this procedure + static ParseResult metarResult; // Global object of last METAR parse result, helps avoiding parsing the same METAR twice + static std::string metarRaw; // The METAR that got parsed globally + + // Safeguard against multi-thread execution + std::lock_guard lck(mutexMetar); + + // samesame or different? + if (metarRaw != raw) { + metarResult = Parser::parse(metarRaw = raw); + if (metarResult.reportMetadata.error != ReportError::NONE) { + LOG_MSG(logWARN, "Parsing METAR failed with error %d\n%s", + int(metarResult.reportMetadata.error), + metarRaw.c_str()); + } else { + LOG_MSG(logDEBUG, "Parsing METAR found %lu groups", + (unsigned long)metarResult.groups.size()); + } + } + return metarResult; +} + +// Parse `metar` and fill weather from there as far as possible +bool LTWeather::IncorporateMETAR() +{ + // --- Parse --- + const ParseResult& r = ParseMETAR(metar); + if (r.reportMetadata.error != ReportError::NONE) + return false; + + // Log before applying METAR + if (dataRefs.ShallLogWeather()) + Log("Weather before applying METAR:"); + + // --- Process by 'visiting' all groups --- + LTWeatherVisitor v(*this); + for (const GroupInfo& gi : r.groups) + if (v.visit(gi)) // returns if to stop processing + break; + v.PostProcessing(); + return true; +} + + +// +// MARK: Weather Network Request handling +// + +/// The request URL, parameters are in this order: radius, longitude, latitude +const char* WEATHER_URL="https://aviationweather.gov/api/data/metar?format=json&bbox=%.2f,%.2f,%.2f,%.2f"; + +/// Didn't find a METAR last time we tried? +bool gbFoundNoMETAR = false; + +// Error messages +#define ERR_WEATHER_ERROR "Weather request returned with error: %s" +#define INFO_NO_NEAR_WEATHER "Found no nearby weather in a %.fnm radius" +#define INFO_FOUND_WEATHER_AGAIN "Successfully updated weather again from %s" + /// @brief Process the response from aviationweather.com -/// @details Response is in XML format. (JSON is not available.) -/// We aren't doing a full XML parse here but rely on the -/// fairly static structure: -/// We straight away search for: -/// `` Indicates just that and stops interpretation.\n -/// ``, ``, ``, ``, -/// and`` are the values we are interested in. +/// @see https://aviationweather.gov/data/api/#/Data/dataMetars +/// @details Response is a _decoded_ METAR in JSON array format, one element per station. +/// We don't need most of the fields, but with the JSON format +/// we get the station's location and the interpreted `altimeter` +/// "for free". +/// We are looking for the closest station in that list bool WeatherProcessResponse (const std::string& _r) { - float lat = NAN; - float lon = NAN; - float hPa = NAN; + double bestLat = NAN; + double bestLon = NAN; + double bestDist = DBL_MAX; + double bestHPa = NAN; std::string stationId; std::string METAR; - // Any error? - std::string::size_type pos = 0; - std::string val = GetXMLValue(_r, "", pos); - if (!val.empty()) { - LOG_MSG(logERR, ERR_WEATHER_ERROR, val.c_str()); - return false; - } + // Where is the user? Looking for METAR closest to that + const positionTy posUser = dataRefs.GetUsersPlanePos(); - // find the pressure - val = GetXMLValue(_r, "", pos); - if (!val.empty()) { - hPa = std::stof(val) * (float)HPA_per_INCH; + // Try to parse as JSON...even in case of errors we might be getting a body + // Unique_ptr ensures it is freed before leaving the function + JSONRootPtr pRoot (_r.c_str()); + if (!pRoot) { LOG_MSG(logERR,ERR_JSON_PARSE); return false; } + const JSON_Array* pArr = json_array(pRoot.get()); + if (!pArr) { LOG_MSG(logERR,ERR_JSON_MAIN_OBJECT); return false; } + + // Parse each METAR + for (size_t i = 0; i < json_array_get_count(pArr); ++i) + { + const JSON_Object* pMObj = json_array_get_object(pArr, i); + if (!pMObj) { + LOG_MSG(logERR, "Couldn't get %ld. element of METAR array", (long)i); + break; + } + + // Make sure our most important fields are available + const double lat = jog_n_nan(pMObj, "lat"); + const double lon = jog_n_nan(pMObj, "lon"); + const double hPa = jog_n_nan(pMObj, "altim"); + if (std::isnan(lat) || std::isnan(lon) || std::isnan(hPa)) { + LOG_MSG(logWARN, "Couldn't process %ld. METAR, skipping", (long)i); + continue; + } - // We fetch the other fields in order of appearance, but need to start once again from the beginning of the buffer - pos = 0; - // Try fetching METAR and station_id - METAR = GetXMLValue(_r, "", pos); - stationId = GetXMLValue(_r, "", pos); - - // then let's see if we also find the weather station's location - val = GetXMLValue(_r, "", pos); - if (!val.empty()) - lat = std::stof(val); - val = GetXMLValue(_r, "", pos); - if (!val.empty()) - lon = std::stof(val); + // Compare METAR field's position with best we have so far, skip if father away + vectorTy vec = posUser.between(positionTy(lat,lon)); + if (vec.dist > dataRefs.GetWeatherMaxMetarDist_m()) // skip if too far away + break; + // Weigh in direction of flight into distance calculation: + // In direction of flight, distance only appears half as far, + // so we prefer METARs in the direction of flight + vec.dist *= (180.0 + std::abs(HeadingDiff(posUser.heading(), vec.angle))) / 360.0; + if (vec.dist >= bestDist) + continue;; - // tell ourselves what we found - dataRefs.SetWeather(hPa, lat, lon, stationId, METAR); - - // found again weather after we had started to suppress messages? - if (gbSuppressWeatherErrMsg) { - // say hooray and report again + // We have a new nearest METAR + bestLat = lat; + bestLon = lon; + bestDist = vec.dist; + bestHPa = hPa; + stationId = jog_s(pMObj, "icaoId"); + METAR = jog_s(pMObj, "rawOb"); + } + + // If we found something + if (!std::isnan(bestLat) && !std::isnan(bestLon) && !std::isnan(bestHPa)) { + // If previously we had not found anything say huray + if (gbFoundNoMETAR) { LOG_MSG(logINFO, INFO_FOUND_WEATHER_AGAIN, stationId.c_str()); - gbSuppressWeatherErrMsg = false; + gbFoundNoMETAR = false; } - + // tell ourselves what we found + dataRefs.SetWeather(float(bestHPa), float(bestLat), float(bestLon), + stationId, METAR); return true; } @@ -203,77 +1293,63 @@ bool WeatherFetch (float _lat, float _lon, float _radius_nm) return false; } - // Loop in case we need to re-do a request with larger radius - bool bRepeat = false; - do { - bRepeat = false; - - // put together the URL, convert nautical to statute miles - const boundingBoxTy box (positionTy(_lat, _lon), _radius_nm * M_per_NM); - const positionTy minPos = box.sw(); - const positionTy maxPos = box.ne(); - snprintf(url, sizeof(url), WEATHER_URL, - minPos.lat(), minPos.lon(), - maxPos.lat(), maxPos.lon()); - - // prepare the handle with the right options - readBuf.reserve(CURL_MAX_WRITE_SIZE); - curl_easy_setopt(pCurl, CURLOPT_NOSIGNAL, 1); - curl_easy_setopt(pCurl, CURLOPT_TIMEOUT, dataRefs.GetNetwTimeoutMax()); - curl_easy_setopt(pCurl, CURLOPT_ERRORBUFFER, curl_errtxt); - curl_easy_setopt(pCurl, CURLOPT_WRITEFUNCTION, WeatherFetchCB); - curl_easy_setopt(pCurl, CURLOPT_WRITEDATA, &readBuf); - curl_easy_setopt(pCurl, CURLOPT_USERAGENT, HTTP_USER_AGENT); - curl_easy_setopt(pCurl, CURLOPT_URL, url); - - // perform the HTTP get request - CURLcode cc = CURLE_OK; - if ((cc = curl_easy_perform(pCurl)) != CURLE_OK) - { - // problem with querying revocation list? - if (LTOnlineChannel::IsRevocationError(curl_errtxt)) { - // try not to query revoke list - curl_easy_setopt(pCurl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE); - LOG_MSG(logWARN, ERR_CURL_DISABLE_REV_QU, LT_DOWNLOAD_CH); - // and just give it another try - cc = curl_easy_perform(pCurl); - } + // put together the URL, with a bounding box with _radius_nm in each direction + const boundingBoxTy box (positionTy(_lat, _lon), _radius_nm * M_per_NM * 2.0); + const positionTy minPos = box.sw(); + const positionTy maxPos = box.ne(); + snprintf(url, sizeof(url), WEATHER_URL, + minPos.lat(), minPos.lon(), + maxPos.lat(), maxPos.lon()); + + // prepare the handle with the right options + readBuf.reserve(CURL_MAX_WRITE_SIZE); + curl_easy_setopt(pCurl, CURLOPT_NOSIGNAL, 1); + curl_easy_setopt(pCurl, CURLOPT_TIMEOUT, dataRefs.GetNetwTimeoutMax()); + curl_easy_setopt(pCurl, CURLOPT_ERRORBUFFER, curl_errtxt); + curl_easy_setopt(pCurl, CURLOPT_WRITEFUNCTION, WeatherFetchCB); + curl_easy_setopt(pCurl, CURLOPT_WRITEDATA, &readBuf); + curl_easy_setopt(pCurl, CURLOPT_USERAGENT, HTTP_USER_AGENT); + curl_easy_setopt(pCurl, CURLOPT_URL, url); - // if (still) error, then log error - if (cc != CURLE_OK) - LOG_MSG(logERR, ERR_CURL_PERFORM, "Weather download", cc, curl_errtxt); + // perform the HTTP get request + CURLcode cc = CURLE_OK; + if ((cc = curl_easy_perform(pCurl)) != CURLE_OK) + { + // problem with querying revocation list? + if (LTOnlineChannel::IsRevocationError(curl_errtxt)) { + // try not to query revoke list + curl_easy_setopt(pCurl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE); + LOG_MSG(logWARN, ERR_CURL_DISABLE_REV_QU, LT_DOWNLOAD_CH); + // and just give it another try + cc = curl_easy_perform(pCurl); } - if (cc == CURLE_OK) - { - // CURL was OK, now check HTTP response code - long httpResponse = 0; - curl_easy_getinfo(pCurl, CURLINFO_RESPONSE_CODE, &httpResponse); + // if (still) error, then log error + if (cc != CURLE_OK) + LOG_MSG(logERR, ERR_CURL_PERFORM, "Weather download", cc, curl_errtxt); + } - // not HTTP_OK? - if (httpResponse != HTTP_OK) { - LOG_MSG(logERR, ERR_CURL_PERFORM, "Weather download", (int)httpResponse, ERR_HTTP_NOT_OK); - } - else { - // Success: Process data - bRet = WeatherProcessResponse(readBuf); - // Not found weather yet? - if (!bRet) { - // How often did we apply ADD_WEATHER_RADIUS_NM already? - const long nRadiusFactor = std::lround(_radius_nm/ADD_WEATHER_RADIUS_NM); - if (nRadiusFactor < MAX_WEATHER_RADIUS_FACTOR) { - if (!gbSuppressWeatherErrMsg) - LOG_MSG(logINFO, INFO_NO_NEAR_WEATHER, _radius_nm); - _radius_nm = (nRadiusFactor+1) * ADD_WEATHER_RADIUS_NM; - bRepeat = true; - } else if (!gbSuppressWeatherErrMsg) { - LOG_MSG(logERR, ERR_NO_WEATHER, _radius_nm); - gbSuppressWeatherErrMsg = true; - } + if (cc == CURLE_OK) + { + // CURL was OK, now check HTTP response code + long httpResponse = 0; + curl_easy_getinfo(pCurl, CURLINFO_RESPONSE_CODE, &httpResponse); + + // not HTTP_OK? + if (httpResponse != HTTP_OK) { + LOG_MSG(logERR, ERR_CURL_PERFORM, "Weather download", (int)httpResponse, ERR_HTTP_NOT_OK); + } + else { + // Success: Process data + if (!WeatherProcessResponse(readBuf)) { + // didn't find weather in data! + if (!gbFoundNoMETAR) { // say so, but once only + LOG_MSG(logINFO, INFO_NO_NEAR_WEATHER, _radius_nm); + gbFoundNoMETAR = true; } } } - } while (bRepeat); + } // cleanup CURL handle curl_easy_cleanup(pCurl); @@ -294,11 +1370,406 @@ bool WeatherFetch (float _lat, float _lon, float _radius_nm) // MARK: Global functions // -/// Is currently an async operation running to refresh the airports from apt.dat? +static bool bWeatherCanSet = false; ///< Would it be possible for LiveTraffic to control weather? (XP12 onwards) +static bool bWeatherControlling = false; ///< Is LiveTraffic in control of weather? +static int weatherOrigSource = -1; ///< Original value of `sim/weather/region/weather_source` before LiveTraffic took over +static int weatherOrigChangeMode = -1; ///< Original value of `sim/weather/region/change_mode` before LiveTraffic took over + +static std::recursive_mutex mtxWeather; ///< manages access to weather storage +static LTWeather nextWeather; ///< next weather to set +static bool bSetWeather = false; ///< is there a next weather to set? +static bool bResetWeather = false; ///< Shall weather be reset, ie. handed back to XP? +static LTWeather setWeather; ///< the weather we set last time +static std::string gEmptyString; ///< an empty string we can refer to if we need to return an empty reference + +// Initialize Weather module, dataRefs +bool WeatherInit () +{ + bWeatherCanSet = WeatherInitDataRefs(); + if (!bWeatherCanSet) { + LOG_MSG(logWARN, "Could not find all Weather dataRefs, cannot set X-Plane's weather (X-Plane < v12?)"); + } + return bWeatherCanSet; +} + +// Shutdown Weather module +void WeatherStop () +{ + WeatherReset(); +} + +// Can we set weather? (X-Plane 12 forward only) +bool WeatherCanSet () +{ + return bWeatherCanSet; +} + +// Are we controlling weather? +bool WeatherInControl () +{ + return bWeatherControlling; +} + +// Is X-Plane set to use real weather? +bool WeatherIsXPRealWeather () +{ + return WeatherCanSet() && wdr_change_mode.get() == WDR_CM_REAL_WEATHER; +} + +// Have X-Plane use its real weather +void WeatherSetXPRealWeather () +{ + if (WeatherCanSet()) + wdr_change_mode.set(WDR_CM_REAL_WEATHER); +} + +/// Internal function that actually sets X-Planes weather to what's defined in nextWeather +void WeatherDoSet () +{ + if (nextWeather.update_immediately) { + if (!WeatherInControl()) { + // Taking control of weather + weatherOrigSource = wdr_weather_source.get(); + weatherOrigChangeMode = wdr_change_mode.get(); + if (dataRefs.ShallLogWeather()) { + LOG_MSG(logDEBUG, "Weather originally %s (source = %d, change mode = %d)", + WeatherGetSource().c_str(), + weatherOrigSource, weatherOrigChangeMode); + LTWeather().Get("Weather just prior to LiveTraffic taking over:"); + } + SHOW_MSG(logINFO, "LiveTraffic takes over controlling X-Plane's weather"); + bWeatherControlling = true; + } else { + SHOW_MSG(logINFO, "LiveTraffic is re-setting X-Plane's weather"); + } + } + + // actually set the weather in X-Plane + nextWeather.Set(); + nextWeather.update_immediately = false; + // get all values from X-Plane right away, after XP's processing + setWeather.Get(); + // if weather's position is given remember that + if (nextWeather.pos.hasPosAlt()) + setWeather.pos = nextWeather.pos; +} + + +// Thread-safely store weather information to be set in X-Plane in the main thread later +void WeatherSet (const LTWeather& w) +{ + if (!WeatherCanSet()) { + LOG_MSG(logDEBUG, "Requested to set weather, but cannot due to missing dataRefs"); + return; + } + + // Access to weather storage, copy weather info + std::lock_guard mtx (mtxWeather); + nextWeather = w; + bSetWeather = true; +} + +// Thread-safely store weather information to be set in X-Plane in the main thread later +void WeatherSet (const std::string& metar, const std::string& metarIcao) +{ + if (!WeatherCanSet()) { + LOG_MSG(logDEBUG, "Requested to set weather, but cannot due to missing dataRefs"); + return; + } + + // Access to weather storage, copy weather info + std::lock_guard mtx (mtxWeather); + if (nextWeather.metar != metar) { // makes only sense in case something has changed + nextWeather = LTWeather(); // reset all, reads `atmosphere_alt_levels_m` already + nextWeather.Get(); // get current weather from X-Plane as basis + nextWeather.metar = metar; // just store METAR, will be processed/incorporated later in the main thread + nextWeather.metarFieldIcao = metarIcao; + nextWeather.posMetarField = positionTy(); + bSetWeather = true; + } +} + +// Set weather constantly to this METAR +void WeatherSetConstant (const std::string& metar) +{ + if (!dataRefs.IsXPThread() || !WeatherCanSet()) { + LOG_MSG(logDEBUG, "Requested to set weather, but cannot due to not being in main thread or missing dataRefs"); + return; + } + + // Reset? + if (metar.empty()) { + WeatherReset(); + } + else { + // Define the weather to be set solely based on the passed-in METAR + nextWeather = LTWeather(); + nextWeather.temperature_altitude_msl_m = nextWeather.wind_altitude_msl_m = nextWeather.atmosphere_alt_levels_m; + nextWeather.metar = metar; + nextWeather.cloud_coverage_percent.fill(0.0f); + nextWeather.cloud_base_msl_m.fill(0.0f); + nextWeather.cloud_tops_msl_m.fill(0.0f); + if (!nextWeather.IncorporateMETAR()) { + SHOW_MSG(logWARN, "Could not parse provided METAR, weather not set!"); + return; + } + + // Copy several value to all altitudes + nextWeather.wind_speed_msc.fill(nextWeather.wind_speed_msc.front()); + nextWeather.wind_direction_degt.fill(nextWeather.wind_direction_degt.front()); + nextWeather.shear_speed_msc.fill(nextWeather.shear_speed_msc.front()); + nextWeather.shear_direction_degt.fill(nextWeather.shear_direction_degt.front()); + nextWeather.turbulence.fill(nextWeather.turbulence.front()); + + // Temperature is a bit more difficult + for (size_t i = 1; i < nextWeather.temperature_altitude_msl_m.size(); ++i) + { + // up to field altitude keep the assigned METAR temperatur + if (nextWeather.temperature_altitude_msl_m[i] < nextWeather.posMetarField.alt_m()) { + nextWeather.temperatures_aloft_deg_c[i] = nextWeather.temperatures_aloft_deg_c[i-1]; + nextWeather.dewpoint_deg_c[i] = nextWeather.dewpoint_deg_c[i-1]; + } + else { + // above field altitude temperature reduces by .6 degree per 100m + const float dAlt = nextWeather.temperature_altitude_msl_m[i] - nextWeather.temperature_altitude_msl_m[i-1]; + nextWeather.temperatures_aloft_deg_c[i] = nextWeather.temperatures_aloft_deg_c[i-1] - (dAlt * 0.006458424f); + // above field altitude dewpoint reduces by .2 degree per 100m until reaching temperatur + nextWeather.dewpoint_deg_c[i] = std::min( + nextWeather.dewpoint_deg_c[i-1] - (dAlt * 0.001784121f), + nextWeather.temperatures_aloft_deg_c[i]); + } + } + + // Disable other weather control + DATA_REFS_LT[DR_CFG_WEATHER_CONTROL].setData(WC_NONE); + + // Remember the METAR we set + setWeather.metar = nextWeather.metar; + setWeather.metarFieldIcao = nextWeather.metarFieldIcao; + setWeather.posMetarField = nextWeather.posMetarField; + + nextWeather.update_immediately = true; + WeatherDoSet(); + SHOW_MSG(logINFO, "Constant weather set based on METAR"); + } +} + + +// Actually update X-Plane's weather if there is anything to do (called from main thread) +void WeatherUpdate () +{ + // Quick exit if we can't or shan't + if (!WeatherCanSet() || !dataRefs.IsXPThread()) return; + + // If the ask is to reset weather + if (bResetWeather) { + WeatherReset(); + return; + } + + // Where is the user's plane? + double altAGL_m = 0.0f; + const positionTy posUser = dataRefs.GetUsersPlanePos(nullptr, nullptr, &altAGL_m); + // Using XP Real Weather just now because we want it so? + const bool bXPRealWeather = WeatherIsXPRealWeather() && WeatherInControl(); + + // Access to weather storage guarded by a lock + std::lock_guard mtx (mtxWeather); + + // Check our METAR situation: Do we have one? How far away? + // Do we have a METAR that shall be processed? + bool bProcessMETAR = !nextWeather.metar.empty() && !nextWeather.metarFieldIcao.empty(); + // Determine location of METAR field + if (bProcessMETAR && !nextWeather.posMetarField.hasPosAlt()) + nextWeather.posMetarField = GetAirportLoc(nextWeather.metarFieldIcao); + // is that considered too far away? + const bool bNoNearbyMETAR = + !bProcessMETAR || + !nextWeather.posMetarField.hasPosAlt() || + float(nextWeather.posMetarField.distRoughSqr(posUser)) > sqr(dataRefs.GetWeatherMaxMetarDist_m()); + if (bNoNearbyMETAR) + bProcessMETAR = false; + + // In case of METAR+XP: If using XP's Real Weather, then ignore "bSetWeather" (at least for now) + if (dataRefs.GetWeatherControl() == WC_METAR_XP) // using METAR+XP mode + { + // No nearby METAR or flying high, should use XP Real Weather + if (bNoNearbyMETAR || + altAGL_m > dataRefs.GetWeatherMaxMetarHeight_m() + (WeatherInControl() ? 100.0 : 0.0)) + { + // should use XP Real Weather, but aren't yet? + if (!bXPRealWeather) { + SHOW_MSG(logINFO, "%s (%s)", + WeatherInControl() ? "Switching to XP Real Weather" : + "LiveTraffic takes over controlling X-Plane's weather, activating XP's real weather", + bNoNearbyMETAR ? "no nearby METAR" : "flying high"); + // Remember that we did _not_ use a METAR to define weather + setWeather.metar.clear(); + setWeather.metarFieldIcao.clear(); + setWeather.posMetarField = positionTy(); + // Set to XP real weather + WeatherSetXPRealWeather(); + bWeatherControlling = true; + } + return; + } + + // Already in control and flying in-between (+/- 100m of max METAR height): no change + if (WeatherInControl() && + (altAGL_m > dataRefs.GetWeatherMaxMetarHeight_m() - 100.0)) + return; + + // Should use available METAR. + // but don't do yet? + if (bXPRealWeather) { + if (WeatherInControl()) { + SHOW_MSG(logINFO, "Switching to METAR weather (flying low)"); + } + // falls through to "Set Weather"! + bSetWeather = true; + } + } + + // If there's no new weather to set + if (!bSetWeather) { + // if we are in control and no real weather set + if (WeatherInControl() && !bXPRealWeather) + wdr_qnh_pas.set(setWeather.qnh_pas); // then re-set QNH as X-Plane likes to override that itself from time to time + return; // but don't do any more + } + + // -- Set Weather -- + bSetWeather = false; // reset flag right away so we don't try again in case of early exits (errors) + + // If there is a METAR to process, then now is the moment + if (bProcessMETAR) + { + if (nextWeather.IncorporateMETAR()) { + // Remember the METAR we used + setWeather.metar = nextWeather.metar; + setWeather.metarFieldIcao = nextWeather.metarFieldIcao; + setWeather.posMetarField = nextWeather.posMetarField; + } + else + bProcessMETAR = false; + } + if (!bProcessMETAR) { + // Remember that we did _not_ use a METAR to define weather + setWeather.metar.clear(); + setWeather.metarFieldIcao.clear(); + setWeather.posMetarField = positionTy(); + } + + // Set weather with immediate effect if first time, or if position changed dramatically + nextWeather.update_immediately |= !WeatherInControl() || + !setWeather.pos.hasPosAlt() || + setWeather.pos.dist(posUser) > WEATHER_MAX_DIST_M; + WeatherDoSet(); +} + +// Reset weather settings to what they were before X-Plane took over +void WeatherReset () +{ + // If not called from main thread just set a flag and wait for main thread + if (!dataRefs.IsXPThread()) { + bResetWeather = true; + return; + } + + if (weatherOrigSource >= 0) wdr_weather_source.set(weatherOrigSource); + if (weatherOrigChangeMode >= 0) wdr_change_mode.set(weatherOrigChangeMode); + + if (bWeatherControlling) { + bWeatherControlling = false; + SHOW_MSG(logINFO, "LiveTraffic no longer controls X-Plane's weather, reset to previous settings"); + if (dataRefs.ShallLogWeather()) { + LOG_MSG(logDEBUG, "Weather reset to %s (source = %d, change mode = %d)", + WeatherGetSource().c_str(), + weatherOrigSource, weatherOrigChangeMode); + } + } + + weatherOrigSource = -1; + weatherOrigChangeMode = -1; + setWeather.pos = setWeather.posMetarField = positionTy(); + setWeather.metar.clear(); + bResetWeather = bSetWeather = false; +} + +// Log current weather +void WeatherLogCurrent (const std::string& msg) +{ + LTWeather().Get(msg); +} + +// Current METAR in use for weather generation +const std::string& WeatherGetMETAR () +{ + if (WeatherInControl()) + return setWeather.metar; // if in control then return the METAR of the weather we did set + else + return gEmptyString; // otherwise (a reference to) an empty string +} + +// Return a human readable string on the weather source, is "LiveTraffic" if WeatherInControl() +std::string WeatherGetSource () +{ + // Preset closest to current conditions + static std::array WEATHER_PRESETS = { + "Clear", "VFR Few", "VFR Scattered", "VFR Broken", "VFR Marginal", "IFR Non-precision", "IFR Precision", "Convective", "Large-cell Storms", "Unknown" + }; + int preset = wdr_weather_preset.get(); + if (preset < 0 || preset > 8) preset = 9; // 'Unknown' + + // Weather Source + static std::array WEATHER_SOURCES = { + "X-Plane Preset", "X-Plane Real Weather", "Controlpad", "Plugin", "Unknown" + }; + int source = wdr_weather_source.get(); + if (source < 0 || source > 3) source = 4; + + // Are we in control? Say so! + if (WeatherInControl()) { + char t[100]; + switch (dataRefs.GetWeatherControl()) { + case WC_REAL_TRAFFIC: + return "LiveTraffic using RealTraffic weather data"; + case WC_METAR_XP: + snprintf(t, sizeof(t), "LiveTraffic, using %s %dft AGL", + (WeatherIsXPRealWeather() ? "XP's real weather above" : "METAR up to"), + dataRefs.GetWeatherMaxMetarHeight_ft()); + return std::string(t); + case WC_NONE: + case WC_INIT: + return std::string("LiveTraffic, unknown"); + } + } + else if (source == 0) // 'Preset' + return std::string(WEATHER_SOURCES[size_t(source)]) + ", " + WEATHER_PRESETS[size_t(preset)]; + else + return std::string(WEATHER_SOURCES[size_t(source)]); +} + +// Extract QNH or SLP from METAR, NAN if not found any info, which is rather unlikely +float WeatherQNHfromMETAR (const std::string& metar) +{ + // --- Parse --- + const ParseResult& r = ParseMETAR(metar); + if (r.reportMetadata.error == ReportError::NONE) { + // Find the pressure group + for (const GroupInfo& gi: r.groups) + if (const PressureGroup *pPg = std::get_if(&gi.group)) + return pPg->atmosphericPressure().toUnit(Pressure::Unit::HECTOPASCAL).value_or(NAN); + } + return NAN; +} + + +/// Is currently an async operation running to fetch METAR? static std::future futWeather; // Asynchronously, fetch fresh weather information -bool WeatherUpdate (const positionTy& pos, float radius_nm) +bool WeatherFetchUpdate (const positionTy& pos, float radius_nm) { // does only make sense in a certain latitude range // (During XP startup irregular values >80 show up) diff --git a/Src/LiveTraffic.cpp b/Src/LiveTraffic.cpp index 19a197f..f06be77 100755 --- a/Src/LiveTraffic.cpp +++ b/Src/LiveTraffic.cpp @@ -133,6 +133,7 @@ void MenuHandler(void * /*mRef*/, void * iRef) break; #ifdef DEBUG case MENU_ID_RELOAD_PLUGINS: + dataRefs.SetAircraftDisplayed(false); // reloading plugins is more reliable if we shutdown internally first XPLMReloadPlugins(); break; case MENU_ID_REMOVE_ALL_BUT: @@ -471,6 +472,17 @@ float LoopCBOneTimeSetup (float, float, int, void*) // Inform dataRef tools about our dataRefs dataRefs.InformDataRefEditors(); + // If weather setting is yet undetermined make a choice + // (This is one-time code introduced with weather functionality, + // should actually be in DataRefs::LoadConfig, + // but can't because determining if user has set real weather + // only works later, in the flight loops.) + // Set to "RealTraffic weather" if X-Plane is set to real weather + // and user has a RT license. + if (dataRefs.GetWeatherControl() < WC_NONE) + DATA_REFS_LT[DR_CFG_WEATHER_CONTROL].setData((WeatherIsXPRealWeather() && !dataRefs.GetRTLicense().empty()) ? + WC_REAL_TRAFFIC : WC_NONE); + // next: Auto Start, but wait another 2 seconds for that eOneTimeState = ONCE_CB_AUTOSTART; return 2; @@ -551,6 +563,9 @@ PLUGIN_API int XPluginStart( // init DataRefs if (!dataRefs.Init()) { DestroyWindow(); return 0; } + // init Weather module (optional) + WeatherInit(); + // read FlightModel.prf file (which we could live without) LTAircraft::FlightModel::ReadFlightModelFile(); @@ -666,6 +681,9 @@ PLUGIN_API void XPluginDisable(void) { // Stop reading apt.dat LTAptDisable(); + + // Reset weather, back in XP's control + WeatherReset(); // if there still is a message window remove it DestroyWindow(); @@ -689,6 +707,9 @@ PLUGIN_API void XPluginStop(void) // Cleanup aircraft handling (including XPMP library) LTMainStop(); + // Cleanup Weather module + WeatherStop(); + // Cleanup dataRef registration, save config file dataRefs.Stop(); diff --git a/Src/SettingsUI.cpp b/Src/SettingsUI.cpp index 115648d..4e3aa3a 100644 --- a/Src/SettingsUI.cpp +++ b/Src/SettingsUI.cpp @@ -585,6 +585,7 @@ void LTSettingsUI::buildInterface() ImGui::TableNextCell(); } + // Historic Data if (ImGui::FilteredLabel("Request Historic Data", sFilter)) { float cbWidth = ImGui::CalcTextSize("Use live data, not historic_____").x; ImGui::SetNextItemWidth(cbWidth); @@ -782,6 +783,42 @@ void LTSettingsUI::buildInterface() if (!*sFilter) { ImGui::TreePop(); ImGui::Spacing(); } } // --- Output Channels --- + // MARK: --- Weather --- + if (WeatherCanSet() && // the entire Weather section only if we actually can influence the weather + ImGui::TreeNodeHelp("Weather", nCol, + HELP_SET_WEATHER, "Open Help on Weather options in Browser", + sFilter, nOpCl)) + { + // Weather main switch + if (ImGui::FilteredLabel("Set Weather", sFilter)) { + const float cbWidth = ImGui::CalcTextSize("METAR (near ground) + X-Plane real weather____").x; + ImGui::SetNextItemWidth(cbWidth); + int n = dataRefs.GetWeatherControl(); + if (ImGui::Combo("##SetWeather", &n, "Off\0METAR (near ground) + X-Plane real weather\0RealTraffic weather data\0", 3)) { + ImGuiContext* pCtxt = ImGui::GetCurrentContext(); // switching off weather can affect context....(?) + DATA_REFS_LT[DR_CFG_WEATHER_CONTROL].setData(n); + ImGui::SetCurrentContext(pCtxt); + } + ImGui::TableNextCell(); + } + + // Use/prefer METAR up to which height AGL? + ImGui::FilteredCfgNumber("Use METAR up to", sFilter, DR_CFG_WEATHER_MAX_METAR_AGL, 1000, 10000, 1000, "%d ft AGL"); + // Use METAR up to which distance from fiel? + ImGui::FilteredCfgNumber("Max. METAR distance", sFilter, DR_CFG_WEATHER_MAX_METAR_DIST, 5, 100, 5, "%d nm"); + + // Allow to define a constant, METAR-based weather + if (ImGui::FilteredInputText("Set weather to", sFilter, txtManualMETAR, 0.0f, + "Paste METAR to define weather, hit ", + ImGuiInputTextFlags_CharsUppercase | + ImGuiInputTextFlags_AutoSelectAll | + ImGuiInputTextFlags_EnterReturnsTrue)) + WeatherSetConstant(txtManualMETAR); + + + if (!*sFilter) { ImGui::TreePop(); ImGui::Spacing(); } + } // --- Weather --- + // MARK: --- Aircraft Labels --- if (ImGui::TreeNodeHelp("Aircraft Labels", nCol, HELP_SET_ACLABELS, "Open Help on Aircraft Label options in Browser", @@ -1283,6 +1320,13 @@ void LTSettingsUI::buildInterface() "Logs how available tracking data was matched with the chosen CSL model (into Log.txt)"); ImGui::FilteredCfgCheckbox("Log a/c positions", sFilter, DR_DBG_AC_POS, "Logs detailed position information of currently selected aircraft (into Log.txt)"); + ImGui::FilteredCfgCheckbox("Log Weather", sFilter, DR_DBG_LOG_WEATHER, + "Logs detailed information about how X-Plane's weather is set (into Log.txt)"); + if (ImGui::FilteredLabel("Log Weather now", sFilter)) { + if (ImGui::ButtonTooltip("Log Weather now","Places information on current weather into Log.txt")) + WeatherLogCurrent("Current weather:"); + ImGui::TableNextCell(); + } ImGui::FilteredCfgCheckbox("Log Raw Network Data", sFilter, DR_DBG_LOG_RAW_FD, "Creates additional log file 'LTRawFD.log'\ncontaining all raw network requests and responses."); diff --git a/Lib/parson/LICENSE b/docs/LICENSE_metaf similarity index 85% rename from Lib/parson/LICENSE rename to docs/LICENSE_metaf index 4431a61..201a95f 100644 --- a/Lib/parson/LICENSE +++ b/docs/LICENSE_metaf @@ -1,6 +1,4 @@ -MIT License - -Copyright (c) 2012 - 2022 Krzysztof Gabis +Copyright (c) 2018-2020 Nick Naumenko (https://gitlab.com/nnaumenko) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +7,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/LICENSE_parson.txt b/docs/LICENSE_parson.txt index c5ee57d..4431a61 100644 --- a/docs/LICENSE_parson.txt +++ b/docs/LICENSE_parson.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012 - 2020 Krzysztof Gabis +Copyright (c) 2012 - 2022 Krzysztof Gabis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/readme.html b/docs/readme.html index 8abd084..056fd9e 100755 --- a/docs/readme.html +++ b/docs/readme.html @@ -133,6 +133,199 @@

Release Notes

v3

+

v3.6.8 Beta

+ +

At least copy the following files, which have changed compared to v3.6.0:

+
    +
  • lin|mac|win_x64/LiveTraffic.xpl
  • +
+ +

Change log:

+ +
    +
  • + Weather: Option "METAR (near ground) + X-Plane real weather" now operational.
    + Doesn't use and is not depending on RealTraffic data but uses the METAR LiveTraffic has always fetched from aviationweather.gov. + Uses METAR to set weather when user is flying below height AGL set in Use METAR up to, + switches to X-Plane's real weather above that height. +
  • +
  • + OpenSky is again enabled by default in new installations.
    + If you want to use OpenSky in an existing installation just activate it in the channel's settings. +
  • +
+ +

v3.6.7 Beta

+ +

At least copy the following files, which have changed compared to v3.6.0:

+
    +
  • lin|mac|win_x64/LiveTraffic.xpl
  • +
+ +

Change log:

+ +
    +
  • + RealTraffic: Parked aircraft +
      +
    • are updated when location changes significantly, after airport layouts around the new location have been processed;
    • +
    • are only requested when configuration Keep Parked Aircraft is enabled;
    • +
    • are pre-processed to remove duplicates per parking position, keeping only the last seen aircraft.
    • +
    +
  • +
  • RealTraffic: Restructured API request rate limit handling, better separating between traffic and weather requests, + speeds up initial loading of weather and traffic.
  • +
  • Increased total maximum of allowed aircraft to 300, room needed to allow for parked aircraft at large airports.
  • +
+ +

v3.6.6 Beta

+ +

At least copy the following files, which have changed compared to v3.6.0:

+
    +
  • lin|mac|win_x64/LiveTraffic.xpl
  • +
+ +

Change log:

+ +
    +
  • + RealTraffic provides parked aircraft now, which populate aircraft parking positions. + These are aircraft that RealTraffic had seen stopping at that position in the last 24 hours.
    + For accurate placement LiveTraffic needs to know the airport layout, so parked aircraft + are only requested once airport layouts have been processed, ie. with a little delay.
    + Limitation currently is that parked aircraft are only requested once + upon activation of the RealTraffic channel at the current location. If you fly around + then they won't update and you won't see parked aircraft at your (far away) destination. + Then is to be fixed in a future version. +
  • +
+ +

v3.6.5 Beta

+ + This is a beta version for RealTraffic weather. Beta versions are time-limited to one month. + Till then there is either a follow-up beta version with a new one month limit, + or an unlimited public version. + + Online documentation isn't yet available, so this Readme describes features more detailed than usual. + +

+ Update: In case of doubt you can always just copy all files from the archive + over the files of your existing installation. +

+ +

At least copy the following files, which have changed compared to v3.6.0:

+
    +
  • lin|mac|win_x64/LiveTraffic.xpl
  • +
+ +

Change log:

+ +
    +
  • + Weather: In X-Plane 12 and later, LiveTraffic can now set X-Plane's weather based on RealTraffic weather data and METAR, + both for live and historic information. See the new section Settings > Weather: +
      +
    • + Set Weather activates weather generation when "RealTraffic weather data" is selected.
      + (The setting "METAR + X-Plane" does not yet work.) +
    • +
    • + Use METAR up to (default: 5,000ft) defines up to which height above ground level the METAR report takes priority + over more coarse weather model data, which however has data for several atmospheric layers and is hence used for the upper atmosphere. +
    • +
    • + Max. METAR distance (default: 25nm) defines the maximum distance to the field of the METAR up to which the METAR is used for weather generation. + This means that no METAR is available closer than this distance that all weather generation bases on the weather model data only. +
    • +
    + The "Status/About" window is enhanced by: +
      +
    • + Weather Source says if X-Plane or LiveTraffic are in charge of weather generation. +
    • +
    • + Weather METAR only appears while + historic RealTraffic data + is being used and then shows the historic METAR currently being used for weather generation. +
    • +
    • + LiveWeather is essentially unchanged. The METAR shown here is the current one, + which is the report used for weather generation if live data (and not historic) is being used. +
    • +
    + Two new features are available in + Settings > Debug > Logging: +
      +
    • + Log Weather logs weather information into Log.txt while being applied to X-Plane:
      + X-Plane's weather before LiveTraffic takes over;
      + Weather model information received from RealTraffic before METAR is applied;
      + Weather as set including applied METAR;
      + Weather data read back from X-Plane after set through LiveTraffic. +
    • +
    • + Log Weather now reads weather info from X-Plane and logs it to Log.txt. +
    • +
    +
  • +
  • + RealTraffic aircraft show up faster, don't require the buffering period any longer. +
  • +
+ +

Notes on testing:

+ +

+ Please always ensure that Log Level + is set to "Debug" (should be by default in a Beta version) + and that the new option Settings > Debug > Logging > Log Weather is activated. +

+ During testing please pay special attention how accurately the generated weather follows the data received from RealTraffic. + LiveTraffic can't look out of your window, don't expect to see the same cloud patterns. + What it shall do is to generate weather that matches received information. +

+ It will take a moment for weather in X-Plane to match what LiveTraffic sets, + especially clouds only form over time. Give it 2-3 minutes before comparing against expectations. +

+ Close to the ground METAR takes priority (Settings > Weather > Use METAR up to). + Check the currently used METAR in "Status/About" and compare it to what you see as X-Plane's weather. + Nearly all of METAR information is being used, but not temporary ("TEMPO") or geographically specific information + (like specific runways or directions of the compass) as X-Plane doesn't allow to set weather so specifically. + Only one "regional" weather can be set. +

+ Note that generally a METAR describes weather only near the ground. So even with a "CAVOK" reporting, there still can be clouds higher up in the sky. + CAVOK, NSC only say there are no clouds "of operational significance" below 5,000ft. + In case of CLR the limits are 12,000ft in the US and 25,000ft in Canada. + Above those limits, high altitude cloud layers can still exist. + Only SKC really seems to mean "no cloud at all". +

+ In higher altitudes weather model data is used as received from RealTraffic. + This is not visible from METAR, which describes weather only up to about 5,000ft height AGL. + To really know what RealTraffic reported (for comparing if it correctly appears in X-Plane) + you would need to look into Log.txt and check the logged weather information. + Looking into other live data sources like e.g. Windy is a possibility, but is not guaranteed + to exactly match RealTraffic's view. +

+ Test RealTraffic's historic data. + You should get that historic weather. Check Weather METAR appearing additionally in "Status/About". + Note that RealTraffic delays METAR data by about 10-15 minutes. So to receive a specific METAR of interest + add about 15 minutes to the timestamp of that report. +

+ When reporting back issues please be very conscious if the deviation you are about to report
+ is more a deviaton of RealTraffic's data (generated weather doesn't match reality as per looking out of the window + or comparing to other weather sources but seems to match available data) or
+ if LiveTraffic's generation doesn't match available input data + (weather visible in X-Plane is inconsistent with the METAR used as per "Status/About" + or with the RealTraffic as logged in Log.txt.)
+ Both would be important, but would affect different systems. +

+ Needless to say...always attach Log.txt with any issue report. + If you think current weather display doesn't match expectations, then add a screenshot + and also click Settings > Debug > Logging > Log Weather now so that current weather data is included in + Log.txt. +

+ +

v3.6.0