923 lines
37 KiB
C++
923 lines
37 KiB
C++
/*
|
|
* Author:
|
|
* Sven Czarnian <devel@svcz.de>
|
|
* Brief:
|
|
* Implements the EuroScope plug-in definition
|
|
* Copyright:
|
|
* 2021 Sven Czarnian
|
|
* License:
|
|
* GNU General Public License v3 (GPLv3)
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <regex>
|
|
|
|
#include <gsl/gsl>
|
|
#include <Shlwapi.h>
|
|
#include <Windows.h>
|
|
|
|
#include <curl/curl.h>
|
|
#pragma warning(push, 0)
|
|
#include <Eigen/Geometry>
|
|
#pragma warning(pop)
|
|
#include <GeographicLib/Gnomonic.hpp>
|
|
#include <json/json.h>
|
|
|
|
#include <aman/com/Backend.h>
|
|
#include <aman/config/CommunicationFileFormat.h>
|
|
#include <aman/config/IdentifierFileFormat.h>
|
|
#include <aman/helper/String.h>
|
|
#include <aman/types/GeoCoordinate.h>
|
|
|
|
#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) {
|
|
std::ignore = stream;
|
|
std::string serverResult = static_cast<char*>(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_inboundsQueueLock(),
|
|
m_inbounds(),
|
|
m_forcedToBackendCallsigns(),
|
|
m_compatible(false),
|
|
m_connectedToNetwork(false),
|
|
m_sweatboxValid(false),
|
|
m_playbackValid(false) {
|
|
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
|
|
|
this->RegisterTagItemType("ETA", static_cast<int>(PlugIn::TagItemElement::EstimatedTimeOfArrival));
|
|
this->RegisterTagItemType("PTA", static_cast<int>(PlugIn::TagItemElement::PlannedTimeOfArrival));
|
|
this->RegisterTagItemType("Delta time", static_cast<int>(PlugIn::TagItemElement::DeltaTime));
|
|
this->RegisterTagItemType("Fixed plan indicator", static_cast<int>(PlugIn::TagItemElement::FixedPlanIndicator));
|
|
this->RegisterTagItemFunction("Runway selection", static_cast<int>(PlugIn::TagItemFunction::RunwaySelectMenu));
|
|
this->RegisterTagItemFunction("Direct to", static_cast<int>(PlugIn::TagItemFunction::DirectToMenu));
|
|
|
|
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<char, MAX_PATH + 1> 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() {
|
|
Backend::instance().deinitialize();
|
|
ZmqContext::instance().deinitialize();
|
|
google::protobuf::ShutdownProtobufLibrary();
|
|
}
|
|
|
|
void PlugIn::validateBackendData() {
|
|
if (false == this->m_configuration.valid) {
|
|
this->m_compatible = false;
|
|
return;
|
|
}
|
|
|
|
/* set up the URL */
|
|
std::string url;
|
|
if (true == this->m_configuration.httpsProtocol)
|
|
url += "https://";
|
|
else
|
|
url += "http://";
|
|
url += this->m_configuration.address + ":" + std::to_string(this->m_configuration.portRestAPI) + "/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<long>(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<Json::CharReader> 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<std::string> 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(gsl::at(entries, 0).c_str()) || PLUGIN_MINOR_VERSION != std::atoi(gsl::at(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;
|
|
|
|
this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Reloaded AMAN-configuration", true, true, false, false, false);
|
|
}
|
|
|
|
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<RadarScreen>();
|
|
|
|
this->OnAirportRunwayActivityChanged();
|
|
|
|
return this->m_screen.get();
|
|
}
|
|
|
|
aman::Aircraft* PlugIn::generateAircraftMessage(const EuroScopePlugIn::CRadarTarget& target) const {
|
|
if (false == target.IsValid() || false == target.GetCorrelatedFlightPlan().IsValid())
|
|
return nullptr;
|
|
|
|
const 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());
|
|
|
|
char wtcMessage[2] = { flightPlan.GetFlightPlanData().GetAircraftWtc(), '\0' };
|
|
retval->set_wtc(wtcMessage);
|
|
|
|
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::distanceToPredictedIaf(const EuroScopePlugIn::CRadarTarget& radarTarget, const EuroScopePlugIn::CFlightPlan& flightPlan,
|
|
const EuroScopePlugIn::CPosition& iafPosition, aman::AircraftReport* report) {
|
|
std::string_view direct(flightPlan.GetControllerAssignedData().GetDirectToPointName());
|
|
|
|
if (0 != direct.length()) {
|
|
GeoCoordinate directCoordinate;
|
|
|
|
/* find the coordinate of the direct waypoint */
|
|
for (int i = 0; i < flightPlan.GetExtractedRoute().GetPointsNumber(); ++i) {
|
|
if (flightPlan.GetExtractedRoute().GetPointName(i) == direct) {
|
|
directCoordinate = GeoCoordinate(static_cast<float>(flightPlan.GetExtractedRoute().GetPointPosition(i).m_Longitude) * degree,
|
|
static_cast<float>(flightPlan.GetExtractedRoute().GetPointPosition(i).m_Latitude) * degree);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (0_m != directCoordinate.distanceTo(GeoCoordinate())) {
|
|
const GeographicLib::Gnomonic projection(GeographicLib::Geodesic::WGS84());
|
|
Eigen::Vector2f currentCartesian, iafCartesian, directCartesian;
|
|
|
|
/* convert to Cartesian with IAF as the reference */
|
|
projection.Forward(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
|
|
static_cast<float>(radarTarget.GetPosition().GetPosition().m_Latitude),
|
|
static_cast<float>(radarTarget.GetPosition().GetPosition().m_Longitude),
|
|
currentCartesian[0], currentCartesian[1]);
|
|
projection.Forward(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
|
|
directCoordinate.latitude().convert(degree),
|
|
directCoordinate.longitude().convert(degree),
|
|
directCartesian[0], directCartesian[1]);
|
|
projection.Forward(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
|
|
static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
|
|
iafCartesian[0], iafCartesian[1]);
|
|
|
|
/* project IAF on line between current position and direct */
|
|
const auto direction = (directCartesian - currentCartesian).normalized();
|
|
Eigen::ParametrizedLine<float, 2> line(currentCartesian, direction);
|
|
const auto projectCartesian = line.projection(iafCartesian);
|
|
|
|
const auto distanceStart = (currentCartesian - projectCartesian).squaredNorm();
|
|
const auto distanceEnd = (directCartesian - projectCartesian).squaredNorm();
|
|
const auto distanceDirect = (currentCartesian - directCartesian).squaredNorm();
|
|
|
|
/* projection of IAF in front of aircraft and not behind direct */
|
|
if (distanceStart <= distanceDirect && distanceEnd <= distanceDirect) {
|
|
EuroScopePlugIn::CPosition projected;
|
|
float lat = 0.0f, lon = 0.0f;
|
|
|
|
projection.Reverse(static_cast<float>(iafPosition.m_Latitude), static_cast<float>(iafPosition.m_Longitude),
|
|
projectCartesian[0], projectCartesian[1], lat, lon);
|
|
projected.m_Latitude = lat;
|
|
projected.m_Longitude = lon;
|
|
|
|
const auto distanceToProjected = radarTarget.GetPosition().GetPosition().DistanceTo(projected);
|
|
report->set_distancetoiaf(static_cast<int>(std::round(distanceToProjected)));
|
|
}
|
|
else {
|
|
report->set_distancetoiaf(0);
|
|
}
|
|
}
|
|
/* did not find the coordinate -> fallback */
|
|
else {
|
|
report->set_distancetoiaf(0);
|
|
}
|
|
}
|
|
/* in heading mode and passed IAF */
|
|
else {
|
|
report->set_distancetoiaf(0);
|
|
}
|
|
}
|
|
|
|
void PlugIn::generateAircraftReportMessage(EuroScopePlugIn::CRadarTarget& radarTarget, aman::AircraftReport* report) {
|
|
const 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 */
|
|
const auto destination = std::string_view(flightPlan.GetFlightPlanData().GetDestination());
|
|
if (4 != destination.length())
|
|
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;
|
|
}
|
|
|
|
int iafRouteIndex = flightPlan.GetExtractedRoute().GetPointsNumber();
|
|
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 == gsl::at(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);
|
|
iafRouteIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (0 != iafName.length())
|
|
break;
|
|
}
|
|
|
|
if (0 != iafName.length()) {
|
|
const std::string_view direct(flightPlan.GetControllerAssignedData().GetDirectToPointName());
|
|
bool directBehindIAF = false;
|
|
|
|
if (0 != direct.length()) {
|
|
for (int i = 0; i < flightPlan.GetExtractedRoute().GetPointsNumber(); ++i) {
|
|
if (direct == flightPlan.GetExtractedRoute().GetPointName(i)) {
|
|
directBehindIAF = iafRouteIndex < i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (true == directBehindIAF) {
|
|
PlugIn::distanceToPredictedIaf(radarTarget, flightPlan, iafPosition, report);
|
|
}
|
|
else {
|
|
const GeoCoordinate iaf(static_cast<float>(iafPosition.m_Longitude) * degree, static_cast<float>(iafPosition.m_Latitude) * degree);
|
|
const Velocity groundspeed = static_cast<float>(radarTarget.GetGS()) * knot;
|
|
Length distanceToIaf;
|
|
const auto idx = Inbound::matchToPredictedPath(flightPlan.GetPositionPredictions(), iaf, groundspeed, distanceToIaf);
|
|
|
|
/* not inbound the IAF -> check if we passed it */
|
|
if (flightPlan.GetPositionPredictions().GetPointsNumber() <= idx)
|
|
PlugIn::distanceToPredictedIaf(radarTarget, flightPlan, iafPosition, report);
|
|
else
|
|
report->set_distancetoiaf(static_cast<int>(std::round(distanceToIaf.convert(nauticmile))));
|
|
}
|
|
}
|
|
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);
|
|
|
|
auto selectedRwyIt = this->m_selectedRunway.find(radarTarget.GetCallsign());
|
|
if (this->m_selectedRunway.cend() != selectedRwyIt)
|
|
report->set_requestedrunway(selectedRwyIt->second);
|
|
|
|
/* set the report time */
|
|
std::stringstream stream;
|
|
const auto reportTime = std::chrono::utc_clock::now();
|
|
stream << std::format("{0:%Y%m%d%H%M%S}", reportTime);
|
|
const auto elements = String::splitString(stream.str(), ".");
|
|
report->set_reporttime(gsl::at(elements, 0));
|
|
}
|
|
|
|
bool PlugIn::OnCompileCommand(const char* cmdline) {
|
|
std::string message(cmdline);
|
|
bool retval = false;
|
|
|
|
#pragma warning(disable: 4244)
|
|
std::transform(message.begin(), message.end(), message.begin(), ::toupper);
|
|
#pragma warning(default: 4244)
|
|
|
|
/* no AMAN command */
|
|
if (0 != message.find(".AMAN"))
|
|
return retval;
|
|
|
|
if (std::string::npos != message.find("RELOAD")) {
|
|
this->validateBackendData();
|
|
retval = true;
|
|
}
|
|
else if (std::string::npos != message.find("SWEATBOX")) {
|
|
this->m_sweatboxValid = !this->m_sweatboxValid;
|
|
if (true == this->m_sweatboxValid)
|
|
this->m_playbackValid = false;
|
|
retval = true;
|
|
}
|
|
else if (std::string::npos != message.find("PLAYBACK")) {
|
|
this->m_playbackValid = !this->m_playbackValid;
|
|
if (true == this->m_playbackValid)
|
|
this->m_sweatboxValid = false;
|
|
retval = true;
|
|
}
|
|
else if (std::string::npos != message.find("FORCE")) {
|
|
const auto elements = String::splitString(message, " ");
|
|
|
|
if (3 <= elements.size()) {
|
|
const auto callsign = elements[2];
|
|
|
|
const auto radarTarget = this->RadarTargetSelect(callsign.c_str());
|
|
|
|
if (true == radarTarget.IsValid()) {
|
|
std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination());
|
|
std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
|
|
std::lock_guard updateGuard(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);
|
|
else
|
|
this->m_forcedToBackendCallsigns.erase(cIt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (true == retval) {
|
|
if (true == this->m_sweatboxValid)
|
|
this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Sweatbox data is used in AMAN", true, true, false, false, false);
|
|
else
|
|
this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Sweatbox is ignored in AMAN", true, true, false, false, false);
|
|
|
|
if (true == this->m_playbackValid)
|
|
this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Playback data is used in AMAN", true, true, false, false, false);
|
|
else
|
|
this->DisplayUserMessage(PLUGIN_NAME, "INFO", "Playback is ignored in AMAN", true, true, false, false, false);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
void PlugIn::OnGetTagItem(EuroScopePlugIn::CFlightPlan flightPlan, EuroScopePlugIn::CRadarTarget radarTarget,
|
|
int itemCode, int tagData, char itemString[16], int* colorCode, COLORREF* rgb,
|
|
double* fontSize) {
|
|
std::ignore = tagData;
|
|
std::ignore = rgb;
|
|
std::ignore = fontSize;
|
|
|
|
std::string callsign(radarTarget.GetCallsign());
|
|
std::string destination(flightPlan.GetFlightPlanData().GetDestination());
|
|
#pragma warning(disable: 4244)
|
|
std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
|
|
#pragma warning(default: 4244)
|
|
|
|
bool forced = false;
|
|
const gsl::span<char, 16> tagString(itemString, 16UL);
|
|
|
|
this->m_updateQueueLock.lock();
|
|
|
|
/* check if the inbound is forced */
|
|
if (0 != this->m_forcedToBackendCallsigns.size()) {
|
|
const auto cIt = std::find(this->m_forcedToBackendCallsigns.cbegin(), this->m_forcedToBackendCallsigns.cend(), callsign);
|
|
forced = this->m_forcedToBackendCallsigns.cend() != cIt;
|
|
}
|
|
|
|
this->m_updateQueueLock.unlock();
|
|
|
|
std::lock_guard guard(this->m_inboundsQueueLock);
|
|
auto it = this->m_inbounds.find(callsign);
|
|
|
|
std::string message;
|
|
switch (static_cast<TagItemElement>(itemCode)) {
|
|
case TagItemElement::EstimatedTimeOfArrival:
|
|
{
|
|
if (this->m_inbounds.cend() != it) {
|
|
const auto eta = it->second.eta();
|
|
if (UtcTime::Point() == eta)
|
|
message = "??:??";
|
|
else
|
|
message = UtcTime::timeToString(it->second.eta(), "%H:%M");
|
|
}
|
|
else if (true == forced) {
|
|
message = "??:??";
|
|
}
|
|
break;
|
|
}
|
|
case TagItemElement::PlannedTimeOfArrival:
|
|
{
|
|
if (this->m_inbounds.cend() != it) {
|
|
const auto pta = it->second.pta();
|
|
if (UtcTime::Point() == pta)
|
|
message = "??:??";
|
|
else
|
|
message = UtcTime::timeToString(pta, "%H:%M");
|
|
}
|
|
else if (true == forced) {
|
|
message = "??:??";
|
|
}
|
|
break;
|
|
}
|
|
case TagItemElement::DeltaTime:
|
|
{
|
|
if (this->m_inbounds.cend() != it) {
|
|
const auto ttl = static_cast<int>(std::roundf(it->second.timeToLose().convert(second)));
|
|
std::stringstream stream;
|
|
stream << ttl;
|
|
message = stream.str();
|
|
}
|
|
else if (true == forced) {
|
|
message = "??";
|
|
}
|
|
break;
|
|
}
|
|
case TagItemElement::FixedPlanIndicator:
|
|
if (this->m_inbounds.cend() != it && false == it->second.fixedPlan())
|
|
message = "*";
|
|
else if (this->m_inbounds.cend() == it && true == forced)
|
|
message = "*";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (0 != message.length()) {
|
|
std::strcpy(itemString, message.c_str());
|
|
*colorCode = EuroScopePlugIn::TAG_COLOR_DEFAULT;
|
|
}
|
|
}
|
|
|
|
void PlugIn::OnFunctionCall(int functionId, const char* itemString, POINT pt, RECT area) {
|
|
std::ignore = itemString;
|
|
std::ignore = pt;
|
|
|
|
auto radarTarget = this->RadarTargetSelectASEL();
|
|
if (false == radarTarget.IsValid() || false == radarTarget.GetCorrelatedFlightPlan().IsValid())
|
|
return;
|
|
|
|
std::string callsign(radarTarget.GetCallsign());
|
|
|
|
std::lock_guard guardUpdate(this->m_updateQueueLock);
|
|
std::lock_guard guardInbound(this->m_inboundsQueueLock);
|
|
|
|
switch (static_cast<PlugIn::TagItemFunction>(functionId)) {
|
|
case PlugIn::TagItemFunction::RunwaySelectMenu:
|
|
{
|
|
std::string destination(radarTarget.GetCorrelatedFlightPlan().GetFlightPlanData().GetDestination());
|
|
std::transform(destination.begin(), destination.end(), destination.begin(), ::toupper);
|
|
auto it = this->m_updateQueue.find(destination);
|
|
|
|
if (this->m_updateQueue.cend() != it) {
|
|
this->OpenPopupList(area, "Select runway", 1);
|
|
bool inList = this->m_selectedRunway.find(callsign) != this->m_selectedRunway.cend();
|
|
this->AddPopupListElement("Remove", "", static_cast<int>(PlugIn::TagItemFunction::RunwaySelect),
|
|
false, EuroScopePlugIn::POPUP_ELEMENT_NO_CHECKBOX, false == inList);
|
|
|
|
for (const auto& rwy : std::as_const(it->second.arrivalRunways))
|
|
this->AddPopupListElement(rwy.c_str(), "", static_cast<int>(PlugIn::TagItemFunction::RunwaySelect));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PlugIn::TagItemFunction::RunwaySelect:
|
|
{
|
|
if (0 == std::strcmp(itemString, "Remove")) {
|
|
auto it = this->m_selectedRunway.find(callsign);
|
|
if (this->m_selectedRunway.end() != it)
|
|
this->m_selectedRunway.erase(it);
|
|
}
|
|
else {
|
|
auto it = this->m_selectedRunway.find(callsign);
|
|
if (this->m_selectedRunway.end() != it)
|
|
it->second = itemString;
|
|
else
|
|
this->m_selectedRunway.insert({ callsign, itemString });
|
|
}
|
|
break;
|
|
}
|
|
case PlugIn::TagItemFunction::DirectToMenu:
|
|
{
|
|
auto it = this->m_inbounds.find(callsign);
|
|
|
|
if (this->m_inbounds.cend() != it && 0 != it->second.arrivalRoute().size()) {
|
|
this->OpenPopupList(area, "Direct To", 2);
|
|
|
|
for (const auto& waypoint : std::as_const(it->second.arrivalRoute())) {
|
|
std::string message;
|
|
|
|
if (it->second.nextWaypoint() == waypoint.name())
|
|
message = "-> ";
|
|
message += waypoint.name().c_str();
|
|
|
|
this->AddPopupListElement(message.c_str(), UtcTime::timeToString(waypoint.plannedArrivalTime(), "%H:%M").c_str(),
|
|
static_cast<int>(PlugIn::TagItemFunction::DirectTo));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PlugIn::TagItemFunction::DirectTo:
|
|
{
|
|
std::string_view message(itemString);
|
|
if (0 != message.rfind("-> ", 0)) {
|
|
auto it = this->m_inbounds.find(callsign);
|
|
|
|
if (this->m_inbounds.end() != it)
|
|
it->second.directTo(radarTarget, itemString);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void PlugIn::addUpdateQueue(EuroScopePlugIn::CRadarTarget& radarTarget) {
|
|
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) {
|
|
/* check that an active arrival runway is selected */
|
|
auto selectedIt = this->m_selectedRunway.find(radarTarget.GetCallsign());
|
|
if (this->m_selectedRunway.end() != selectedIt) {
|
|
auto activeRwyIt = std::find(it->second.arrivalRunways.cbegin(), it->second.arrivalRunways.cend(), selectedIt->second);
|
|
if (it->second.arrivalRunways.cend() == activeRwyIt)
|
|
this->m_selectedRunway.erase(selectedIt);
|
|
}
|
|
|
|
it->second.inboundUpdates.push_back(radarTarget.GetCallsign());
|
|
}
|
|
}
|
|
|
|
void PlugIn::updateInbound(EuroScopePlugIn::CRadarTarget& radarTarget) {
|
|
std::lock_guard guard(this->m_inboundsQueueLock);
|
|
auto it = this->m_inbounds.find(radarTarget.GetCallsign());
|
|
if (this->m_inbounds.end() != it)
|
|
it->second.update(radarTarget);
|
|
}
|
|
|
|
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;
|
|
|
|
/* validate the correct connection */
|
|
bool validConnection = EuroScopePlugIn::CONNECTION_TYPE_DIRECT == this->GetConnectionType();
|
|
validConnection |= true == this->m_sweatboxValid && EuroScopePlugIn::CONNECTION_TYPE_SWEATBOX == this->GetConnectionType();
|
|
validConnection |= true == this->m_playbackValid && EuroScopePlugIn::CONNECTION_TYPE_PLAYBACK == this->GetConnectionType();
|
|
if (false == validConnection)
|
|
return;
|
|
|
|
this->addUpdateQueue(radarTarget);
|
|
this->updateInbound(radarTarget);
|
|
}
|
|
|
|
void PlugIn::OnFlightPlanControllerAssignedDataUpdate(EuroScopePlugIn::CFlightPlan flightPlan, int type) {
|
|
auto radarTarget = flightPlan.GetCorrelatedRadarTarget();
|
|
|
|
if (EuroScopePlugIn::CTR_DATA_TYPE_DIRECT_TO != type || false == radarTarget.IsValid())
|
|
return;
|
|
|
|
std::string waypoint(flightPlan.GetControllerAssignedData().GetDirectToPointName());
|
|
if (0 != waypoint.length()) {
|
|
std::lock_guard guard(this->m_inboundsQueueLock);
|
|
auto it = this->m_inbounds.find(flightPlan.GetCorrelatedRadarTarget().GetCallsign());
|
|
if (this->m_inbounds.end() != it)
|
|
it->second.directTo(radarTarget, waypoint);
|
|
}
|
|
}
|
|
|
|
void PlugIn::OnFlightPlanDisconnect(EuroScopePlugIn::CFlightPlan flightPlan) {
|
|
std::string callsign(flightPlan.GetCorrelatedRadarTarget().GetCallsign());
|
|
|
|
this->m_updateQueueLock.lock();
|
|
auto it = std::find(this->m_forcedToBackendCallsigns.begin(), this->m_forcedToBackendCallsigns.end(), callsign);
|
|
if (this->m_forcedToBackendCallsigns.end() != it)
|
|
this->m_forcedToBackendCallsigns.erase(it);
|
|
this->m_updateQueueLock.unlock();
|
|
|
|
this->m_inboundsQueueLock.lock();
|
|
auto inIt = this->m_inbounds.find(flightPlan.GetCorrelatedRadarTarget().GetCallsign());
|
|
if (this->m_inbounds.end() != inIt)
|
|
this->m_inbounds.erase(inIt);
|
|
this->m_inboundsQueueLock.unlock();
|
|
}
|
|
|
|
void PlugIn::updateSequence(std::shared_ptr<aman::AircraftSequence>& sequence) {
|
|
/* cleanup the inbound list */
|
|
this->m_inboundsQueueLock.lock();
|
|
for (auto it = this->m_inbounds.begin(); this->m_inbounds.end() != it;) {
|
|
bool found = false;
|
|
|
|
/* check if the inbound is part of the server */
|
|
for (auto i = 0; i < sequence->sequence_size(); ++i) {
|
|
if (sequence->sequence(i).callsign() == it->first) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (false == found) {
|
|
/* delete the forced runway selection */
|
|
auto selected = this->m_selectedRunway.find(it->first);
|
|
if (this->m_selectedRunway.end() != selected)
|
|
this->m_selectedRunway.erase(selected);
|
|
|
|
it = this->m_inbounds.erase(it);
|
|
}
|
|
else
|
|
++it;
|
|
}
|
|
this->m_inboundsQueueLock.unlock();
|
|
|
|
/* update the inbound list */
|
|
for (auto i = 0; i < sequence->sequence_size(); ++i) {
|
|
const auto& inbound = sequence->sequence(i);
|
|
EuroScopePlugIn::CRadarTarget target;
|
|
bool found = false;
|
|
|
|
for (target = this->RadarTargetSelectFirst(); true == target.IsValid(); target = this->RadarTargetSelectNext(target)) {
|
|
if (target.GetCallsign() == inbound.callsign()) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (true == found) {
|
|
this->m_inboundsQueueLock.lock();
|
|
auto it = this->m_inbounds.find(inbound.callsign());
|
|
if (this->m_inbounds.end() != it)
|
|
it->second.update(target, inbound, sequence->winddata());
|
|
else
|
|
this->m_inbounds.insert({ inbound.callsign(), Inbound(target, inbound, sequence->winddata()) });
|
|
this->m_inboundsQueueLock.unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlugIn::OnTimer(int counter) {
|
|
/* cleanup the internal data */
|
|
if (EuroScopePlugIn::CONNECTION_TYPE_NO == this->GetConnectionType()) {
|
|
if (true == this->m_connectedToNetwork) {
|
|
std::lock_guard guardUpdate(this->m_updateQueueLock);
|
|
for (auto& airport : this->m_updateQueue)
|
|
airport.second.inboundUpdates.clear();
|
|
|
|
std::lock_guard guardInbound(this->m_inboundsQueueLock);
|
|
this->m_inbounds.clear();
|
|
|
|
this->m_connectedToNetwork = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
this->m_connectedToNetwork = true;
|
|
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;
|
|
update.set_airport(airport.first);
|
|
|
|
if (0 != airport.second.inboundUpdates.size()) {
|
|
for (auto target = this->RadarTargetSelectFirst(); true == target.IsValid(); target = this->RadarTargetSelectNext(target)) {
|
|
auto it = std::find(airport.second.inboundUpdates.begin(), airport.second.inboundUpdates.end(), target.GetCallsign());
|
|
if (airport.second.inboundUpdates.end() != it) {
|
|
auto report = update.add_reports();
|
|
this->generateAircraftReportMessage(target, report);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* send the report and request the current sequence */
|
|
auto sequence = Backend::instance().update(update);
|
|
if (nullptr == sequence)
|
|
this->DisplayUserMessage(PLUGIN_NAME, "ERROR", "Unable to send a new aircraft report update", true, true, true, true, true);
|
|
else
|
|
this->updateSequence(sequence);
|
|
|
|
airport.second.inboundUpdates.clear();
|
|
}
|
|
|
|
this->m_updateQueueLock.unlock();
|
|
}
|
|
|
|
void PlugIn::OnAirportRunwayActivityChanged() {
|
|
std::lock_guard guard(this->m_updateQueueLock);
|
|
|
|
/* cleanup the arrival airports */
|
|
for (auto& airport : this->m_updateQueue)
|
|
airport.second.arrivalRunways.clear();
|
|
|
|
EuroScopePlugIn::CSectorElement rwy;
|
|
for (rwy = this->SectorFileElementSelectFirst(EuroScopePlugIn::SECTOR_ELEMENT_RUNWAY); true == rwy.IsValid();
|
|
rwy = this->SectorFileElementSelectNext(rwy, EuroScopePlugIn::SECTOR_ELEMENT_RUNWAY)) {
|
|
/* remove leading and trailing whitespaces */
|
|
std::string airport(rwy.GetAirportName());
|
|
airport = std::regex_replace(airport, std::regex("^ +| +$|( ) +"), "$1");
|
|
|
|
if (true == rwy.IsElementActive(false, 0) || true == rwy.IsElementActive(false, 1)) {
|
|
auto airportData = this->m_updateQueue.find(airport);
|
|
if (this->m_updateQueue.end() != airportData) {
|
|
if (true == rwy.IsElementActive(false, 0))
|
|
airportData->second.arrivalRunways.push_back(rwy.GetRunwayName(0));
|
|
if (true == rwy.IsElementActive(false, 1))
|
|
airportData->second.arrivalRunways.push_back(rwy.GetRunwayName(1));
|
|
}
|
|
}
|
|
}
|
|
}
|