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:287
QDateTime expectedArrivalTime
Actual arrival time, if available.
Definition journey.h:309
static double distance(double lat1, double lon1, double lat2, double lon2)
Compute the distance between two geo coordinates, in meters.
Definition location.cpp:458
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...
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
QMetaEnum fromType()
int keysToValue(const char *keys, bool *ok) const const
VeryCoarseTimer
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:50:52 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.