File indexing completed on 2024-05-12 04:42:55
0001 /* 0002 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> 0003 SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 0006 #include "scriptedrestonboardbackend_p.h" 0007 #include "logging.h" 0008 #include "positiondata_p.h" 0009 0010 #include "../lib/datatypes/stopoverutil_p.h" 0011 0012 #include <KPublicTransport/Journey> 0013 #include <KPublicTransport/Stopover> 0014 0015 #include <KTimeZone> 0016 0017 #include <QFile> 0018 #include <QJSEngine> 0019 #include <QJsonValue> 0020 #include <QJsonObject> 0021 #include <QNetworkRequest> 0022 #include <QScopeGuard> 0023 #include <QTimer> 0024 #include <QTimeZone> 0025 0026 using namespace KPublicTransport; 0027 0028 ScriptedRestOnboardBackend::ScriptedRestOnboardBackend(QObject *parent) 0029 : RestOnboardBackend(parent) 0030 { 0031 } 0032 0033 ScriptedRestOnboardBackend::~ScriptedRestOnboardBackend() 0034 { 0035 if (m_watchdogTimer) { 0036 m_watchdogTimer->deleteLater(); 0037 } 0038 m_watchdogThread.quit(); 0039 m_watchdogThread.wait(); 0040 } 0041 0042 bool ScriptedRestOnboardBackend::supportsPosition() const 0043 { 0044 return m_positionEndpoint.isValid(); 0045 } 0046 0047 bool ScriptedRestOnboardBackend::supportsJourney() const 0048 { 0049 return m_journeyEndpoint.isValid(); 0050 } 0051 0052 QNetworkRequest ScriptedRestOnboardBackend::createPositionRequest() const 0053 { 0054 return QNetworkRequest(m_positionEndpoint); 0055 } 0056 0057 QNetworkRequest ScriptedRestOnboardBackend::createJourneyRequest() const 0058 { 0059 return QNetworkRequest(m_journeyEndpoint); 0060 } 0061 0062 static double strictToNumber(const QJSValue &val) 0063 { 0064 if (val.isNumber()) { 0065 return val.toNumber(); 0066 } 0067 if (val.isString()) { 0068 bool result = false; 0069 const auto n = val.toString().toDouble(&result); 0070 return result ? n : NAN; 0071 } 0072 return NAN; 0073 } 0074 0075 PositionData ScriptedRestOnboardBackend::parsePositionData(const QJsonValue &response) const 0076 { 0077 setupEngine(); 0078 0079 // watchdog setup 0080 QMetaObject::invokeMethod(m_watchdogTimer, qOverload<>(&QTimer::start)); 0081 const auto watchdogStop = qScopeGuard([this]() { 0082 QMetaObject::invokeMethod(m_watchdogTimer, qOverload<>(&QTimer::stop)); 0083 }); 0084 m_engine->setInterrupted(false); 0085 0086 auto func = m_engine->globalObject().property(m_positionFunction); 0087 if (!func.isCallable()) { 0088 qCWarning(Log) << "Script entry point not found!" << m_positionFunction; 0089 return {}; 0090 } 0091 0092 const auto arg = m_engine->toScriptValue(response); 0093 const auto result = func.call(QJSValueList{arg}); 0094 if (result.isError()) { 0095 printScriptError(result); 0096 return {}; 0097 } 0098 0099 // convert JS result 0100 PositionData pos; 0101 pos.timestamp = QDateTime::fromString(result.property(QStringLiteral("timestamp")).toString(), Qt::ISODate); 0102 pos.latitude = strictToNumber(result.property(QStringLiteral("latitude"))); 0103 pos.longitude = strictToNumber(result.property(QStringLiteral("longitude"))); 0104 pos.speed = strictToNumber(result.property(QStringLiteral("speed"))); 0105 pos.heading = strictToNumber(result.property(QStringLiteral("heading"))); 0106 pos.altitude = strictToNumber(result.property(QStringLiteral("altitude"))); 0107 return pos; 0108 } 0109 0110 Journey ScriptedRestOnboardBackend::parseJourneyData(const QJsonValue &response) const 0111 { 0112 setupEngine(); 0113 0114 // watchdog setup 0115 QMetaObject::invokeMethod(m_watchdogTimer, qOverload<>(&QTimer::start)); 0116 const auto watchdogStop = qScopeGuard([this]() { 0117 QMetaObject::invokeMethod(m_watchdogTimer, qOverload<>(&QTimer::stop)); 0118 }); 0119 m_engine->setInterrupted(false); 0120 0121 auto func = m_engine->globalObject().property(m_journeyFunction); 0122 if (!func.isCallable()) { 0123 qCWarning(Log) << "Script entry point not found!" << m_journeyFunction; 0124 return {}; 0125 } 0126 0127 const auto arg = m_engine->toScriptValue(response); 0128 const auto result = func.call(QJSValueList{arg}); 0129 if (result.isError()) { 0130 printScriptError(result); 0131 return {}; 0132 } 0133 0134 // convert JS result 0135 auto jny = Journey::fromJson(QJsonValue::fromVariant(result.toVariant()).toObject()); 0136 auto sections = jny.takeSections(); 0137 0138 for (auto §ion : sections) { 0139 auto stops = section.takeIntermediateStops(); 0140 // fill in missing titmezones 0141 for (auto &stop : stops) { 0142 QTimeZone tz(stop.stopPoint().timeZone()); 0143 0144 if (!tz.isValid() && stop.stopPoint().hasCoordinate()) { 0145 if (const auto tzId = KTimeZone::fromLocation(stop.stopPoint().latitude(), stop.stopPoint().longitude())) { 0146 tz = QTimeZone(tzId); 0147 } 0148 } 0149 0150 if (tz.isValid()) { 0151 StopoverUtil::applyTimeZone(stop, tz); 0152 } 0153 } 0154 0155 // many backends will have the entire trip as intermediate stops, redistribute 0156 // that for our format 0157 if (section.from().isEmpty() && !stops.empty()) { 0158 const auto s = stops.front(); 0159 section.setDeparture(s); 0160 stops.erase(stops.begin()); 0161 } 0162 0163 if (section.to().isEmpty() && !stops.empty()) { 0164 const auto s = stops.back(); 0165 section.setArrival(s); 0166 stops.pop_back(); 0167 } 0168 0169 section.setIntermediateStops(std::move(stops)); 0170 } 0171 0172 jny.setSections(std::move(sections)); 0173 return jny; 0174 } 0175 0176 void ScriptedRestOnboardBackend::setupEngine() const 0177 { 0178 if (m_engine) { 0179 return; 0180 } 0181 m_engine.reset(new QJSEngine); 0182 m_engine->installExtensions(QJSEngine::ConsoleExtension); 0183 0184 m_watchdogThread.start(); 0185 m_watchdogTimer = new QTimer; 0186 m_watchdogTimer->setInterval(std::chrono::milliseconds(500)); 0187 m_watchdogTimer->setSingleShot(true); 0188 m_watchdogTimer->moveToThread(&m_watchdogThread); 0189 QObject::connect(m_watchdogTimer, &QTimer::timeout, this, [this]() { m_engine->setInterrupted(true); }, Qt::DirectConnection); 0190 0191 // load script 0192 QFile f(QLatin1String(":/org.kde.kpublictransport.onboard/") + m_scriptName); 0193 if (!f.open(QFile::ReadOnly)) { 0194 qCWarning(Log) << "Failed to open extractor script" << f.fileName() << f.errorString(); 0195 return; 0196 } 0197 0198 const auto result = m_engine->evaluate(QString::fromUtf8(f.readAll()), f.fileName()); 0199 if (result.isError()) { 0200 printScriptError(result); 0201 return; 0202 } 0203 } 0204 0205 void ScriptedRestOnboardBackend::printScriptError(const QJSValue &error) const 0206 { 0207 qCWarning(Log) << "JS ERROR: " << m_scriptName << error.property(QLatin1String("lineNumber")).toInt() << ": " << error.toString(); 0208 }