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 {}; }
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
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  CAtcStationList atcStations;
203  CSimulatedAircraftList aircraft;
204  QMap<CCallsign, CFlightPlanRemarks> flightPlanRemarksMap;
205  auto updateTimestampFromFile =
206  QDateTime::fromString(jsonDoc["general"]["update_timestamp"].toString(), Qt::ISODateWithMs);
207 
208  const bool alreadyRead = (updateTimestampFromFile == this->getUpdateTimestamp());
209  if (alreadyRead)
210  {
211  CLogMessage(this).info(u"VATSIM file has same timestamp, skipped");
212  return;
213  }
214 
215  for (QJsonValueRef pilot : jsonDoc["pilots"].toArray())
216  {
217  if (!this->doWorkCheck())
218  {
219  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
220  return;
221  }
222  aircraft.push_back(parsePilot(pilot.toObject(), illegalEquipmentCodes));
223  flightPlanRemarksMap.insert(aircraft.back().getCallsign(), parseFlightPlanRemarks(pilot.toObject()));
224  }
225  for (QJsonValueRef controller : jsonDoc["controllers"].toArray())
226  {
227  if (!this->doWorkCheck())
228  {
229  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
230  return;
231  }
232  atcStations.push_back(parseController(controller.toObject()));
233  }
234  for (QJsonValueRef atis : jsonDoc["atis"].toArray())
235  {
236  if (!this->doWorkCheck())
237  {
238  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
239  return;
240  }
241  atcStations.push_back(parseController(atis.toObject()));
242  }
243 
244  // this part needs to be synchronized
245  {
246  QWriteLocker wl(&m_lock);
247  this->setUpdateTimestamp(updateTimestampFromFile);
248  m_aircraft = aircraft;
249  m_atcStations = atcStations;
250  m_flightPlanRemarks = flightPlanRemarksMap;
251  }
252 
253  // warnings, if required
254  if (!illegalEquipmentCodes.isEmpty())
255  {
258  u"Illegal / ignored equipment code(s) in VATSIM data file: %1")
259  << illegalEquipmentCodes.join(", "));
260  }
261 
262  // data read finished
263  emit this->dataFileRead(dataFileData.size() / 1000);
264  emit this->dataRead(CEntityFlags::VatsimDataFile, CEntityFlags::ReadFinished, dataFileData.size() / 1000,
265  url);
266  }
267  else
268  {
269  // network error
270  CLogMessage(this).warning(u"Reading VATSIM data file failed '%1' '%2'")
271  << nwReply->errorString() << urlString;
272  nwReply->abort();
273  emit this->dataRead(CEntityFlags::VatsimDataFile, CEntityFlags::ReadFailed, 0, url);
274  }
275  }
276 
277  CSimulatedAircraft CVatsimDataFileReader::parsePilot(const QJsonObject &pilot,
278  QStringList &o_illegalEquipmentCodes) const
279  {
280  const CCallsign callsign(pilot["callsign"].toString());
281  const CUser user(pilot["cid"].toString(), pilot["name"].toString(), callsign);
282  const CCoordinateGeodetic position(pilot["latitude"].toDouble(), pilot["longitude"].toDouble(),
283  pilot["altitude"].toInt());
284  const CHeading heading(pilot["heading"].toInt(), CAngleUnit::deg());
285  const CSpeed groundspeed(pilot["groundspeed"].toInt(), CSpeedUnit::kts());
286  const CAircraftSituation situation(callsign, position, heading, {}, {}, groundspeed);
287  CSimulatedAircraft aircraft(callsign, user, situation);
288  const QString icaoAndEquipment(pilot["flight_plan"]["aircraft"].toString().trimmed()); // in ICAO format
289  CFlightPlanAircraftInfo info(icaoAndEquipment);
290  if (info.getAircraftIcao().hasValidDesignator()) { aircraft.setAircraftIcaoCode(info.getAircraftIcao()); }
291  else if (!icaoAndEquipment.isEmpty()) { o_illegalEquipmentCodes.push_back(icaoAndEquipment); }
292  aircraft.setTransponderCode(pilot["transponder"].toString().toInt());
293  return aircraft;
294  }
295 
296  CFlightPlanRemarks CVatsimDataFileReader::parseFlightPlanRemarks(const QJsonObject &pilot) const
297  {
298  return { pilot["flight_plan"]["remarks"].toString().trimmed() };
299  }
300 
301  CAtcStation CVatsimDataFileReader::parseController(const QJsonObject &controller) const
302  {
303  const CCallsign callsign(controller["callsign"].toString());
304  const CUser user(controller["cid"].toString(), controller["name"].toString(), callsign);
305  const CFrequency freq(controller["frequency"].toString().toDouble(), CFrequencyUnit::kHz());
306  const CLength range(controller["visual_range"].toInt(), CLengthUnit::NM());
307  const QJsonArray atisLines = controller["text_atis"].toArray();
308  const auto atisText = makeRange(atisLines).transform([](auto line) { return line.toString(); });
309  const CInformationMessage atis(CInformationMessage::ATIS, atisText.to<QStringList>().join('\n'));
310  return { callsign, user, freq, {}, range, true, {}, {}, atis };
311  }
312 
313  void CVatsimDataFileReader::reloadSettings()
314  {
315  CReaderSettings s = m_settings.get();
316  setInitialAndPeriodicTime(std::chrono::milliseconds(s.getInitialTime().toMs()),
317  std::chrono::milliseconds(s.getPeriodicTime().toMs()));
318  }
319 } // 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.
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 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 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
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isEmpty() const const
void push_back(QList< T >::parameter_type value)
QMap< Key, T >::iterator insert(QMap< Key, T >::const_iterator pos, const Key &key, const T &value)
T value(const Key &key, const T &defaultValue) const const
bool isEmpty() const const
qsizetype size() const const
QByteArray toUtf8() const const
QString join(QChar separator) const const
ISODateWithMs
char * toString(QSizePolicy sp)
QString toString(QUrl::FormattingOptions options) const const
QString url(QUrl::FormattingOptions options) const const