swift
vatsimdatafilereader.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2013 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QByteArray>
7 #include <QDateTime>
8 #include <QMetaObject>
9 #include <QNetworkReply>
10 #include <QPointer>
11 #include <QReadLocker>
12 #include <QRegularExpression>
13 #include <QScopedPointer>
14 #include <QScopedPointerDeleteLater>
15 #include <QStringBuilder>
16 #include <QTimer>
17 #include <QUrl>
18 #include <QWriteLocker>
19 #include <Qt>
20 #include <QtGlobal>
21 
22 #include "core/application.h"
24 #include "misc/aviation/altitude.h"
27 #include "misc/logmessage.h"
30 #include "misc/network/server.h"
31 #include "misc/network/url.h"
32 #include "misc/network/user.h"
33 #include "misc/pq/frequency.h"
34 #include "misc/pq/length.h"
35 #include "misc/pq/speed.h"
36 #include "misc/pq/units.h"
37 #include "misc/predicates.h"
38 #include "misc/range.h"
40 #include "misc/statusmessage.h"
41 #include "misc/verify.h"
42 
43 using namespace swift::misc;
44 using namespace swift::misc::aviation;
45 using namespace swift::misc::network;
46 using namespace swift::misc::geo;
47 using namespace swift::misc::simulation;
48 using namespace swift::misc::physical_quantities;
49 using namespace swift::core::data;
50 
51 namespace swift::core::vatsim
52 {
53  CVatsimDataFileReader::CVatsimDataFileReader(QObject *owner)
54  : CThreadedReaderPeriodic(owner, "CVatsimDataFileReader")
55  {
56  this->reloadSettings();
57  }
58 
60  {
61  QReadLocker rl(&m_lock);
62  return m_aircraft;
63  }
64 
66  {
67  QReadLocker rl(&m_lock);
68  return m_atcStations;
69  }
70 
72  {
73  const CCallsignSet cs({ callsign });
74  return this->getAtcStationsForCallsigns(cs);
75  }
76 
78  {
79  return this->getAtcStations().findByCallsigns(callsigns);
80  }
81 
83  {
84  return this->getAircraft().findByCallsigns(callsigns).transform(
86  }
87 
89  {
90  const CCallsignSet callsigns({ callsign });
91  return this->getPilotsForCallsigns(callsigns);
92  }
93 
95  {
96  const CSimulatedAircraft aircraft = this->getAircraft().findFirstByCallsign(callsign);
97  return aircraft.getAirlineIcaoCode();
98  }
99 
101  {
102  const CSimulatedAircraft aircraft = this->getAircraft().findFirstByCallsign(callsign);
103  return aircraft.getAircraftIcaoCode();
104  }
105 
107  {
108  if (callsign.isEmpty()) { return CVoiceCapabilities(); }
109  QReadLocker rl(&m_lock);
110  return m_flightPlanRemarks.value(callsign).getVoiceCapabilities();
111  }
112 
114  {
115  if (callsign.isEmpty()) { return QString(); }
116  QReadLocker rl(&m_lock);
117  return m_flightPlanRemarks.value(callsign);
118  }
119 
121  {
122  this->getAircraft().updateWithVatsimDataFileData(aircraftToBeUdpated);
123  }
124 
126  {
127  const CCallsignSet cs({ callsign });
128  return this->getControllersForCallsigns(cs);
129  }
130 
132  {
133  return this->getAtcStations().findByCallsigns(callsigns).transform(
134  predicates::MemberTransform(&CAtcStation::getController));
135  }
136 
138  {
139  const CCallsignSet callsigns({ callsign });
140  return this->getUsersForCallsigns(callsigns);
141  }
142 
144  {
145  CUserList users;
146  if (callsigns.isEmpty()) { return users; }
147  for (const CCallsign &callsign : callsigns)
148  {
149  users.push_back(this->getPilotsForCallsign(callsign));
150  users.push_back(this->getControllersForCallsign(callsign));
151  }
152  return users;
153  }
154 
155  void CVatsimDataFileReader::doWorkImpl() { this->read(); }
156 
157  void CVatsimDataFileReader::read()
158  {
159  this->threadAssertCheck();
160  if (!this->doWorkCheck()) { return; }
161 
162  Q_ASSERT_X(sApp, Q_FUNC_INFO, "Missing application");
163  const QUrl url(sApp->getVatsimDataFileUrl());
164  if (url.isEmpty()) { return; }
165  this->getFromNetworkAndLog(url, { this, &CVatsimDataFileReader::parseVatsimFile });
166  }
167 
168  void CVatsimDataFileReader::parseVatsimFile(QNetworkReply *nwReplyPtr)
169  {
170  // wrap pointer, make sure any exit cleans up reply
171  // required to use delete later as object is created in a different thread
172  QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> nwReply(nwReplyPtr);
173  this->threadAssertCheck();
174 
175  // Worker thread, make sure to write only synced here!
176  if (!this->doWorkCheck())
177  {
178  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
179  return; // stop, terminate straight away, ending thread
180  }
181 
182  this->logNetworkReplyReceived(nwReplyPtr);
183  QStringList illegalEquipmentCodes;
184  const QUrl url = nwReply->url();
185  const QString urlString = url.toString();
186 
187  if (nwReply->error() == QNetworkReply::NoError)
188  {
189  const QString dataFileData = nwReply->readAll();
190  nwReply->close(); // close asap
191 
192  if (dataFileData.isEmpty()) { return; }
193  if (!this->didContentChange(dataFileData)) // Quick check by hash
194  {
195  CLogMessage(this).info(u"VATSIM file '%1' has same content, skipped") << urlString;
196  return;
197  }
198  auto jsonDoc = QJsonDocument::fromJson(dataFileData.toUtf8());
199  if (jsonDoc.isEmpty()) { return; }
200 
201  // build on local vars for thread safety
202  CServerList fsdServers;
203  CAtcStationList atcStations;
204  CSimulatedAircraftList aircraft;
205  QMap<CCallsign, CFlightPlanRemarks> flightPlanRemarksMap;
206  auto updateTimestampFromFile =
207  QDateTime::fromString(jsonDoc["general"]["update_timestamp"].toString(), Qt::ISODateWithMs);
208 
209  const bool alreadyRead = (updateTimestampFromFile == this->getUpdateTimestamp());
210  if (alreadyRead)
211  {
212  CLogMessage(this).info(u"VATSIM file has same timestamp, skipped");
213  return;
214  }
215 
216  for (QJsonValueRef pilot : jsonDoc["pilots"].toArray())
217  {
218  if (!this->doWorkCheck())
219  {
220  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
221  return;
222  }
223  aircraft.push_back(parsePilot(pilot.toObject(), illegalEquipmentCodes));
224  flightPlanRemarksMap.insert(aircraft.back().getCallsign(), parseFlightPlanRemarks(pilot.toObject()));
225  }
226  for (QJsonValueRef controller : jsonDoc["controllers"].toArray())
227  {
228  if (!this->doWorkCheck())
229  {
230  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
231  return;
232  }
233  atcStations.push_back(parseController(controller.toObject()));
234  }
235  for (QJsonValueRef atis : jsonDoc["atis"].toArray())
236  {
237  if (!this->doWorkCheck())
238  {
239  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
240  return;
241  }
242  atcStations.push_back(parseController(atis.toObject()));
243  }
244 
245  // Setup for VATSIM servers and sorting for comparison
246  fsdServers.sortBy(&CServer::getName, &CServer::getDescription);
247 
248  // this part needs to be synchronized
249  {
250  QWriteLocker wl(&m_lock);
251  this->setUpdateTimestamp(updateTimestampFromFile);
252  m_aircraft = aircraft;
253  m_atcStations = atcStations;
254  m_flightPlanRemarks = flightPlanRemarksMap;
255  }
256 
257  // warnings, if required
258  if (!illegalEquipmentCodes.isEmpty())
259  {
262  u"Illegal / ignored equipment code(s) in VATSIM data file: %1")
263  << illegalEquipmentCodes.join(", "));
264  }
265 
266  // data read finished
267  emit this->dataFileRead(dataFileData.size() / 1000);
268  emit this->dataRead(CEntityFlags::VatsimDataFile, CEntityFlags::ReadFinished, dataFileData.size() / 1000,
269  url);
270  }
271  else
272  {
273  // network error
274  CLogMessage(this).warning(u"Reading VATSIM data file failed '%1' '%2'")
275  << nwReply->errorString() << urlString;
276  nwReply->abort();
277  emit this->dataRead(CEntityFlags::VatsimDataFile, CEntityFlags::ReadFailed, 0, url);
278  }
279  }
280 
281  CSimulatedAircraft CVatsimDataFileReader::parsePilot(const QJsonObject &pilot,
282  QStringList &o_illegalEquipmentCodes) const
283  {
284  const CCallsign callsign(pilot["callsign"].toString());
285  const CUser user(pilot["cid"].toString(), pilot["name"].toString(), callsign);
286  const CCoordinateGeodetic position(pilot["latitude"].toDouble(), pilot["longitude"].toDouble(),
287  pilot["altitude"].toInt());
288  const CHeading heading(pilot["heading"].toInt(), CAngleUnit::deg());
289  const CSpeed groundspeed(pilot["groundspeed"].toInt(), CSpeedUnit::kts());
290  const CAircraftSituation situation(callsign, position, heading, {}, {}, groundspeed);
291  CSimulatedAircraft aircraft(callsign, user, situation);
292  const QString icaoAndEquipment(pilot["flight_plan"]["aircraft"].toString().trimmed()); // in ICAO format
293  CFlightPlanAircraftInfo info(icaoAndEquipment);
294  if (info.getAircraftIcao().hasValidDesignator()) { aircraft.setAircraftIcaoCode(info.getAircraftIcao()); }
295  else if (!icaoAndEquipment.isEmpty()) { o_illegalEquipmentCodes.push_back(icaoAndEquipment); }
296  aircraft.setTransponderCode(pilot["transponder"].toString().toInt());
297  return aircraft;
298  }
299 
300  CFlightPlanRemarks CVatsimDataFileReader::parseFlightPlanRemarks(const QJsonObject &pilot) const
301  {
302  return CFlightPlanRemarks(pilot["flight_plan"]["remarks"].toString().trimmed());
303  }
304 
305  CAtcStation CVatsimDataFileReader::parseController(const QJsonObject &controller) const
306  {
307  const CCallsign callsign(controller["callsign"].toString());
308  const CUser user(controller["cid"].toString(), controller["name"].toString(), callsign);
309  const CFrequency freq(controller["frequency"].toString().toDouble(), CFrequencyUnit::kHz());
310  const CLength range(controller["visual_range"].toInt(), CLengthUnit::NM());
311  const QJsonArray atisLines = controller["text_atis"].toArray();
312  const auto atisText = makeRange(atisLines).transform([](auto line) { return line.toString(); });
313  const CInformationMessage atis(CInformationMessage::ATIS, atisText.to<QStringList>().join('\n'));
314  return CAtcStation(callsign, user, freq, {}, range, true, {}, {}, atis);
315  }
316 
317  void CVatsimDataFileReader::reloadSettings()
318  {
319  CReaderSettings s = m_settings.get();
320  setInitialAndPeriodicTime(std::chrono::milliseconds(s.getInitialTime().toMs()),
321  std::chrono::milliseconds(s.getPeriodicTime().toMs()));
322  }
323 } // namespace swift::core::vatsim
SWIFT_CORE_EXPORT swift::core::CApplication * sApp
Single instance of application object.
Definition: application.cpp:71
swift::misc::network::CUrl getVatsimDataFileUrl() const
Consolidated version of data file URL, either from CGlobalSetup or CVatsimSetup.
QDateTime getUpdateTimestamp() const
Thread safe, get update timestamp.
void setUpdateTimestamp(const QDateTime &updateTimestamp=QDateTime::currentDateTimeUtc())
Thread safe, set update timestamp.
static void logInconsistentData(const swift::misc::CStatusMessage &msg, const char *funcInfo=nullptr)
Use this to log inconsistent data.
void threadAssertCheck() const
Make sure everything runs correctly in own thread.
void logNetworkReplyReceived(QNetworkReply *reply)
Network reply received, mark in m_urlReadLog.
QReadWriteLock m_lock
lock which can be used from the derived classes
bool didContentChange(const QString &content, int startPosition=-1)
Stores new content hash and returns if content changed (based on hash value.
QNetworkReply * getFromNetworkAndLog(const swift::misc::network::CUrl &url, const swift::misc::CSlot< void(QNetworkReply *)> &callback)
Get request from network, and log with m_urlReadLog.
bool doWorkCheck() const
Still enabled etc.?
Periodically executes doWorkImpl() in a separate thread.
void setInitialAndPeriodicTime(std::chrono::milliseconds initialTime, std::chrono::milliseconds periodicTime)
Set initial and periodic times Changes only apply after the next time the timer restarts.
swift::misc::network::CUserList getControllersForCallsigns(const swift::misc::aviation::CCallsignSet &callsigns) const
Controllers for callsigns.
swift::misc::aviation::CFlightPlanRemarks getFlightPlanRemarksForCallsign(const swift::misc::aviation::CCallsign &callsign) const
Flight plan remarks for callsign.
void dataRead(swift::misc::network::CEntityFlags::Entity entity, swift::misc::network::CEntityFlags::ReadState state, int number, const QUrl &url)
Data have been read.
swift::misc::network::CUserList getPilotsForCallsigns(const swift::misc::aviation::CCallsignSet &callsigns) const
Users for callsigns.
swift::misc::aviation::CAirlineIcaoCode getAirlineIcaoCode(const swift::misc::aviation::CCallsign &callsign) const
Airline ICAO info for callsign.
void dataFileRead(int kB)
Data have been read.
virtual void doWorkImpl()
This method does the actual work in the derived class.
swift::misc::aviation::CAtcStationList getAtcStationsForCallsigns(const swift::misc::aviation::CCallsignSet &callsigns) const
Get ATC stations for callsigns.
swift::misc::aviation::CAtcStationList getAtcStationsForCallsign(const swift::misc::aviation::CCallsign &callsign) const
Get ATC stations for callsign.
swift::misc::network::CVoiceCapabilities getVoiceCapabilityForCallsign(const swift::misc::aviation::CCallsign &callsign) const
Voice capability for callsign.
void updateWithVatsimDataFileData(swift::misc::simulation::CSimulatedAircraft &aircraftToBeUdpated) const
Update aircraft with VATSIM aircraft data from data file.
swift::misc::network::CUserList getUsersForCallsigns(const swift::misc::aviation::CCallsignSet &callsigns) const
Users for callsign(s)
swift::misc::network::CUserList getPilotsForCallsign(const swift::misc::aviation::CCallsign &callsign) const
Users for callsign.
swift::misc::network::CUserList getControllersForCallsign(const swift::misc::aviation::CCallsign &callsign) const
Controllers for callsign.
swift::misc::aviation::CAtcStationList getAtcStations() const
Get ATC station.
swift::misc::network::CUserList getUsersForCallsign(const swift::misc::aviation::CCallsign &callsign) const
User for callsign.
swift::misc::aviation::CAircraftIcaoCode getAircraftIcaoCode(const swift::misc::aviation::CCallsign &callsign) const
Aircraft ICAO info for callsign.
swift::misc::simulation::CSimulatedAircraftList getAircraft() const
Get aircraft.
T get() const
Get a copy of the current value.
Definition: valuecache.h:408
bool isEmpty() const
Synonym for empty.
Definition: collection.h:191
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
auto transform(F function) const
Return a new container generated by applying some transformation function to all elements of this one...
Definition: range.h:403
void sortBy(K1 key1, Keys... keys)
In-place sort by some particular key(s).
Definition: sequence.h:576
void push_back(const T &value)
Appends an element at the end of the sequence.
Definition: sequence.h:305
reference back()
Access the last element.
Definition: sequence.h:249
Streamable status message, e.g.
constexpr static auto SeverityInfo
Status severities.
Value object for ICAO classification.
Value object encapsulating information of an aircraft's situation.
Value object for ICAO classification.
Value object encapsulating information about an ATC station.
Definition: atcstation.h:38
Value object for a list of ATC stations.
Value object encapsulating information of a callsign.
Definition: callsign.h:30
bool isEmpty() const
Is empty?
Definition: callsign.h:63
Value object for a set of callsigns.
Definition: callsignset.h:26
Flightplan-related information about an aircraft (aircraft ICAO, equipment and WTC)
Flight plan remarks, parsed values.
Definition: flightplan.h:46
Heading as used in aviation, can be true or magnetic heading.
Definition: heading.h:41
Value object encapsulating information message (ATIS, METAR, TAF)
OBJ findFirstByCallsign(const CCallsign &callsign, const OBJ &ifNotFound={}) const
Find the first aircraft by callsign, if none return given one.
CONTAINER findByCallsigns(const CCallsignSet &callsigns) const
Find 0..n aircraft matching any of a set of callsigns.
Value object encapsulating a list of servers.
Definition: serverlist.h:23
Value object encapsulating information of a user.
Definition: user.h:28
Value object encapsulating a list of voice rooms.
Definition: userlist.h:26
Value object encapsulating information for voice capabilities.
Physical unit length (length)
Definition: length.h:18
Comprehensive information of an aircraft.
const network::CUser & getPilot() const
Get user.
const aviation::CCallsign & getCallsign() const
Get callsign.
const aviation::CAircraftIcaoCode & getAircraftIcaoCode() const
Get aircraft ICAO info.
const aviation::CAirlineIcaoCode & getAirlineIcaoCode() const
Airline ICAO code if any.
Value object encapsulating a list of aircraft.
bool updateWithVatsimDataFileData(CSimulatedAircraft &aircraftToBeUpdated) const
Update aircraft with data from VATSIM data file.
Core data traits (aka cached values) and classes.
auto MemberTransform(T memberFunc)
Returns a function object that returns the value returned by one of it's argument member functions.
Definition: predicates.h:54
Free functions in swift::misc.
auto makeRange(I begin, I2 end) -> CRange< I >
Returns a CRange constructed from begin and end iterators of deduced types.
Definition: range.h:316