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 }