KPublicTransport

onboardstatusmanager.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "onboardstatusmanager_p.h"
7#include "logging.h"
8
9#include "backend/scriptedrestonboardbackend_p.h"
10
11#include <QFile>
12#include <QJsonArray>
13#include <QJsonDocument>
14#include <QJsonObject>
15#include <QMetaProperty>
16#include <QNetworkAccessManager>
17
18using namespace KPublicTransport;
19
20void initResources()
21{
22 Q_INIT_RESOURCE(data);
23}
24
25OnboardStatusManager::OnboardStatusManager(QObject *parent)
26 : QObject(parent)
27{
28 qCDebug(Log);
29 initResources();
30
31 m_positionUpdateTimer.setSingleShot(true);
32 m_positionUpdateTimer.setTimerType(Qt::VeryCoarseTimer);
33 connect(&m_positionUpdateTimer, &QTimer::timeout, this, &OnboardStatusManager::requestPosition);
34 m_journeyUpdateTimer.setSingleShot(true);
35 m_journeyUpdateTimer.setTimerType(Qt::VeryCoarseTimer);
36 connect(&m_journeyUpdateTimer, &QTimer::timeout, this, &OnboardStatusManager::requestJourney);
37 connect(&m_wifiMonitor, &WifiMonitor::statusChanged, this, &OnboardStatusManager::wifiChanged);
38 connect(&m_wifiMonitor, &WifiMonitor::wifiChanged, this, &OnboardStatusManager::wifiChanged);
39 wifiChanged();
40}
41
42OnboardStatusManager::~OnboardStatusManager() = default;
43
44OnboardStatusManager* OnboardStatusManager::instance()
45{
46 static OnboardStatusManager mgr;
47 return &mgr;
48}
49
50OnboardStatus::Status OnboardStatusManager::status() const
51{
52 return m_status;
53}
54
55void OnboardStatusManager::setStatus(OnboardStatus::Status status)
56{
57 if (m_status == status) {
58 return;
59 }
60
61 m_status = status;
62 if (m_status != OnboardStatus::Onboard) {
63 m_previousPos = {};
64 m_currentPos = {};
65 m_journey = {};
66 }
67
68 Q_EMIT statusChanged();
69}
70
71PositionData OnboardStatusManager::currentPosition() const
72{
73 return m_currentPos;
74}
75
76bool OnboardStatusManager::supportsPosition() const
77{
78 return m_backend && m_backend->supportsPosition();
79}
80
81Journey OnboardStatusManager::currentJourney() const
82{
83 return m_journey;
84}
85
86bool OnboardStatusManager::supportsJourney() const
87{
88 return m_backend && m_backend->supportsJourney();
89}
90
91void OnboardStatusManager::registerFrontend(const OnboardStatus *status)
92{
93 qCDebug(Log) << "registering onboard frontend";
94 connect(status, &OnboardStatus::updateIntervalChanged, this, &OnboardStatusManager::requestForceUpdate);
95 m_frontends.push_back(status);
96 requestForceUpdate();
97}
98
99void OnboardStatusManager::unregisterFrontend(const OnboardStatus *status)
100{
101 qCDebug(Log) << "unregistering onboard frontend";
102 disconnect(status, &OnboardStatus::updateIntervalChanged, this, &OnboardStatusManager::requestUpdate);
103 const auto it = std::find(m_frontends.begin(), m_frontends.end(), status);
104 if (it != m_frontends.end()) {
105 m_frontends.erase(it);
106 }
107 requestUpdate();
108}
109
110void OnboardStatusManager::requestPosition()
111{
112 if (m_backend && !m_pendingPositionUpdate) {
113 m_pendingPositionUpdate = true;
114 m_backend->requestPosition(nam());
115 }
116}
117
118void OnboardStatusManager::requestJourney()
119{
120 if (m_backend && !m_pendingJourneyUpdate) {
121 m_pendingJourneyUpdate = true;
122 m_backend->requestJourney(nam());
123 }
124}
125
126void OnboardStatusManager::wifiChanged()
127{
128 auto ssid = m_wifiMonitor.ssid();
129 auto status = m_wifiMonitor.status();
130
131 if (Q_UNLIKELY(qEnvironmentVariableIsSet("KPUBLICTRANSPORT_ONBOARD_FAKE_CONFIG"))) {
132 QFile f(qEnvironmentVariable("KPUBLICTRANSPORT_ONBOARD_FAKE_CONFIG"));
133 if (!f.open(QFile::ReadOnly)) {
134 qCWarning(Log) << f.errorString() << f.fileName();
135 }
136 const auto config = QJsonDocument::fromJson(f.readAll()).object();
137 ssid = config.value(QLatin1String("ssid")).toString();
138 status = static_cast<WifiMonitor::Status>(QMetaEnum::fromType<WifiMonitor::Status>().keysToValue(config.value(QLatin1String("wifiStatus")).toString().toUtf8().constData()));
139 }
140
141 qCDebug(Log) << ssid << status;
142 switch (status) {
143 case WifiMonitor::NotAvailable:
145 break;
146 case WifiMonitor::Available:
147 {
148 if (ssid.isEmpty()) {
150 break;
151 }
152 loadAccessPointData();
153 const auto it = std::lower_bound(m_accessPointData.begin(), m_accessPointData.end(), ssid);
154 if (it == m_accessPointData.end() || (*it).ssid != ssid) {
156 break;
157 }
158 loadBackend((*it).backendId);
159 if (m_backend) {
160 setStatus(OnboardStatus::Onboard);
161 } else {
163 }
164 requestForceUpdate();
165 break;
166 }
167 case WifiMonitor::NoPermission:
169 break;
170 case WifiMonitor::WifiNotEnabled:
172 break;
173 case WifiMonitor::LocationServiceNotEnabled:
175 break;
176 }
177}
178
179void OnboardStatusManager::loadAccessPointData()
180{
181 if (!m_accessPointData.empty()) {
182 return;
183 }
184
185 QFile f(QStringLiteral(":/org.kde.kpublictransport.onboard/accesspoints.json"));
186 if (!f.open(QFile::ReadOnly)) {
187 qCWarning(Log) << "Failed to load access point database:" << f.errorString() << f.fileName();
188 return;
189 }
190
192 const auto aps = QJsonDocument::fromJson(f.readAll(), &error).array();
193 if (error.error != QJsonParseError::NoError) {
194 qCWarning(Log) << "Failed to parse access point data:" << error.errorString();
195 return;
196 }
197
198 m_accessPointData.reserve(aps.size());
199 for (const auto &apVal : aps) {
200 const auto ap = apVal.toObject();
201 AccessPointInfo info;
202 info.ssid = ap.value(QLatin1String("ssid")).toString();
203 info.backendId = ap.value(QLatin1String("id")).toString();
204 m_accessPointData.push_back(std::move(info));
205 }
206
207 std::sort(m_accessPointData.begin(), m_accessPointData.end());
208}
209
210void OnboardStatusManager::loadBackend(const QString &id)
211{
212 const bool oldSupportsPosition = supportsPosition();
213 const bool oldSupportsJourney = supportsJourney();
214
215 m_backend = createBackend(id);
216 if (!m_backend) {
217 return;
218 }
219
220 connect(m_backend.get(), &AbstractOnboardBackend::positionReceived, this, &OnboardStatusManager::positionUpdated);
221 connect(m_backend.get(), &AbstractOnboardBackend::journeyReceived, this, &OnboardStatusManager::journeyUpdated);
222
223 if (oldSupportsPosition != supportsPosition()) {
224 Q_EMIT supportsPositionChanged();
225 }
226 if (oldSupportsJourney != supportsJourney()) {
227 Q_EMIT supportsJourneyChanged();
228 }
229}
230
231std::unique_ptr<AbstractOnboardBackend> OnboardStatusManager::createBackend(const QString& id)
232{
233 std::unique_ptr<AbstractOnboardBackend> backend;
234
235 QFile f(QLatin1String(":/org.kde.kpublictransport.onboard/") + id + QLatin1String(".json"));
236 if (!f.open(QFile::ReadOnly)) {
237 qCWarning(Log) << "Failed to open onboard API configuration:" << f.errorString() << f.fileName();
238 return backend;
239 }
240
241 const auto config = QJsonDocument::fromJson(f.readAll()).object();
242 const auto backendTypeName = config.value(QLatin1String("backend")).toString();
243 if (backendTypeName == QLatin1String("ScriptedRestOnboardBackend")) { // TODO use names from QMetaObject
244 backend.reset(new ScriptedRestOnboardBackend);
245 }
246
247 if (!backend) {
248 qCWarning(Log) << "Failed to create onboard API backend:" << backendTypeName;
249 return backend;
250 }
251
252 const auto mo = backend->metaObject();
253 const auto options = config.value(QLatin1String("options")).toObject();
254 for (auto it = options.begin(); it != options.end(); ++it) {
255 const auto idx = mo->indexOfProperty(it.key().toUtf8().constData());
256 if (idx < 0) {
257 qCWarning(Log) << "Unknown backend setting:" << it.key();
258 continue;
259 }
260 const auto mp = mo->property(idx);
261 mp.write(backend.get(), it.value().toVariant());
262 }
263
264 return backend;
265}
266
267constexpr inline double degToRad(double deg)
268{
269 return deg / 180.0 * M_PI;
270}
271
272constexpr inline double radToDeg(double rad)
273{
274 return rad / M_PI * 180.0;
275}
276
277void OnboardStatusManager::positionUpdated(const PositionData &pos)
278{
279 m_pendingPositionUpdate = false;
280 m_previousPos = m_currentPos;
281 m_currentPos = pos;
282 if (!m_currentPos.timestamp.isValid()) {
283 m_currentPos.timestamp = QDateTime::currentDateTime();
284 }
285
286 // compute heading based on previous position, if we actually moved sufficiently
287 if (std::isnan(m_currentPos.heading) &&
288 m_previousPos.hasCoordinate() &&
289 m_currentPos.hasCoordinate() &&
290 Location::distance(m_currentPos.latitude, m_currentPos.longitude, m_previousPos.latitude, m_previousPos.longitude) > 10.0)
291 {
292 const auto deltaLon = degToRad(m_currentPos.longitude) - degToRad(m_previousPos.longitude);
293 const auto y = std::cos(degToRad(m_currentPos.latitude)) * std::sin(deltaLon);
294 const auto x = std::cos(degToRad(m_previousPos.latitude)) * std::sin(degToRad(m_previousPos.latitude)) - std::sin(degToRad(m_previousPos.latitude)) * std::cos(degToRad(m_currentPos.latitude)) * std::cos(deltaLon);
295 m_currentPos.heading = std::fmod(radToDeg(std::atan2(y, x)) + 360.0, 360.0);
296 }
297
298 // compute speed based on previous position if necessary
299 if (std::isnan(m_currentPos.speed) && m_previousPos.hasCoordinate() && m_currentPos.hasCoordinate())
300 {
301 const auto dist = Location::distance(m_currentPos.latitude, m_currentPos.longitude, m_previousPos.latitude, m_previousPos.longitude);
302 const double timeDelta = m_previousPos.timestamp.secsTo(m_currentPos.timestamp);
303 if (timeDelta > 0) {
304 m_currentPos.speed = 3.6 * dist / timeDelta;
305 }
306 }
307
308 Q_EMIT positionChanged();
309 requestUpdate();
310}
311
312void OnboardStatusManager::journeyUpdated(const Journey &jny)
313{
314 m_pendingJourneyUpdate = false;
315 m_journey = jny;
316
317 // don't sanity-check in fake mode, that will likely use outdated data
318 if (Q_LIKELY(qEnvironmentVariableIsEmpty("KPUBLICTRANSPORT_ONBOARD_FAKE_CONFIG"))) {
319
320 // check if the journey is at least remotely plausible
321 // sometimes the onboard systems are stuck on a previous journey...
323 m_journey = {};
324 }
325 }
326
327 Q_EMIT journeyChanged();
328 requestUpdate();
329}
330
331QNetworkAccessManager* OnboardStatusManager::nam()
332{
333 if (!m_nam) {
334 m_nam = new QNetworkAccessManager(this);
335 m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
336 }
337 return m_nam;
338}
339
340void OnboardStatusManager::requestUpdate()
341{
342 scheduleUpdate(false);
343}
344
345void OnboardStatusManager::requestForceUpdate()
346{
347 scheduleUpdate(true);
348}
349
350void OnboardStatusManager::scheduleUpdate(bool force)
351{
352 if (!m_backend || m_frontends.empty()) {
353 m_positionUpdateTimer.stop();
354 m_journeyUpdateTimer.stop();
355 return;
356 }
357
358 if (!m_pendingPositionUpdate) {
359 int interval = std::numeric_limits<int>::max();
360 for (auto f : m_frontends) {
361 if (f->positionUpdateInterval() > 0) {
362 interval = std::min(interval, f->positionUpdateInterval());
363 }
364 }
365 if (m_positionUpdateTimer.isActive()) {
366 interval = std::min(m_positionUpdateTimer.remainingTime() / 1000, interval);
367 }
368 if (interval < std::numeric_limits<int>::max()) {
369 qCDebug(Log) << "next position update:" << interval << force;
370 m_positionUpdateTimer.start(std::chrono::seconds(force ? 0 : interval));
371 }
372 }
373
374 if (!m_pendingJourneyUpdate) {
375 int interval = std::numeric_limits<int>::max();
376 for (auto f : m_frontends) {
377 if (f->journeyUpdateInterval() > 0) {
378 interval = std::min(interval, f->journeyUpdateInterval());
379 }
380 }
381 if (m_journeyUpdateTimer.isActive()) {
382 interval = std::min(m_journeyUpdateTimer.remainingTime() / 1000, interval);
383 }
384 if (interval < std::numeric_limits<int>::max()) {
385 qCDebug(Log) << "next journey update:" << interval << force;
386 m_journeyUpdateTimer.start(std::chrono::seconds(force ? 0 : interval));
387 }
388 }
389}
390
391void OnboardStatusManager::requestPermissions()
392{
393 m_wifiMonitor.requestPermissions();
394}
A journey plan.
Definition journey.h:267
QDateTime expectedArrivalTime
Actual arrival time, if available.
Definition journey.h:289
static float distance(float lat1, float lon1, float lat2, float lon2)
Compute the distance between two geo coordinates, in meters.
Definition location.cpp:436
Access to public transport onboard API.
@ LocationServiceNotEnabled
Wi-Fi monitoring is not possible due to the location service being disabled (Android only).
@ MissingPermissions
Wi-Fi monitoring not functional due to missing application permissions.
@ NotConnected
Wi-Fi monitoring functional, but currently not connected to an onboard Wi-Fi.
@ NotAvailable
Wi-Fi monitoring is generally not available on this platform.
@ WifiNotEnabled
Wi-Fi monitoring is not possible due to Wi-Fi being disabled.
@ Onboard
currently connected to a known onboard Wi-Fi system.
Q_SCRIPTABLE CaptureState status()
char * toString(const EngineQuery &query)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
Query operations and data types for accessing realtime public transport information from online servi...
constexpr double radToDeg(double rad)
constexpr double degToRad(double deg)
QDateTime addSecs(qint64 s) const const
QDateTime currentDateTime()
QJsonArray array() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QJsonValue value(QLatin1StringView key) const const
QString toString() const const
VeryCoarseTimer
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:47:54 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.