PlugIn.cpp 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. /*
  2. * Author:
  3. * Sven Czarnian <devel@svcz.de>
  4. * Brief:
  5. * Implements the EuroScope plug-in definition
  6. * Copyright:
  7. * 2021 Sven Czarnian
  8. * License:
  9. * GNU General Public License v3 (GPLv3)
  10. */
  11. #include "stdafx.h"
  12. #include <algorithm>
  13. #include <cctype>
  14. #include <regex>
  15. #include <gsl/gsl>
  16. #include <Shlwapi.h>
  17. #include <Windows.h>
  18. #include <curl/curl.h>
  19. #pragma warning(push, 0)
  20. #include <Eigen/Geometry>
  21. #pragma warning(pop)
  22. #include <GeographicLib/Gnomonic.hpp>
  23. #include <json/json.h>
  24. #include <aman/com/Backend.h>
  25. #include <aman/config/CommunicationFileFormat.h>
  26. #include <aman/config/IdentifierFileFormat.h>
  27. #include <aman/helper/String.h>
  28. #include <aman/types/GeoCoordinate.h>
  29. #include "com/ZmqContext.h"
  30. #include "PlugIn.h"
  31. EXTERN_C IMAGE_DOS_HEADER __ImageBase;
  32. using namespace aman;
  33. static std::string __receivedAmanData;
  34. static std::size_t receiveCurl(void* ptr, std::size_t size, std::size_t nmemb, void* stream) {
  35. std::ignore = stream;
  36. std::string serverResult = static_cast<char*>(ptr);
  37. __receivedAmanData += serverResult;
  38. return size * nmemb;
  39. }
  40. PlugIn::PlugIn() :
  41. EuroScopePlugIn::CPlugIn(EuroScopePlugIn::COMPATIBILITY_CODE,
  42. PLUGIN_NAME,
  43. PLUGIN_VERSION,
  44. PLUGIN_DEVELOPER,
  45. PLUGIN_COPYRIGHT),
  46. m_configuration(),
  47. m_screen(),
  48. m_updateQueueLock(),
  49. m_updateQueue(),
  50. m_initialApproachFixes(),
  51. m_inboundsQueueLock(),
  52. m_inbounds(),
  53. m_forcedToBackendCallsigns(),
  54. m_compatible(false),
  55. m_connectedToNetwork(false),
  56. m_sweatboxValid(false),
  57. m_playbackValid(false) {
  58. GOOGLE_PROTOBUF_VERIFY_VERSION;
  59. this->RegisterTagItemType("ETA", static_cast<int>(PlugIn::TagItemElement::EstimatedTimeOfArrival));
  60. this->RegisterTagItemType("PTA", static_cast<int>(PlugIn::TagItemElement::PlannedTimeOfArrival));
  61. this->RegisterTagItemType("Delta time", static_cast<int>(PlugIn::TagItemElement::DeltaTime));
  62. this->RegisterTagItemType("Fixed plan indicator", static_cast<int>(PlugIn::TagItemElement::FixedPlanIndicator));
  63. this->RegisterTagItemFunction("Runway selection", static_cast<int>(PlugIn::TagItemFunction::RunwaySelectMenu));
  64. this->RegisterTagItemFunction("Direct to", static_cast<int>(PlugIn::TagItemFunction::DirectToMenu));
  65. this->DisplayUserMessage(PLUGIN_NAME, "INFO", (std::string("Loaded ") + PLUGIN_NAME + " " + PLUGIN_VERSION).c_str(), true, true, false, false, false);
  66. /* get the dll-path */
  67. char path[MAX_PATH + 1] = { 0 };
  68. const gsl::span<char, MAX_PATH + 1> span(path);
  69. GetModuleFileNameA((HINSTANCE)&__ImageBase, span.data(), span.size());
  70. PathRemoveFileSpecA(span.data());
  71. std::string dllPath = span.data();
  72. CommunicationFileFormat comFormat;
  73. if (false == comFormat.parse(dllPath + "\\AmanCommunication.txt", this->m_configuration) || true == comFormat.errorFound()) {
  74. this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to parse AmanCommunication.txt", true, true, true, true, true);
  75. this->DisplayUserMessage(PLUGIN_NAME, "ERROR", ("AmanCommunication.txt:" + std::to_string(comFormat.errorLine()) + " - " + comFormat.errorMessage()).c_str(), true, true, true, true, true);
  76. return;
  77. }
  78. IdentifierFileFormat identFormat;
  79. if (false == identFormat.parse(dllPath + "\\AmanIdentity.txt", this->m_configuration) || true == identFormat.errorFound()) {
  80. this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to parse AmanIdentity.txt", true, true, true, true, true);
  81. this->DisplayUserMessage(PLUGIN_NAME, "ERROR", ("AmanIdentity.txt:" + std::to_string(identFormat.errorLine()) + " - " + identFormat.errorMessage()).c_str(), true, true, true, true, true);
  82. return;
  83. }
  84. ZmqContext::instance().initialize();
  85. if (false == Backend::instance().initialize(this->m_configuration))
  86. this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to initialize the reporter-connection to the backend", true, true, true, true, true);
  87. this->validateBackendData();
  88. }
  89. PlugIn::~PlugIn() {
  90. Backend::instance().deinitialize();
  91. ZmqContext::instance().deinitialize();
  92. google::protobuf::ShutdownProtobufLibrary();
  93. }
  94. void PlugIn::receiveConfiguration(const std::string& airport) {
  95. /* set up the URL */
  96. std::string url;
  97. if (true == this->m_configuration.httpsProtocol)
  98. url += "https://";
  99. else
  100. url += "http://";
  101. url += this->m_configuration.address + ":" + std::to_string(this->m_configuration.portRestAPI) + "/aman/configuration/" + airport;
  102. CURL* curl = curl_easy_init();
  103. CURLcode result;
  104. curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
  105. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
  106. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
  107. curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, static_cast<long>(CURL_HTTP_VERSION_1_1));
  108. curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
  109. curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
  110. curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveCurl);
  111. curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);
  112. struct curl_slist* headers = nullptr;
  113. headers = curl_slist_append(headers, "Accept: */*");
  114. headers = curl_slist_append(headers, "Content-Type: application/json");
  115. curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
  116. __receivedAmanData.clear();
  117. result = curl_easy_perform(curl);
  118. if (CURLE_OK != result) {
  119. MessageBoxA(nullptr, "Unable to receive backend information", "AMAN-Error", MB_OK);
  120. return;
  121. }
  122. /* validate the json data */
  123. Json::Value root;
  124. Json::CharReaderBuilder builder;
  125. std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
  126. if (false == reader->parse(__receivedAmanData.c_str(), __receivedAmanData.c_str() + __receivedAmanData.length(), &root, nullptr)) {
  127. MessageBoxA(nullptr, "Received invalid backend data", "AMAN-Error", MB_OK);
  128. return;
  129. }
  130. for (auto it = root.begin(); root.end() != it; ++it) {
  131. if ("iafs" == it.key().asString() && true == it->isArray()) {
  132. auto iafArray = *it;
  133. this->m_initialApproachFixes.insert({ airport, {} });
  134. this->m_initialApproachFixes[airport].reserve(iafArray.size());
  135. for (Json::Value::ArrayIndex i = 0; i < iafArray.size(); ++i) {
  136. if (true == iafArray[i].isObject()) {
  137. Angle latitude, longitude;
  138. std::string name;
  139. for (auto cit = iafArray[i].begin(); iafArray[i].end() != cit; ++cit) {
  140. if ("name" == cit.key().asString())
  141. name = cit->asString();
  142. else if ("lat" == cit.key().asString())
  143. latitude = cit->asFloat() * degree;
  144. else if ("lon" == cit.key().asString())
  145. longitude = cit->asFloat() * degree;
  146. }
  147. this->m_initialApproachFixes[airport].push_back({ name, GeoCoordinate(longitude, latitude) });
  148. }
  149. }
  150. }
  151. }
  152. }
  153. void PlugIn::validateBackendData() {
  154. if (false == this->m_configuration.valid) {
  155. this->m_compatible = false;
  156. return;
  157. }
  158. /* set up the URL */
  159. std::string url;
  160. if (true == this->m_configuration.httpsProtocol)
  161. url += "https://";
  162. else
  163. url += "http://";
  164. url += this->m_configuration.address + ":" + std::to_string(this->m_configuration.portRestAPI) + "/aman/airports";
  165. CURL* curl = curl_easy_init();
  166. CURLcode result;
  167. curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
  168. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
  169. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
  170. curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, static_cast<long>(CURL_HTTP_VERSION_1_1));
  171. curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
  172. curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
  173. curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveCurl);
  174. curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);
  175. struct curl_slist* headers = nullptr;
  176. headers = curl_slist_append(headers, "Accept: */*");
  177. headers = curl_slist_append(headers, "Content-Type: application/json");
  178. curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
  179. __receivedAmanData.clear();
  180. result = curl_easy_perform(curl);
  181. this->m_compatible = false;
  182. if (CURLE_OK != result) {
  183. MessageBoxA(nullptr, "Unable to receive backend information", "AMAN-Error", MB_OK);
  184. return;
  185. }
  186. /* validate the json data */
  187. Json::Value root;
  188. Json::CharReaderBuilder builder;
  189. std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
  190. if (false == reader->parse(__receivedAmanData.c_str(), __receivedAmanData.c_str() + __receivedAmanData.length(), &root, nullptr)) {
  191. MessageBoxA(nullptr, "Received invalid backend data", "AMAN-Error", MB_OK);
  192. return;
  193. }
  194. std::vector<std::string> airports;
  195. std::string version;
  196. for (auto it = root.begin(); root.end() != it; ++it) {
  197. if ("version" == it.key().asString()) {
  198. version = it->asString();
  199. }
  200. else if ("airports" == it.key().asString() && true == it->isArray()) {
  201. auto airportArray = *it;
  202. airports.reserve(airportArray.size());
  203. for (Json::Value::ArrayIndex i = 0; i < airportArray.size(); ++i)
  204. airports.push_back(airportArray[i].asString());
  205. }
  206. }
  207. /* invalid version or airports info */
  208. auto entries = String::splitString(version, ".");
  209. if (3 != entries.size() || 0 == airports.size()) {
  210. MessageBoxA(nullptr, "Unable to receive backend information", "AMAN-Error", MB_OK);
  211. return;
  212. }
  213. /* incompatible communication version */
  214. if (PLUGIN_MAJOR_VERSION != std::atoi(gsl::at(entries, 0).c_str()) || PLUGIN_MINOR_VERSION != std::atoi(gsl::at(entries, 1).c_str())) {
  215. MessageBoxA(nullptr, "Plugin version is outdated. An update is required", "AMAN-Error", MB_OK);
  216. return;
  217. }
  218. std::lock_guard guard(this->m_updateQueueLock);
  219. this->m_updateQueue.clear();
  220. this->m_initialApproachFixes.clear();
  221. for (const auto& airport : std::as_const(airports)) {
  222. this->m_updateQueue.insert({ airport, {} });
  223. this->receiveConfiguration(airport);
  224. }
  225. this->m_compatible = true;
  226. this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Loaded AMAN-configuration", true, true, false, false, false);
  227. }
  228. EuroScopePlugIn::CRadarScreen* PlugIn::OnRadarScreenCreated(const char* displayName, bool needsRadarContent, bool geoReferenced,
  229. bool canBeSaved, bool canBeCreated) {
  230. std::ignore = needsRadarContent;
  231. std::ignore = geoReferenced;
  232. std::ignore = canBeSaved;
  233. std::ignore = canBeCreated;
  234. std::ignore = displayName;
  235. if (nullptr == this->m_screen)
  236. this->m_screen = std::make_shared<RadarScreen>();
  237. this->OnAirportRunwayActivityChanged();
  238. return this->m_screen.get();
  239. }
  240. aman::Aircraft* PlugIn::generateAircraftMessage(const EuroScopePlugIn::CRadarTarget& target) const {
  241. if (false == target.IsValid() || false == target.GetCorrelatedFlightPlan().IsValid())
  242. return nullptr;
  243. const auto flightPlan = target.GetCorrelatedFlightPlan();
  244. std::string callsign(target.GetCallsign());
  245. aman::Aircraft* retval = new aman::Aircraft();
  246. /* fill all available information */
  247. retval->set_callsign(callsign);
  248. if (3 <= callsign.length())
  249. retval->set_airline(callsign.substr(0, 3));
  250. retval->set_type(flightPlan.GetFlightPlanData().GetAircraftFPType());
  251. char wtcMessage[2] = { flightPlan.GetFlightPlanData().GetAircraftWtc(), '\0' };
  252. retval->set_wtc(wtcMessage);
  253. retval->set_enginecount(flightPlan.GetFlightPlanData().GetEngineNumber());
  254. switch (flightPlan.GetFlightPlanData().GetEngineType()) {
  255. case 'P':
  256. case 'T':
  257. retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_TURBOPROB);
  258. break;
  259. case 'E':
  260. retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_ELECTRIC);
  261. break;
  262. case 'J':
  263. retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_JET);
  264. break;
  265. default:
  266. retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_UNKNOWN);
  267. break;
  268. }
  269. return retval;
  270. }
  271. void PlugIn::distanceToPredictedIaf(const EuroScopePlugIn::CRadarTarget& radarTarget, const EuroScopePlugIn::CFlightPlan& flightPlan,
  272. const EuroScopePlugIn::CPosition& iafPosition, aman::AircraftReport* report) {
  273. std::string_view direct(flightPlan.GetControllerAssignedData().GetDirectToPointName());
  274. if (0 != direct.length()) {
  275. GeoCoordinate directCoordinate;
  276. /* find the coordinate of the direct waypoint */
  277. for (int i = 0; i < flightPlan.GetExtractedRoute().GetPointsNumber(); ++i) {
  278. if (flightPlan.GetExtractedRoute().GetPointName(i) == direct) {
  279. directCoordinate = GeoCoordinate(static_cast<float>(flightPlan.GetExtractedRoute().GetPointPosition(i).m_Longitude) * degree,
  280. static_cast<float>(flightPlan.GetExtractedRoute().GetPointPosition(i).m_Latitude) * degree);
  281. break;
  282. }
  283. }
  284. if (0_m != directCoordinate.distanceTo(GeoCoordinate())) {
  285. const GeographicLib::Gnomonic projection(GeographicLib::Geodesic::WGS84());
  286. Eigen::Vector2f currentCartesian, iafCartesian, directCartesian;
  287. /* convert to Cartesian with IAF as the reference */
  288. projection.Forward(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
  289. static_cast<float>(radarTarget.GetPosition().GetPosition().m_Latitude),
  290. static_cast<float>(radarTarget.GetPosition().GetPosition().m_Longitude),
  291. currentCartesian[0], currentCartesian[1]);
  292. projection.Forward(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
  293. directCoordinate.latitude().convert(degree),
  294. directCoordinate.longitude().convert(degree),
  295. directCartesian[0], directCartesian[1]);
  296. projection.Forward(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
  297. static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
  298. iafCartesian[0], iafCartesian[1]);
  299. /* project IAF on line between current position and direct */
  300. const auto direction = (directCartesian - currentCartesian).normalized();
  301. Eigen::ParametrizedLine<float, 2> line(currentCartesian, direction);
  302. const auto projectCartesian = line.projection(iafCartesian);
  303. const auto distanceStart = (currentCartesian - projectCartesian).squaredNorm();
  304. const auto distanceEnd = (directCartesian - projectCartesian).squaredNorm();
  305. const auto distanceDirect = (currentCartesian - directCartesian).squaredNorm();
  306. /* projection of IAF in front of aircraft and not behind direct */
  307. if (distanceStart <= distanceDirect && distanceEnd <= distanceDirect) {
  308. EuroScopePlugIn::CPosition projected;
  309. float lat = 0.0f, lon = 0.0f;
  310. projection.Reverse(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
  311. projectCartesian[0], projectCartesian[1], lat, lon);
  312. projected.m_Latitude = lat;
  313. projected.m_Longitude = lon;
  314. const auto distanceToProjected = radarTarget.GetPosition().GetPosition().DistanceTo(projected);
  315. report->set_distancetoiaf(static_cast<int>(std::round(distanceToProjected)));
  316. }
  317. else {
  318. report->set_distancetoiaf(0);
  319. }
  320. }
  321. /* did not find the coordinate -> fallback */
  322. else {
  323. report->set_distancetoiaf(0);
  324. }
  325. }
  326. /* in heading mode and passed IAF */
  327. else {
  328. report->set_distancetoiaf(0);
  329. }
  330. }
  331. void PlugIn::generateAircraftReportMessage(EuroScopePlugIn::CRadarTarget& radarTarget, aman::AircraftReport* report) {
  332. const auto flightPlan = radarTarget.GetCorrelatedFlightPlan();
  333. /* ignore invalid flightplans */
  334. if (false == flightPlan.IsValid())
  335. return;
  336. /* ignore flights that are not tracked by the current controller */
  337. if (false == flightPlan.GetTrackingControllerIsMe())
  338. return;
  339. /* ignore non-IFR flights */
  340. if (nullptr == flightPlan.GetFlightPlanData().GetPlanType() || 'I' != *flightPlan.GetFlightPlanData().GetPlanType())
  341. return;
  342. /* filter invalid destinations */
  343. const auto destination = std::string(flightPlan.GetFlightPlanData().GetDestination());
  344. if (4 != destination.length())
  345. return;
  346. /* filter by airborne identifier (assume a GS>50kn and a big distance to the origin) */
  347. if (50 > radarTarget.GetGS() || 10.0 > flightPlan.GetDistanceFromOrigin())
  348. return;
  349. /* generate protobuf message */
  350. aman::Aircraft* aircraft = this->generateAircraftMessage(radarTarget);
  351. if (nullptr == aircraft)
  352. return;
  353. aman::Dynamics* dynamics = new aman::Dynamics();
  354. dynamics->set_altitude(radarTarget.GetPosition().GetFlightLevel());
  355. dynamics->set_heading(radarTarget.GetPosition().GetReportedHeading());
  356. dynamics->set_groundspeed(radarTarget.GetPosition().GetReportedGS());
  357. dynamics->set_verticalspeed(radarTarget.GetVerticalSpeed());
  358. aman::Coordinate* coordinate = new aman::Coordinate();
  359. coordinate->set_latitude(radarTarget.GetPosition().GetPosition().m_Latitude);
  360. coordinate->set_longitude(radarTarget.GetPosition().GetPosition().m_Longitude);
  361. /* create the report */
  362. switch (this->ControllerMyself().GetFacility()) {
  363. case 1:
  364. case 6:
  365. report->set_reportedby(aman::AircraftReport::CENTER);
  366. break;
  367. case 2:
  368. report->set_reportedby(aman::AircraftReport::DELIVERY);
  369. break;
  370. case 3:
  371. report->set_reportedby(aman::AircraftReport::GROUND);
  372. break;
  373. case 4:
  374. report->set_reportedby(aman::AircraftReport::TOWER);
  375. break;
  376. case 5:
  377. report->set_reportedby(aman::AircraftReport::APPROACH);
  378. break;
  379. default:
  380. report->set_reportedby(aman::AircraftReport::UNKNOWN);
  381. break;
  382. }
  383. int iafRouteIndex = flightPlan.GetExtractedRoute().GetPointsNumber();
  384. auto currentPosition = radarTarget.GetPosition().GetPosition();
  385. GeoCoordinate iafPosition;
  386. std::string iafName;
  387. auto iafIt = this->m_initialApproachFixes.find(destination);
  388. if (this->m_initialApproachFixes.cend() != iafIt) {
  389. for (const auto& iaf : std::as_const(iafIt->second)) {
  390. for (int i = 0; i < flightPlan.GetExtractedRoute().GetPointsNumber(); ++i) {
  391. if (flightPlan.GetExtractedRoute().GetPointName(i) == iaf.name) {
  392. iafPosition = iaf.coordinate;
  393. iafName = iaf.name;
  394. break;
  395. }
  396. }
  397. if (0 == iafName.length() && flightPlan.GetControllerAssignedData().GetDirectToPointName() == iaf.name) {
  398. iafPosition = iaf.coordinate;
  399. iafName = iaf.name;
  400. }
  401. if (0 != iafName.length())
  402. break;
  403. }
  404. }
  405. if (0 != iafName.length()) {
  406. report->set_initialapproachfix(iafName);
  407. std::string_view direct(flightPlan.GetControllerAssignedData().GetDirectToPointName());
  408. bool directBehindIAF = false;
  409. if (0 != direct.length()) {
  410. for (int i = 0; i < flightPlan.GetExtractedRoute().GetPointsNumber(); ++i) {
  411. if (direct == flightPlan.GetExtractedRoute().GetPointName(i)) {
  412. directBehindIAF = iafRouteIndex < i;
  413. break;
  414. }
  415. }
  416. }
  417. EuroScopePlugIn::CPosition iaf;
  418. iaf.m_Longitude = iafPosition.longitude().convert(degree);
  419. iaf.m_Latitude = iafPosition.latitude().convert(degree);
  420. if (true == directBehindIAF) {
  421. PlugIn::distanceToPredictedIaf(radarTarget, flightPlan, iaf, report);
  422. }
  423. else {
  424. const Velocity groundspeed = static_cast<float>(radarTarget.GetGS()) * knot;
  425. Length distanceToIaf;
  426. const auto idx = Inbound::matchToPredictedPath(flightPlan.GetPositionPredictions(), iafPosition, groundspeed, distanceToIaf);
  427. /* not inbound the IAF -> check if we passed it */
  428. if (flightPlan.GetPositionPredictions().GetPointsNumber() <= idx)
  429. PlugIn::distanceToPredictedIaf(radarTarget, flightPlan, iaf, report);
  430. else
  431. report->set_distancetoiaf(static_cast<int>(std::round(distanceToIaf.convert(nauticmile))));
  432. }
  433. }
  434. report->set_destination(std::string(destination));
  435. /* support GrPlugin and TST with the stand association */
  436. if (nullptr != radarTarget.GetCorrelatedFlightPlan().GetControllerAssignedData().GetFlightStripAnnotation(6)) {
  437. std::string stand(radarTarget.GetCorrelatedFlightPlan().GetControllerAssignedData().GetFlightStripAnnotation(6));
  438. auto split = String::splitString(stand, "/");
  439. if (3 == split.size() && "s" == gsl::at(split, 0) && "s" == gsl::at(split, 2))
  440. report->set_plannedgate(gsl::at(split, 1));
  441. }
  442. report->set_allocated_aircraft(aircraft);
  443. report->set_allocated_dynamics(dynamics);
  444. report->set_allocated_position(coordinate);
  445. auto selectedRwyIt = this->m_selectedRunway.find(radarTarget.GetCallsign());
  446. if (this->m_selectedRunway.cend() != selectedRwyIt)
  447. report->set_requestedrunway(selectedRwyIt->second);
  448. /* set the report time */
  449. std::stringstream stream;
  450. const auto reportTime = std::chrono::utc_clock::now();
  451. stream << std::format("{0:%Y%m%d%H%M%S}", reportTime);
  452. const auto elements = String::splitString(stream.str(), ".");
  453. report->set_reporttime(gsl::at(elements, 0));
  454. }
  455. bool PlugIn::OnCompileCommand(const char* cmdline) {
  456. std::string message(cmdline);
  457. bool retval = false;
  458. #pragma warning(disable: 4244)
  459. std::transform(message.begin(), message.end(), message.begin(), ::toupper);
  460. #pragma warning(default: 4244)
  461. /* no AMAN command */
  462. if (0 != message.find(".AMAN"))
  463. return retval;
  464. if (std::string::npos != message.find("RELOAD")) {
  465. this->validateBackendData();
  466. retval = true;
  467. }
  468. else if (std::string::npos != message.find("SWEATBOX")) {
  469. this->m_sweatboxValid = !this->m_sweatboxValid;
  470. if (true == this->m_sweatboxValid)
  471. this->m_playbackValid = false;
  472. retval = true;
  473. }
  474. else if (std::string::npos != message.find("PLAYBACK")) {
  475. this->m_playbackValid = !this->m_playbackValid;
  476. if (true == this->m_playbackValid)
  477. this->m_sweatboxValid = false;
  478. retval = true;
  479. }
  480. else if (std::string::npos != message.find("FORCE")) {
  481. const auto elements = String::splitString(message, " ");
  482. if (3 <= elements.size()) {
  483. const auto callsign = elements[2];
  484. const auto radarTarget = this->RadarTargetSelect(callsign.c_str());
  485. if (true == radarTarget.IsValid()) {
  486. std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination());
  487. std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
  488. std::lock_guard updateGuard(this->m_updateQueueLock);
  489. auto it = this->m_updateQueue.find(destination);
  490. if (this->m_updateQueue.end() != it) {
  491. auto cIt = std::find(this->m_forcedToBackendCallsigns.cbegin(), this->m_forcedToBackendCallsigns.cend(), callsign);
  492. if (this->m_forcedToBackendCallsigns.cend() == cIt)
  493. this->m_forcedToBackendCallsigns.push_back(callsign);
  494. else
  495. this->m_forcedToBackendCallsigns.erase(cIt);
  496. }
  497. }
  498. }
  499. }
  500. if (true == retval) {
  501. if (true == this->m_sweatboxValid)
  502. this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Sweatbox data is used in AMAN", true, true, false, false, false);
  503. else
  504. this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Sweatbox is ignored in AMAN", true, true, false, false, false);
  505. if (true == this->m_playbackValid)
  506. this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Playback data is used in AMAN", true, true, false, false, false);
  507. else
  508. this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Playback is ignored in AMAN", true, true, false, false, false);
  509. }
  510. return retval;
  511. }
  512. void PlugIn::OnGetTagItem(EuroScopePlugIn::CFlightPlan flightPlan, EuroScopePlugIn::CRadarTarget radarTarget,
  513. int itemCode, int tagData, char itemString[16], int* colorCode, COLORREF* rgb,
  514. double* fontSize) {
  515. std::ignore = tagData;
  516. std::ignore = rgb;
  517. std::ignore = fontSize;
  518. std::string callsign(radarTarget.GetCallsign());
  519. std::string destination(flightPlan.GetFlightPlanData().GetDestination());
  520. #pragma warning(disable: 4244)
  521. std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
  522. #pragma warning(default: 4244)
  523. bool forced = false;
  524. const gsl::span<char, 16> tagString(itemString, 16UL);
  525. this->m_updateQueueLock.lock();
  526. /* check if the inbound is forced */
  527. if (0 != this->m_forcedToBackendCallsigns.size()) {
  528. const auto cIt = std::find(this->m_forcedToBackendCallsigns.cbegin(), this->m_forcedToBackendCallsigns.cend(), callsign);
  529. forced = this->m_forcedToBackendCallsigns.cend() != cIt;
  530. }
  531. this->m_updateQueueLock.unlock();
  532. std::lock_guard guard(this->m_inboundsQueueLock);
  533. auto it = this->m_inbounds.find(callsign);
  534. std::string message;
  535. switch (static_cast<TagItemElement>(itemCode)) {
  536. case TagItemElement::EstimatedTimeOfArrival:
  537. {
  538. if (this->m_inbounds.cend() != it) {
  539. const auto eta = it->second.eta();
  540. if (UtcTime::Point() == eta)
  541. message = "??:??";
  542. else
  543. message = UtcTime::timeToString(it->second.eta(), "%H:%M");
  544. }
  545. else if (true == forced) {
  546. message = "??:??";
  547. }
  548. break;
  549. }
  550. case TagItemElement::PlannedTimeOfArrival:
  551. {
  552. if (this->m_inbounds.cend() != it) {
  553. const auto pta = it->second.pta();
  554. if (UtcTime::Point() == pta)
  555. message = "??:??";
  556. else
  557. message = UtcTime::timeToString(pta, "%H:%M");
  558. }
  559. else if (true == forced) {
  560. message = "??:??";
  561. }
  562. break;
  563. }
  564. case TagItemElement::DeltaTime:
  565. {
  566. if (this->m_inbounds.cend() != it) {
  567. const auto ttl = static_cast<int>(std::roundf(it->second.timeToLose().convert(second)));
  568. std::stringstream stream;
  569. stream << ttl;
  570. message = stream.str();
  571. }
  572. else if (true == forced) {
  573. message = "??";
  574. }
  575. break;
  576. }
  577. case TagItemElement::FixedPlanIndicator:
  578. if (this->m_inbounds.cend() != it && false == it->second.fixedPlan())
  579. message = "*";
  580. else if (this->m_inbounds.cend() == it && true == forced)
  581. message = "*";
  582. break;
  583. default:
  584. break;
  585. }
  586. if (0 != message.length()) {
  587. std::strcpy(itemString, message.c_str());
  588. *colorCode = EuroScopePlugIn::TAG_COLOR_DEFAULT;
  589. }
  590. }
  591. void PlugIn::OnFunctionCall(int functionId, const char* itemString, POINT pt, RECT area) {
  592. std::ignore = itemString;
  593. std::ignore = pt;
  594. auto radarTarget = this->RadarTargetSelectASEL();
  595. if (false == radarTarget.IsValid() || false == radarTarget.GetCorrelatedFlightPlan().IsValid())
  596. return;
  597. std::string callsign(radarTarget.GetCallsign());
  598. std::lock_guard guardUpdate(this->m_updateQueueLock);
  599. std::lock_guard guardInbound(this->m_inboundsQueueLock);
  600. switch (static_cast<PlugIn::TagItemFunction>(functionId)) {
  601. case PlugIn::TagItemFunction::RunwaySelectMenu:
  602. {
  603. std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination());
  604. std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
  605. auto it = this->m_updateQueue.find(destination);
  606. if (this->m_updateQueue.cend() != it) {
  607. this->OpenPopupList(area, "Select runway", 1);
  608. bool inList = this->m_selectedRunway.find(callsign) != this->m_selectedRunway.cend();
  609. this->AddPopupListElement("Remove", "", static_cast<int>(PlugIn::TagItemFunction::RunwaySelect),
  610. false, EuroScopePlugIn::POPUP_ELEMENT_NO_CHECKBOX, false == inList);
  611. for (const auto& rwy : std::as_const(it->second.arrivalRunways))
  612. this->AddPopupListElement(rwy.c_str(), "", static_cast<int>(PlugIn::TagItemFunction::RunwaySelect));
  613. }
  614. break;
  615. }
  616. case PlugIn::TagItemFunction::RunwaySelect:
  617. {
  618. if (0 == std::strcmp(itemString, "Remove")) {
  619. auto it = this->m_selectedRunway.find(callsign);
  620. if (this->m_selectedRunway.end() != it)
  621. this->m_selectedRunway.erase(it);
  622. }
  623. else {
  624. auto it = this->m_selectedRunway.find(callsign);
  625. if (this->m_selectedRunway.end() != it)
  626. it->second = itemString;
  627. else
  628. this->m_selectedRunway.insert({ callsign, itemString });
  629. }
  630. break;
  631. }
  632. case PlugIn::TagItemFunction::DirectToMenu:
  633. {
  634. auto it = this->m_inbounds.find(callsign);
  635. if (this->m_inbounds.cend() != it && 0 != it->second.arrivalRoute().size()) {
  636. this->OpenPopupList(area, "Direct To", 2);
  637. for (const auto& waypoint : std::as_const(it->second.arrivalRoute())) {
  638. std::string message;
  639. if (it->second.nextWaypoint() == waypoint.name())
  640. message = "-> ";
  641. message += waypoint.name().c_str();
  642. this->AddPopupListElement(message.c_str(), UtcTime::timeToString(waypoint.plannedArrivalTime(), "%H:%M").c_str(),
  643. static_cast<int>(PlugIn::TagItemFunction::DirectTo));
  644. }
  645. }
  646. break;
  647. }
  648. case PlugIn::TagItemFunction::DirectTo:
  649. {
  650. std::string_view message(itemString);
  651. if (0 != message.rfind("-> ", 0)) {
  652. auto it = this->m_inbounds.find(callsign);
  653. if (this->m_inbounds.end() != it)
  654. it->second.directTo(radarTarget, itemString);
  655. }
  656. break;
  657. }
  658. default:
  659. break;
  660. }
  661. }
  662. void PlugIn::addUpdateQueue(EuroScopePlugIn::CRadarTarget& radarTarget) {
  663. std::lock_guard guard(this->m_updateQueueLock);
  664. auto forcedIt = std::find(this->m_forcedToBackendCallsigns.cbegin(), this->m_forcedToBackendCallsigns.cend(), radarTarget.GetCallsign());
  665. if (false == radarTarget.GetCorrelatedFlightPlan().GetTrackingControllerIsMe() && this->m_forcedToBackendCallsigns.cend() == forcedIt)
  666. return;
  667. std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination());
  668. #pragma warning(disable: 4244)
  669. std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
  670. #pragma warning(default: 4244)
  671. auto it = this->m_updateQueue.find(destination);
  672. if (this->m_updateQueue.end() != it) {
  673. /* check that an active arrival runway is selected */
  674. auto selectedIt = this->m_selectedRunway.find(radarTarget.GetCallsign());
  675. if (this->m_selectedRunway.end() != selectedIt) {
  676. auto activeRwyIt = std::find(it->second.arrivalRunways.cbegin(), it->second.arrivalRunways.cend(), selectedIt->second);
  677. if (it->second.arrivalRunways.cend() == activeRwyIt)
  678. this->m_selectedRunway.erase(selectedIt);
  679. }
  680. it->second.inboundUpdates.push_back(radarTarget.GetCallsign());
  681. }
  682. }
  683. void PlugIn::updateInbound(EuroScopePlugIn::CRadarTarget& radarTarget) {
  684. std::lock_guard guard(this->m_inboundsQueueLock);
  685. auto it = this->m_inbounds.find(radarTarget.GetCallsign());
  686. if (this->m_inbounds.end() != it)
  687. it->second.update(radarTarget);
  688. }
  689. void PlugIn::OnRadarTargetPositionUpdate(EuroScopePlugIn::CRadarTarget radarTarget) {
  690. /* do nothing if the reporter is not initialized and ignore invalid targets */
  691. if (false == this->m_compatible || false == Backend::instance().initialized() || false == radarTarget.IsValid())
  692. return;
  693. /* validate the correct connection */
  694. bool validConnection = EuroScopePlugIn::CONNECTION_TYPE_DIRECT == this->GetConnectionType();
  695. validConnection |= true == this->m_sweatboxValid && EuroScopePlugIn::CONNECTION_TYPE_SWEATBOX == this->GetConnectionType();
  696. validConnection |= true == this->m_playbackValid && EuroScopePlugIn::CONNECTION_TYPE_PLAYBACK == this->GetConnectionType();
  697. if (false == validConnection)
  698. return;
  699. this->addUpdateQueue(radarTarget);
  700. this->updateInbound(radarTarget);
  701. }
  702. void PlugIn::OnFlightPlanControllerAssignedDataUpdate(EuroScopePlugIn::CFlightPlan flightPlan, int type) {
  703. auto radarTarget = flightPlan.GetCorrelatedRadarTarget();
  704. if (EuroScopePlugIn::CTR_DATA_TYPE_DIRECT_TO != type || false == radarTarget.IsValid())
  705. return;
  706. std::string waypoint(flightPlan.GetControllerAssignedData().GetDirectToPointName());
  707. if (0 != waypoint.length()) {
  708. std::lock_guard guard(this->m_inboundsQueueLock);
  709. auto it = this->m_inbounds.find(flightPlan.GetCorrelatedRadarTarget().GetCallsign());
  710. if (this->m_inbounds.end() != it)
  711. it->second.directTo(radarTarget, waypoint);
  712. }
  713. }
  714. void PlugIn::OnFlightPlanDisconnect(EuroScopePlugIn::CFlightPlan flightPlan) {
  715. std::string callsign(flightPlan.GetCorrelatedRadarTarget().GetCallsign());
  716. this->m_updateQueueLock.lock();
  717. auto it = std::find(this->m_forcedToBackendCallsigns.begin(), this->m_forcedToBackendCallsigns.end(), callsign);
  718. if (this->m_forcedToBackendCallsigns.end() != it)
  719. this->m_forcedToBackendCallsigns.erase(it);
  720. this->m_updateQueueLock.unlock();
  721. this->m_inboundsQueueLock.lock();
  722. auto inIt = this->m_inbounds.find(flightPlan.GetCorrelatedRadarTarget().GetCallsign());
  723. if (this->m_inbounds.end() != inIt)
  724. this->m_inbounds.erase(inIt);
  725. this->m_inboundsQueueLock.unlock();
  726. }
  727. void PlugIn::updateSequence(std::shared_ptr<aman::AircraftSequence>& sequence) {
  728. /* cleanup the inbound list */
  729. this->m_inboundsQueueLock.lock();
  730. for (auto it = this->m_inbounds.begin(); this->m_inbounds.end() != it;) {
  731. bool found = false;
  732. /* check if the inbound is part of the server */
  733. for (auto i = 0; i < sequence->sequence_size(); ++i) {
  734. if (sequence->sequence(i).callsign() == it->first) {
  735. found = true;
  736. break;
  737. }
  738. }
  739. if (false == found) {
  740. /* delete the forced runway selection */
  741. auto selected = this->m_selectedRunway.find(it->first);
  742. if (this->m_selectedRunway.end() != selected)
  743. this->m_selectedRunway.erase(selected);
  744. it = this->m_inbounds.erase(it);
  745. }
  746. else
  747. ++it;
  748. }
  749. this->m_inboundsQueueLock.unlock();
  750. /* update the inbound list */
  751. for (auto i = 0; i < sequence->sequence_size(); ++i) {
  752. const auto& inbound = sequence->sequence(i);
  753. EuroScopePlugIn::CRadarTarget target;
  754. bool found = false;
  755. for (target = this->RadarTargetSelectFirst(); true == target.IsValid(); target = this->RadarTargetSelectNext(target)) {
  756. if (target.GetCallsign() == inbound.callsign()) {
  757. found = true;
  758. break;
  759. }
  760. }
  761. if (true == found) {
  762. this->m_inboundsQueueLock.lock();
  763. auto it = this->m_inbounds.find(inbound.callsign());
  764. if (this->m_inbounds.end() != it)
  765. it->second.update(target, inbound, sequence->winddata());
  766. else
  767. this->m_inbounds.insert({ inbound.callsign(), Inbound(target, inbound, sequence->winddata()) });
  768. this->m_inboundsQueueLock.unlock();
  769. }
  770. }
  771. }
  772. void PlugIn::OnTimer(int counter) {
  773. /* cleanup the internal data */
  774. if (EuroScopePlugIn::CONNECTION_TYPE_NO == this->GetConnectionType()) {
  775. if (true == this->m_connectedToNetwork) {
  776. std::lock_guard guardUpdate(this->m_updateQueueLock);
  777. for (auto& airport : this->m_updateQueue)
  778. airport.second.inboundUpdates.clear();
  779. std::lock_guard guardInbound(this->m_inboundsQueueLock);
  780. this->m_inbounds.clear();
  781. this->m_connectedToNetwork = false;
  782. }
  783. return;
  784. }
  785. this->m_connectedToNetwork = true;
  786. if (false == this->m_compatible || false == Backend::instance().initialized() || 0 != (counter % 10))
  787. return;
  788. this->m_updateQueueLock.lock();
  789. for (auto& airport : this->m_updateQueue) {
  790. aman::AircraftUpdate update;
  791. update.set_airport(airport.first);
  792. if (0 != airport.second.inboundUpdates.size()) {
  793. for (auto target = this->RadarTargetSelectFirst(); true == target.IsValid(); target = this->RadarTargetSelectNext(target)) {
  794. auto it = std::find(airport.second.inboundUpdates.begin(), airport.second.inboundUpdates.end(), target.GetCallsign());
  795. if (airport.second.inboundUpdates.end() != it) {
  796. auto report = update.add_reports();
  797. this->generateAircraftReportMessage(target, report);
  798. }
  799. }
  800. }
  801. /* send the report and request the current sequence */
  802. auto sequence = Backend::instance().update(update);
  803. if (nullptr == sequence)
  804. this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to send a new aircraft report update", true, true, true, true, true);
  805. else
  806. this->updateSequence(sequence);
  807. airport.second.inboundUpdates.clear();
  808. }
  809. this->m_updateQueueLock.unlock();
  810. }
  811. void PlugIn::OnAirportRunwayActivityChanged() {
  812. std::lock_guard guard(this->m_updateQueueLock);
  813. /* cleanup the arrival airports */
  814. for (auto& airport : this->m_updateQueue)
  815. airport.second.arrivalRunways.clear();
  816. EuroScopePlugIn::CSectorElement rwy;
  817. for (rwy = this->SectorFileElementSelectFirst(EuroScopePlugIn::SECTOR_ELEMENT_RUNWAY); true == rwy.IsValid();
  818. rwy = this->SectorFileElementSelectNext(rwy, EuroScopePlugIn::SECTOR_ELEMENT_RUNWAY)) {
  819. /* remove leading and trailing whitespaces */
  820. std::string airport(rwy.GetAirportName());
  821. airport = std::regex_replace(airport, std::regex("^ +| +$|( ) +"), "$1");
  822. if (true == rwy.IsElementActive(false, 0) || true == rwy.IsElementActive(false, 1)) {
  823. auto airportData = this->m_updateQueue.find(airport);
  824. if (this->m_updateQueue.end() != airportData) {
  825. if (true == rwy.IsElementActive(false, 0))
  826. airportData->second.arrivalRunways.push_back(rwy.GetRunwayName(0));
  827. if (true == rwy.IsElementActive(false, 1))
  828. airportData->second.arrivalRunways.push_back(rwy.GetRunwayName(1));
  829. }
  830. }
  831. }
  832. }