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) : CThreadedReader(owner, "CVatsimDataFileReader")
54  {
55  this->reloadSettings();
56  }
57 
59  {
60  QReadLocker rl(&m_lock);
61  return m_aircraft;
62  }
63 
65  {
66  QReadLocker rl(&m_lock);
67  return m_atcStations;
68  }
69 
71  {
72  const CCallsignSet cs({ callsign });
73  return this->getAtcStationsForCallsigns(cs);
74  }
75 
77  {
78  return this->getAtcStations().findByCallsigns(callsigns);
79  }
80 
82  {
83  return this->getAircraft().findByCallsigns(callsigns).transform(
85  }
86 
88  {
89  const CCallsignSet callsigns({ callsign });
90  return this->getPilotsForCallsigns(callsigns);
91  }
92 
94  {
95  const CSimulatedAircraft aircraft = this->getAircraft().findFirstByCallsign(callsign);
96  return aircraft.getAirlineIcaoCode();
97  }
98 
100  {
101  const CSimulatedAircraft aircraft = this->getAircraft().findFirstByCallsign(callsign);
102  return aircraft.getAircraftIcaoCode();
103  }
104 
106  {
107  if (callsign.isEmpty()) { return CVoiceCapabilities(); }
108  QReadLocker rl(&m_lock);
109  return m_flightPlanRemarks.value(callsign).getVoiceCapabilities();
110  }
111 
113  {
114  if (callsign.isEmpty()) { return QString(); }
115  QReadLocker rl(&m_lock);
116  return m_flightPlanRemarks.value(callsign);
117  }
118 
120  {
121  this->getAircraft().updateWithVatsimDataFileData(aircraftToBeUdpated);
122  }
123 
125  {
126  const CCallsignSet cs({ callsign });
127  return this->getControllersForCallsigns(cs);
128  }
129 
131  {
132  return this->getAtcStations().findByCallsigns(callsigns).transform(
133  predicates::MemberTransform(&CAtcStation::getController));
134  }
135 
137  {
138  const CCallsignSet callsigns({ callsign });
139  return this->getUsersForCallsigns(callsigns);
140  }
141 
143  {
144  CUserList users;
145  if (callsigns.isEmpty()) { return users; }
146  for (const CCallsign &callsign : callsigns)
147  {
148  users.push_back(this->getPilotsForCallsign(callsign));
149  users.push_back(this->getControllersForCallsign(callsign));
150  }
151  return users;
152  }
153 
155  {
156  QPointer<CVatsimDataFileReader> myself(this);
157  QTimer::singleShot(0, this, [=] {
158  if (!myself) { return; }
159  myself->read();
160  });
161  }
162 
163  void CVatsimDataFileReader::doWorkImpl() { this->read(); }
164 
165  void CVatsimDataFileReader::read()
166  {
167  this->threadAssertCheck();
168  if (!this->doWorkCheck()) { return; }
169 
170  Q_ASSERT_X(sApp, Q_FUNC_INFO, "Missing application");
171  const QUrl url(sApp->getVatsimDataFileUrl());
172  if (url.isEmpty()) { return; }
173  this->getFromNetworkAndLog(url, { this, &CVatsimDataFileReader::parseVatsimFile });
174  }
175 
176  void CVatsimDataFileReader::parseVatsimFile(QNetworkReply *nwReplyPtr)
177  {
178  // wrap pointer, make sure any exit cleans up reply
179  // required to use delete later as object is created in a different thread
180  QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> nwReply(nwReplyPtr);
181  this->threadAssertCheck();
182 
183  // Worker thread, make sure to write only synced here!
184  if (!this->doWorkCheck())
185  {
186  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
187  return; // stop, terminate straight away, ending thread
188  }
189 
190  this->logNetworkReplyReceived(nwReplyPtr);
191  QStringList illegalEquipmentCodes;
192  const QUrl url = nwReply->url();
193  const QString urlString = url.toString();
194 
195  if (nwReply->error() == QNetworkReply::NoError)
196  {
197  const QString dataFileData = nwReply->readAll();
198  nwReply->close(); // close asap
199 
200  if (dataFileData.isEmpty()) { return; }
201  if (!this->didContentChange(dataFileData)) // Quick check by hash
202  {
203  CLogMessage(this).info(u"VATSIM file '%1' has same content, skipped") << urlString;
204  return;
205  }
206  auto jsonDoc = QJsonDocument::fromJson(dataFileData.toUtf8());
207  if (jsonDoc.isEmpty()) { return; }
208 
209  // build on local vars for thread safety
210  CServerList fsdServers;
211  CAtcStationList atcStations;
212  CSimulatedAircraftList aircraft;
213  QMap<CCallsign, CFlightPlanRemarks> flightPlanRemarksMap;
214  auto updateTimestampFromFile =
215  QDateTime::fromString(jsonDoc["general"]["update_timestamp"].toString(), Qt::ISODateWithMs);
216 
217  const bool alreadyRead = (updateTimestampFromFile == this->getUpdateTimestamp());
218  if (alreadyRead)
219  {
220  CLogMessage(this).info(u"VATSIM file has same timestamp, skipped");
221  return;
222  }
223 
224  for (QJsonValueRef pilot : jsonDoc["pilots"].toArray())
225  {
226  if (!this->doWorkCheck())
227  {
228  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
229  return;
230  }
231  aircraft.push_back(parsePilot(pilot.toObject(), illegalEquipmentCodes));
232  flightPlanRemarksMap.insert(aircraft.back().getCallsign(), parseFlightPlanRemarks(pilot.toObject()));
233  }
234  for (QJsonValueRef controller : jsonDoc["controllers"].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(controller.toObject()));
242  }
243  for (QJsonValueRef atis : jsonDoc["atis"].toArray())
244  {
245  if (!this->doWorkCheck())
246  {
247  CLogMessage(this).info(u"Terminated VATSIM file parsing process");
248  return;
249  }
250  atcStations.push_back(parseController(atis.toObject()));
251  }
252 
253  // Setup for VATSIM servers and sorting for comparison
254  fsdServers.sortBy(&CServer::getName, &CServer::getDescription);
255 
256  // this part needs to be synchronized
257  {
258  QWriteLocker wl(&m_lock);
259  this->setUpdateTimestamp(updateTimestampFromFile);
260  m_aircraft = aircraft;
261  m_atcStations = atcStations;
262  m_flightPlanRemarks = flightPlanRemarksMap;
263  }
264 
265  // warnings, if required
266  if (!illegalEquipmentCodes.isEmpty())
267  {
270  u"Illegal / ignored equipment code(s) in VATSIM data file: %1")
271  << illegalEquipmentCodes.join(", "));
272  }
273 
274  // data read finished
275  emit this->dataFileRead(dataFileData.size() / 1000);
276  emit this->dataRead(CEntityFlags::VatsimDataFile, CEntityFlags::ReadFinished, dataFileData.size() / 1000,
277  url);
278  }
279  else
280  {
281  // network error
282  CLogMessage(this).warning(u"Reading VATSIM data file failed '%1' '%2'")
283  << nwReply->errorString() << urlString;
284  nwReply->abort();
285  emit this->dataRead(CEntityFlags::VatsimDataFile, CEntityFlags::ReadFailed, 0, url);
286  }
287  }
288 
289  CSimulatedAircraft CVatsimDataFileReader::parsePilot(const QJsonObject &pilot,
290  QStringList &o_illegalEquipmentCodes) const
291  {
292  const CCallsign callsign(pilot["callsign"].toString());
293  const CUser user(pilot["cid"].toString(), pilot["name"].toString(), callsign);
294  const CCoordinateGeodetic position(pilot["latitude"].toDouble(), pilot["longitude"].toDouble(),
295  pilot["altitude"].toInt());
296  const CHeading heading(pilot["heading"].toInt(), CAngleUnit::deg());
297  const CSpeed groundspeed(pilot["groundspeed"].toInt(), CSpeedUnit::kts());
298  const CAircraftSituation situation(callsign, position, heading, {}, {}, groundspeed);
299  CSimulatedAircraft aircraft(callsign, user, situation);
300  const QString icaoAndEquipment(pilot["flight_plan"]["aircraft"].toString().trimmed()); // in ICAO format
301  CFlightPlanAircraftInfo info(icaoAndEquipment);
302  if (info.getAircraftIcao().hasValidDesignator()) { aircraft.setAircraftIcaoCode(info.getAircraftIcao()); }
303  else if (!icaoAndEquipment.isEmpty()) { o_illegalEquipmentCodes.push_back(icaoAndEquipment); }
304  aircraft.setTransponderCode(pilot["transponder"].toString().toInt());
305  return aircraft;
306  }
307 
308  CFlightPlanRemarks CVatsimDataFileReader::parseFlightPlanRemarks(const QJsonObject &pilot) const
309  {
310  return CFlightPlanRemarks(pilot["flight_plan"]["remarks"].toString().trimmed());
311  }
312 
313  CAtcStation CVatsimDataFileReader::parseController(const QJsonObject &controller) const
314  {
315  const CCallsign callsign(controller["callsign"].toString());
316  const CUser user(controller["cid"].toString(), controller["name"].toString(), callsign);
317  const CFrequency freq(controller["frequency"].toString().toDouble(), CFrequencyUnit::kHz());
318  const CLength range(controller["visual_range"].toInt(), CLengthUnit::NM());
319  const QJsonArray atisLines = controller["text_atis"].toArray();
320  const auto atisText = makeRange(atisLines).transform([](auto line) { return line.toString(); });
321  const CInformationMessage atis(CInformationMessage::ATIS, atisText.to<QStringList>().join('\n'));
322  return CAtcStation(callsign, user, freq, {}, range, true, {}, {}, atis);
323  }
324 
325  void CVatsimDataFileReader::reloadSettings()
326  {
327  CReaderSettings s = m_settings.get();
328  setInitialAndPeriodicTime(s.getInitialTime().toMs(), s.getPeriodicTime().toMs());
329  }
330 } // 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.
Support for threaded based reading and parsing tasks such as data files via http, or file system and ...
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 everthing 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.
void setInitialAndPeriodicTime(int initialTime, int periodicTime)
Set initial and periodic times.
bool doWorkCheck() const
Still enabled etc.?
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 readInBackgroundThread()
Start reading in own thread.
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
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30