/* * Author: * Sven Czarnian * Brief: * Implements the EuroScope plug-in definition * Copyright: * 2021 Sven Czarnian * License: * GNU General Public License v3 (GPLv3) */ #include "stdafx.h" #include #include #include #include #include #include #include #include #include #include #include #include "com/ZmqContext.h" #include "PlugIn.h" EXTERN_C IMAGE_DOS_HEADER __ImageBase; using namespace aman; static std::string __receivedAmanData; static std::size_t receiveCurl(void* ptr, std::size_t size, std::size_t nmemb, void* stream) { (void)stream; std::string serverResult = static_cast(ptr); __receivedAmanData += serverResult; return size * nmemb; } PlugIn::PlugIn() : EuroScopePlugIn::CPlugIn(EuroScopePlugIn::COMPATIBILITY_CODE, PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_DEVELOPER, PLUGIN_COPYRIGHT), m_configuration(), m_screen(), m_updateQueueLock(), m_updateQueue(), m_forcedToBackendCallsigns(), m_compatible(false) { GOOGLE_PROTOBUF_VERIFY_VERSION; this->DisplayUserMessage(PLUGIN_NAME, "INFO", (std::string("Loaded ") + PLUGIN_NAME + " " + PLUGIN_VERSION).c_str(), true, true, false, false, false); /* get the dll-path */ char path[MAX_PATH + 1] = { 0 }; const gsl::span span(path); GetModuleFileNameA((HINSTANCE)&__ImageBase, span.data(), span.size()); PathRemoveFileSpecA(span.data()); std::string dllPath = span.data(); CommunicationFileFormat comFormat; if (false == comFormat.parse(dllPath + "\\AmanCommunication.txt", this->m_configuration) || true == comFormat.errorFound()) { this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to parse AmanCommunication.txt", true, true, true, true, true); this->DisplayUserMessage(PLUGIN_NAME, "ERROR", ("AmanCommunication.txt:" + std::to_string(comFormat.errorLine()) + " - " + comFormat.errorMessage()).c_str(), true, true, true, true, true); return; } IdentifierFileFormat identFormat; if (false == identFormat.parse(dllPath + "\\AmanIdentity.txt", this->m_configuration) || true == identFormat.errorFound()) { this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to parse AmanIdentity.txt", true, true, true, true, true); this->DisplayUserMessage(PLUGIN_NAME, "ERROR", ("AmanIdentity.txt:" + std::to_string(identFormat.errorLine()) + " - " + identFormat.errorMessage()).c_str(), true, true, true, true, true); return; } ZmqContext::instance().initialize(); if (false == Backend::instance().initialize(this->m_configuration)) this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to initialize the reporter-connection to the backend", true, true, true, true, true); this->validateBackendData(); } PlugIn::~PlugIn() noexcept { Backend::instance().deinitialize(); ZmqContext::instance().deinitialize(); google::protobuf::ShutdownProtobufLibrary(); } void PlugIn::validateBackendData() { std::string url = "http://" + this->m_configuration.address + ":5000/aman/airports"; CURL* curl = curl_easy_init(); CURLcode result; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, static_cast(CURL_HTTP_VERSION_1_1)); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveCurl); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L); struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Accept: */*"); headers = curl_slist_append(headers, "Content-Type: application/json"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); __receivedAmanData.clear(); result = curl_easy_perform(curl); this->m_compatible = false; if (CURLE_OK != result) { MessageBoxA(nullptr, "Unable to receive backend information", "AMAN-Error", MB_OK); return; } /* validate the json data */ Json::Value root; Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); if (false == reader->parse(__receivedAmanData.c_str(), __receivedAmanData.c_str() + __receivedAmanData.length(), &root, nullptr)) { MessageBoxA(nullptr, "Received invalid backend data", "AMAN-Error", MB_OK); return; } std::vector airports; std::string version; for (auto it = root.begin(); root.end() != it; ++it) { if ("version" == it.key().asString()) { version = it->asString(); } else if ("airports" == it.key().asString() && true == it->isArray()) { auto airportArray = *it; airports.reserve(airportArray.size()); for (Json::Value::ArrayIndex i = 0; i < airportArray.size(); ++i) airports.push_back(airportArray[i].asString()); } } /* invalid version or airports info */ auto entries = String::splitString(version, "."); if (3 != entries.size() || 0 == airports.size()) { MessageBoxA(nullptr, "Unable to receive backend information", "AMAN-Error", MB_OK); return; } /* incompatible communication version */ if (PLUGIN_MAJOR_VERSION != std::atoi(entries[0].c_str()) || PLUGIN_MINOR_VERSION != std::atoi(entries[1].c_str())) { MessageBoxA(nullptr, "Plugin version is outdated. An update is required", "AMAN-Error", MB_OK); return; } std::lock_guard guard(this->m_updateQueueLock); this->m_updateQueue.clear(); for (const auto& airport : std::as_const(airports)) this->m_updateQueue.insert({ airport, {} }); this->m_compatible = true; } EuroScopePlugIn::CRadarScreen* PlugIn::OnRadarScreenCreated(const char* displayName, bool needsRadarContent, bool geoReferenced, bool canBeSaved, bool canBeCreated) { std::ignore = needsRadarContent; std::ignore = geoReferenced; std::ignore = canBeSaved; std::ignore = canBeCreated; std::ignore = displayName; if (nullptr == this->m_screen) this->m_screen = std::make_shared(); return this->m_screen.get(); } aman::Aircraft* PlugIn::generateAircraftMessage(const EuroScopePlugIn::CRadarTarget& target) { if (false == target.IsValid() || false == target.GetCorrelatedFlightPlan().IsValid()) return nullptr; auto flightPlan = target.GetCorrelatedFlightPlan(); std::string callsign(target.GetCallsign()); aman::Aircraft* retval = new aman::Aircraft(); /* fill all available information */ retval->set_callsign(callsign); if (3 <= callsign.length()) retval->set_airline(callsign.substr(0, 3)); retval->set_type(flightPlan.GetFlightPlanData().GetAircraftFPType()); retval->set_wtc(std::to_string(flightPlan.GetFlightPlanData().GetAircraftWtc())); /* TODO get recat */ retval->set_enginecount(flightPlan.GetFlightPlanData().GetEngineNumber()); switch (flightPlan.GetFlightPlanData().GetEngineType()) { case 'P': case 'T': retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_TURBOPROB); break; case 'E': retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_ELECTRIC); break; case 'J': retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_JET); break; default: retval->set_enginetype(aman::Aircraft_EngineType::Aircraft_EngineType_UNKNOWN); break; } return retval; } void PlugIn::generateAircraftReportMessage(const EuroScopePlugIn::CRadarTarget& radarTarget, aman::AircraftReport* report) { auto flightPlan = radarTarget.GetCorrelatedFlightPlan(); /* ignore invalid flightplans */ if (false == flightPlan.IsValid()) return; /* ignore flights that are not tracked by the current controller */ if (false == flightPlan.GetTrackingControllerIsMe()) return; /* ignore non-IFR flights */ if (nullptr == flightPlan.GetFlightPlanData().GetPlanType() || 'I' != *flightPlan.GetFlightPlanData().GetPlanType()) return; /* filter invalid destinations */ auto destination = std::string_view(flightPlan.GetFlightPlanData().GetDestination()); if (4 != destination.length()) return; /* filter by distance to destination */ double distanceNM = flightPlan.GetDistanceToDestination(); if (5.0 > distanceNM || 250.0 < distanceNM) return; /* filter by airborne identifier (assume a GS>50kn and a big distance to the origin) */ if (50 > radarTarget.GetGS() || 10.0 > flightPlan.GetDistanceFromOrigin()) return; /* generate protobuf message */ aman::Aircraft* aircraft = this->generateAircraftMessage(radarTarget); if (nullptr == aircraft) return; aman::Dynamics* dynamics = new aman::Dynamics(); dynamics->set_altitude(radarTarget.GetPosition().GetFlightLevel()); dynamics->set_heading(radarTarget.GetPosition().GetReportedHeading()); dynamics->set_groundspeed(radarTarget.GetPosition().GetReportedGS()); dynamics->set_verticalspeed(radarTarget.GetVerticalSpeed()); aman::Coordinate* coordinate = new aman::Coordinate(); coordinate->set_latitude(radarTarget.GetPosition().GetPosition().m_Latitude); coordinate->set_longitude(radarTarget.GetPosition().GetPosition().m_Longitude); /* create the report */ switch (this->ControllerMyself().GetFacility()) { case 1: case 6: report->set_reportedby(aman::AircraftReport::CENTER); break; case 2: report->set_reportedby(aman::AircraftReport::DELIVERY); break; case 3: report->set_reportedby(aman::AircraftReport::GROUND); break; case 4: report->set_reportedby(aman::AircraftReport::TOWER); break; case 5: report->set_reportedby(aman::AircraftReport::APPROACH); break; default: report->set_reportedby(aman::AircraftReport::UNKNOWN); break; } auto currentPosition = radarTarget.GetPosition().GetPosition(); EuroScopePlugIn::CPosition iafPosition; std::string iafName; for (auto element = this->SectorFileElementSelectFirst(EuroScopePlugIn::SECTOR_ELEMENT_STAR); true == element.IsValid(); element = this->SectorFileElementSelectNext(element, EuroScopePlugIn::SECTOR_ELEMENT_STAR)) { auto split = String::splitString(element.GetName(), " "); /* find the correct star */ if (0 != split.size() && destination == split[0]) { /* get the IAF */ EuroScopePlugIn::CPosition position; if (true == element.GetPosition(&position, 0)) { /* match the waypoints to get the name*/ for (int i = 0; i < flightPlan.GetExtractedRoute().GetPointsNumber(); ++i) { if (1.0f >= flightPlan.GetExtractedRoute().GetPointPosition(i).DistanceTo(position)) { iafPosition = flightPlan.GetExtractedRoute().GetPointPosition(i); iafName = flightPlan.GetExtractedRoute().GetPointName(i); report->set_initialapproachfix(iafName); break; } } } } if (0 != iafName.length()) break; } if (0 != iafName.length()) { auto iafDistance = currentPosition.DistanceTo(iafPosition); /* calculate the distance to the IAF */ double distanceToIaf = 0.0f; for (int i = 1; i < flightPlan.GetPositionPredictions().GetPointsNumber(); ++i) { double distance = flightPlan.GetPositionPredictions().GetPosition(i).DistanceTo(currentPosition); double headingDelta = std::abs(radarTarget.GetPosition().GetReportedHeading() - flightPlan.GetPositionPredictions().GetPosition(i).DirectionTo(iafPosition)); /* * 1. no direct way to IAF -> some direct given -> stop after lateral passing * 2. passed the IAF on a way to a direct */ if ((90.0 < headingDelta && 270.0 > headingDelta) || iafDistance < distance) break; distanceToIaf += distance; currentPosition = flightPlan.GetPositionPredictions().GetPosition(i); } if (0.01f >= std::abs(distanceToIaf)) distanceToIaf = iafDistance; report->set_distancetoiaf(static_cast(std::round(distanceToIaf))); } report->set_destination(std::string(destination)); /* support GrPlugin and TST with the stand association */ if (nullptr != radarTarget.GetCorrelatedFlightPlan().GetControllerAssignedData().GetFlightStripAnnotation(6)) { std::string stand(radarTarget.GetCorrelatedFlightPlan().GetControllerAssignedData().GetFlightStripAnnotation(6)); auto split = String::splitString(stand, "/"); if (3 == split.size() && "s" == gsl::at(split, 0) && "s" == gsl::at(split, 2)) report->set_plannedgate(gsl::at(split, 1)); } report->set_allocated_aircraft(aircraft); report->set_allocated_dynamics(dynamics); report->set_allocated_position(coordinate); /* set the report time */ std::stringstream stream; auto reportTime = std::chrono::utc_clock::now(); stream << std::format("{0:%Y%m%d%H%M%S}", reportTime); report->set_reporttime(String::splitString(stream.str(), ".")[0]); } void PlugIn::OnFunctionCall(int functionId, const char* itemString, POINT pt, RECT area) { std::ignore = itemString; std::ignore = pt; std::ignore = area; auto radarTarget = this->RadarTargetSelectASEL(); if (false == radarTarget.IsValid() || false == radarTarget.GetCorrelatedFlightPlan().IsValid()) return; std::string callsign(radarTarget.GetCallsign()); switch (static_cast(functionId)) { case PlugIn::TagItemFunction::ForceToBackend: { std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination()); std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper); std::lock_guard guard(this->m_updateQueueLock); auto it = this->m_updateQueue.find(destination); if (this->m_updateQueue.end() != it) { auto cIt = std::find(this->m_forcedToBackendCallsigns.cbegin(), this->m_forcedToBackendCallsigns.cend(), callsign); if (this->m_forcedToBackendCallsigns.cend() == cIt) this->m_forcedToBackendCallsigns.push_back(callsign); } break; } default: break; } } void PlugIn::OnRadarTargetPositionUpdate(EuroScopePlugIn::CRadarTarget radarTarget) { /* do nothing if the reporter is not initialized and ignore invalid targets */ if (false == this->m_compatible || false == Backend::instance().initialized() || false == radarTarget.IsValid()) return; std::lock_guard guard(this->m_updateQueueLock); auto forcedIt = std::find(this->m_forcedToBackendCallsigns.cbegin(), this->m_forcedToBackendCallsigns.cend(), radarTarget.GetCallsign()); if (false == radarTarget.GetCorrelatedFlightPlan().GetTrackingControllerIsMe() && this->m_forcedToBackendCallsigns.cend() == forcedIt) return; std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination()); #pragma warning(disable: 4244) std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper); #pragma warning(default: 4244) auto it = this->m_updateQueue.find(destination); if (this->m_updateQueue.end() != it) it->second.push_back(radarTarget.GetCallsign()); } void PlugIn::OnFlightPlanDisconnect(EuroScopePlugIn::CFlightPlan flightPlan) { std::string callsign(flightPlan.GetCorrelatedRadarTarget().GetCallsign()); std::lock_guard guard(this->m_updateQueueLock); auto it = std::find(this->m_forcedToBackendCallsigns.begin(), this->m_forcedToBackendCallsigns.end(), callsign); if (this->m_forcedToBackendCallsigns.end() != it) this->m_forcedToBackendCallsigns.erase(it); } void PlugIn::OnTimer(int counter) { if (false == this->m_compatible || false == Backend::instance().initialized() || 0 != (counter % 10)) return; this->m_updateQueueLock.lock(); for (auto& airport : this->m_updateQueue) { aman::AircraftUpdate update; bool inserted = false; for (auto target = this->RadarTargetSelectFirst(); true == target.IsValid(); target = this->RadarTargetSelectNext(target)) { auto it = std::find(airport.second.begin(), airport.second.end(), target.GetCallsign()); if (airport.second.end() != it) { auto report = update.add_reports(); this->generateAircraftReportMessage(target, report); inserted = true; } } /* send the report */ auto sequence = Backend::instance().update(update); if (true == inserted && nullptr != sequence) this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to send a new aircraft report update", true, true, true, true, true); airport.second.clear(); } this->m_updateQueue.clear(); this->m_updateQueueLock.unlock(); }