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 &section : 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 }