File indexing completed on 2024-12-01 07:25:49

0001 /*
0002     SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 #include "onboardstatusmanager_p.h"
0007 #include "logging.h"
0008 
0009 #include "backend/scriptedrestonboardbackend_p.h"
0010 
0011 #include <QFile>
0012 #include <QJsonArray>
0013 #include <QJsonDocument>
0014 #include <QJsonObject>
0015 #include <QMetaProperty>
0016 #include <QNetworkAccessManager>
0017 
0018 using namespace KPublicTransport;
0019 
0020 void initResources()
0021 {
0022     Q_INIT_RESOURCE(data);
0023 }
0024 
0025 OnboardStatusManager::OnboardStatusManager(QObject *parent)
0026     : QObject(parent)
0027 {
0028     qCDebug(Log);
0029     initResources();
0030 
0031     m_positionUpdateTimer.setSingleShot(true);
0032     m_positionUpdateTimer.setTimerType(Qt::VeryCoarseTimer);
0033     connect(&m_positionUpdateTimer, &QTimer::timeout, this, &OnboardStatusManager::requestPosition);
0034     m_journeyUpdateTimer.setSingleShot(true);
0035     m_journeyUpdateTimer.setTimerType(Qt::VeryCoarseTimer);
0036     connect(&m_journeyUpdateTimer, &QTimer::timeout, this, &OnboardStatusManager::requestJourney);
0037     connect(&m_wifiMonitor, &WifiMonitor::statusChanged, this, &OnboardStatusManager::wifiChanged);
0038     connect(&m_wifiMonitor, &WifiMonitor::wifiChanged, this, &OnboardStatusManager::wifiChanged);
0039     wifiChanged();
0040 }
0041 
0042 OnboardStatusManager::~OnboardStatusManager() = default;
0043 
0044 OnboardStatusManager* OnboardStatusManager::instance()
0045 {
0046     static OnboardStatusManager mgr;
0047     return &mgr;
0048 }
0049 
0050 OnboardStatus::Status OnboardStatusManager::status() const
0051 {
0052     return m_status;
0053 }
0054 
0055 void OnboardStatusManager::setStatus(OnboardStatus::Status status)
0056 {
0057     if (m_status == status) {
0058         return;
0059     }
0060 
0061     m_status = status;
0062     if (m_status != OnboardStatus::Onboard) {
0063         m_previousPos = {};
0064         m_currentPos = {};
0065         m_journey = {};
0066     }
0067 
0068     Q_EMIT statusChanged();
0069 }
0070 
0071 PositionData OnboardStatusManager::currentPosition() const
0072 {
0073     return m_currentPos;
0074 }
0075 
0076 bool OnboardStatusManager::supportsPosition() const
0077 {
0078     return m_backend && m_backend->supportsPosition();
0079 }
0080 
0081 Journey OnboardStatusManager::currentJourney() const
0082 {
0083     return m_journey;
0084 }
0085 
0086 bool OnboardStatusManager::supportsJourney() const
0087 {
0088     return m_backend && m_backend->supportsJourney();
0089 }
0090 
0091 void OnboardStatusManager::registerFrontend(const OnboardStatus *status)
0092 {
0093     qCDebug(Log) << "registering onboard frontend";
0094     connect(status, &OnboardStatus::updateIntervalChanged, this, &OnboardStatusManager::requestForceUpdate);
0095     m_frontends.push_back(status);
0096     requestForceUpdate();
0097 }
0098 
0099 void OnboardStatusManager::unregisterFrontend(const OnboardStatus *status)
0100 {
0101     qCDebug(Log) << "unregistering onboard frontend";
0102     disconnect(status, &OnboardStatus::updateIntervalChanged, this, &OnboardStatusManager::requestUpdate);
0103     const auto it = std::find(m_frontends.begin(), m_frontends.end(), status);
0104     if (it != m_frontends.end()) {
0105         m_frontends.erase(it);
0106     }
0107     requestUpdate();
0108 }
0109 
0110 void OnboardStatusManager::requestPosition()
0111 {
0112     if (m_backend && !m_pendingPositionUpdate) {
0113         m_pendingPositionUpdate = true;
0114         m_backend->requestPosition(nam());
0115     }
0116 }
0117 
0118 void OnboardStatusManager::requestJourney()
0119 {
0120     if (m_backend && !m_pendingJourneyUpdate) {
0121         m_pendingJourneyUpdate = true;
0122         m_backend->requestJourney(nam());
0123     }
0124 }
0125 
0126 void OnboardStatusManager::wifiChanged()
0127 {
0128     auto ssid = m_wifiMonitor.ssid();
0129     auto status = m_wifiMonitor.status();
0130 
0131     if (Q_UNLIKELY(qEnvironmentVariableIsSet("KPUBLICTRANSPORT_ONBOARD_FAKE_CONFIG"))) {
0132         QFile f(qEnvironmentVariable("KPUBLICTRANSPORT_ONBOARD_FAKE_CONFIG"));
0133         if (!f.open(QFile::ReadOnly)) {
0134             qCWarning(Log) << f.errorString() << f.fileName();
0135         }
0136         const auto config = QJsonDocument::fromJson(f.readAll()).object();
0137         ssid = config.value(QLatin1String("ssid")).toString();
0138         status = static_cast<WifiMonitor::Status>(QMetaEnum::fromType<WifiMonitor::Status>().keysToValue(config.value(QLatin1String("wifiStatus")).toString().toUtf8().constData()));
0139     }
0140 
0141     qCDebug(Log) << ssid << status;
0142     switch (status) {
0143         case WifiMonitor::NotAvailable:
0144             setStatus(OnboardStatus::NotAvailable);
0145             break;
0146         case WifiMonitor::Available:
0147         {
0148             if (ssid.isEmpty()) {
0149                 setStatus(OnboardStatus::NotConnected);
0150                 break;
0151             }
0152             loadAccessPointData();
0153             const auto it = std::lower_bound(m_accessPointData.begin(), m_accessPointData.end(), ssid);
0154             if (it == m_accessPointData.end() || (*it).ssid != ssid) {
0155                 setStatus(OnboardStatus::NotConnected);
0156                 break;
0157             }
0158             loadBackend((*it).backendId);
0159             if (m_backend) {
0160                 setStatus(OnboardStatus::Onboard);
0161             } else {
0162                 setStatus(OnboardStatus::NotConnected);
0163             }
0164             requestForceUpdate();
0165             break;
0166         }
0167         case WifiMonitor::NoPermission:
0168             setStatus(OnboardStatus::MissingPermissions);
0169             break;
0170         case WifiMonitor::WifiNotEnabled:
0171             setStatus(OnboardStatus::WifiNotEnabled);
0172             break;
0173         case WifiMonitor::LocationServiceNotEnabled:
0174             setStatus(OnboardStatus::LocationServiceNotEnabled);
0175             break;
0176     }
0177 }
0178 
0179 void OnboardStatusManager::loadAccessPointData()
0180 {
0181     if (!m_accessPointData.empty()) {
0182         return;
0183     }
0184 
0185     QFile f(QStringLiteral(":/org.kde.kpublictransport.onboard/accesspoints.json"));
0186     if (!f.open(QFile::ReadOnly)) {
0187         qCWarning(Log) << "Failed to load access point database:" << f.errorString() << f.fileName();
0188         return;
0189     }
0190 
0191     QJsonParseError error;
0192     const auto aps = QJsonDocument::fromJson(f.readAll(), &error).array();
0193     if (error.error != QJsonParseError::NoError) {
0194         qCWarning(Log) << "Failed to parse access point data:" << error.errorString();
0195         return;
0196     }
0197 
0198     m_accessPointData.reserve(aps.size());
0199     for (const auto &apVal : aps) {
0200         const auto ap = apVal.toObject();
0201         AccessPointInfo info;
0202         info.ssid = ap.value(QLatin1String("ssid")).toString();
0203         info.backendId = ap.value(QLatin1String("id")).toString();
0204         m_accessPointData.push_back(std::move(info));
0205     }
0206 
0207     std::sort(m_accessPointData.begin(), m_accessPointData.end());
0208 }
0209 
0210 void OnboardStatusManager::loadBackend(const QString &id)
0211 {
0212     const bool oldSupportsPosition = supportsPosition();
0213     const bool oldSupportsJourney = supportsJourney();
0214 
0215     m_backend = createBackend(id);
0216     if (!m_backend) {
0217         return;
0218     }
0219 
0220     connect(m_backend.get(), &AbstractOnboardBackend::positionReceived, this, &OnboardStatusManager::positionUpdated);
0221     connect(m_backend.get(), &AbstractOnboardBackend::journeyReceived, this, &OnboardStatusManager::journeyUpdated);
0222 
0223     if (oldSupportsPosition != supportsPosition()) {
0224         Q_EMIT supportsPositionChanged();
0225     }
0226     if (oldSupportsJourney != supportsJourney()) {
0227         Q_EMIT supportsJourneyChanged();
0228     }
0229 }
0230 
0231 std::unique_ptr<AbstractOnboardBackend> OnboardStatusManager::createBackend(const QString& id)
0232 {
0233     std::unique_ptr<AbstractOnboardBackend> backend;
0234 
0235     QFile f(QLatin1String(":/org.kde.kpublictransport.onboard/") + id + QLatin1String(".json"));
0236     if (!f.open(QFile::ReadOnly)) {
0237         qCWarning(Log) << "Failed to open onboard API configuration:" << f.errorString() << f.fileName();
0238         return backend;
0239     }
0240 
0241     const auto config = QJsonDocument::fromJson(f.readAll()).object();
0242     const auto backendTypeName = config.value(QLatin1String("backend")).toString();
0243     if (backendTypeName == QLatin1String("ScriptedRestOnboardBackend")) { // TODO use names from QMetaObject
0244         backend.reset(new ScriptedRestOnboardBackend);
0245     }
0246 
0247     if (!backend) {
0248         qCWarning(Log) << "Failed to create onboard API backend:" << backendTypeName;
0249         return backend;
0250     }
0251 
0252     const auto mo = backend->metaObject();
0253     const auto options = config.value(QLatin1String("options")).toObject();
0254     for (auto it = options.begin(); it != options.end(); ++it) {
0255         const auto idx = mo->indexOfProperty(it.key().toUtf8().constData());
0256         if (idx < 0) {
0257             qCWarning(Log) << "Unknown backend setting:" << it.key();
0258             continue;
0259         }
0260         const auto mp = mo->property(idx);
0261         mp.write(backend.get(), it.value().toVariant());
0262     }
0263 
0264     return backend;
0265 }
0266 
0267 constexpr inline double degToRad(double deg)
0268 {
0269     return deg / 180.0 * M_PI;
0270 }
0271 
0272 constexpr inline double radToDeg(double rad)
0273 {
0274     return rad / M_PI * 180.0;
0275 }
0276 
0277 void OnboardStatusManager::positionUpdated(const PositionData &pos)
0278 {
0279     m_pendingPositionUpdate = false;
0280     m_previousPos = m_currentPos;
0281     m_currentPos = pos;
0282     if (!m_currentPos.timestamp.isValid()) {
0283         m_currentPos.timestamp = QDateTime::currentDateTime();
0284     }
0285 
0286     // compute heading based on previous position, if we actually moved sufficiently
0287     if (std::isnan(m_currentPos.heading) &&
0288         m_previousPos.hasCoordinate() &&
0289         m_currentPos.hasCoordinate() &&
0290         Location::distance(m_currentPos.latitude, m_currentPos.longitude, m_previousPos.latitude, m_previousPos.longitude) > 10.0)
0291     {
0292         const auto deltaLon = degToRad(m_currentPos.longitude) - degToRad(m_previousPos.longitude);
0293         const auto y = std::cos(degToRad(m_currentPos.latitude)) * std::sin(deltaLon);
0294         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);
0295         m_currentPos.heading = std::fmod(radToDeg(std::atan2(y, x)) + 360.0, 360.0);
0296     }
0297 
0298     // compute speed based on previous position if necessary
0299     if (std::isnan(m_currentPos.speed) && m_previousPos.hasCoordinate() && m_currentPos.hasCoordinate())
0300     {
0301         const auto dist = Location::distance(m_currentPos.latitude, m_currentPos.longitude, m_previousPos.latitude, m_previousPos.longitude);
0302         const double timeDelta = m_previousPos.timestamp.secsTo(m_currentPos.timestamp);
0303         if (timeDelta > 0) {
0304             m_currentPos.speed = 3.6 * dist / timeDelta;
0305         }
0306     }
0307 
0308     Q_EMIT positionChanged();
0309     requestUpdate();
0310 }
0311 
0312 void OnboardStatusManager::journeyUpdated(const Journey &jny)
0313 {
0314     m_pendingJourneyUpdate = false;
0315     m_journey = jny;
0316 
0317     // don't sanity-check in fake mode, that will likely use outdated data
0318     if (Q_LIKELY(qEnvironmentVariableIsEmpty("KPUBLICTRANSPORT_ONBOARD_FAKE_CONFIG"))) {
0319 
0320         // check if the journey is at least remotely plausible
0321         // sometimes the onboard systems are stuck on a previous journey...
0322         if (jny.expectedArrivalTime().addSecs(60 * 60) < QDateTime::currentDateTime()) {
0323             m_journey = {};
0324         }
0325     }
0326 
0327     Q_EMIT journeyChanged();
0328     requestUpdate();
0329 }
0330 
0331 QNetworkAccessManager* OnboardStatusManager::nam()
0332 {
0333     if (!m_nam) {
0334         m_nam = new QNetworkAccessManager(this);
0335         m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
0336     }
0337     return m_nam;
0338 }
0339 
0340 void OnboardStatusManager::requestUpdate()
0341 {
0342     scheduleUpdate(false);
0343 }
0344 
0345 void OnboardStatusManager::requestForceUpdate()
0346 {
0347     scheduleUpdate(true);
0348 }
0349 
0350 void OnboardStatusManager::scheduleUpdate(bool force)
0351 {
0352     if (!m_backend || m_frontends.empty()) {
0353         m_positionUpdateTimer.stop();
0354         m_journeyUpdateTimer.stop();
0355         return;
0356     }
0357 
0358     if (!m_pendingPositionUpdate) {
0359         int interval = std::numeric_limits<int>::max();
0360         for (auto f : m_frontends) {
0361             if (f->positionUpdateInterval() > 0) {
0362                 interval = std::min(interval, f->positionUpdateInterval());
0363             }
0364         }
0365         if (m_positionUpdateTimer.isActive()) {
0366             interval = std::min(m_positionUpdateTimer.remainingTime() / 1000, interval);
0367         }
0368         if (interval < std::numeric_limits<int>::max()) {
0369             qCDebug(Log) << "next position update:" << interval << force;
0370             m_positionUpdateTimer.start(std::chrono::seconds(force ? 0 : interval));
0371         }
0372     }
0373 
0374     if (!m_pendingJourneyUpdate) {
0375         int interval = std::numeric_limits<int>::max();
0376         for (auto f : m_frontends) {
0377             if (f->journeyUpdateInterval() > 0) {
0378                 interval = std::min(interval, f->journeyUpdateInterval());
0379             }
0380         }
0381         if (m_journeyUpdateTimer.isActive()) {
0382             interval = std::min(m_journeyUpdateTimer.remainingTime() / 1000, interval);
0383         }
0384         if (interval < std::numeric_limits<int>::max()) {
0385             qCDebug(Log) << "next journey update:" << interval << force;
0386             m_journeyUpdateTimer.start(std::chrono::seconds(force ? 0 : interval));
0387         }
0388     }
0389 }
0390 
0391 void OnboardStatusManager::requestPermissions()
0392 {
0393     m_wifiMonitor.requestPermissions();
0394 }