diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 34545f0..8a4c8b1 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -1,146 +1,148 @@ set(kpublictransport_srcs departurereply.cpp departurerequest.cpp journeyreply.cpp journeyrequest.cpp locationreply.cpp locationrequest.cpp manager.cpp reply.cpp requestcontext.cpp vehiclelayoutreply.cpp vehiclelayoutrequest.cpp backends/abstractbackend.cpp backends/cache.cpp backends/deutschebahnbackend.cpp backends/deutschebahnvehiclelayoutparser.cpp backends/efabackend.cpp backends/efaparser.cpp backends/efacompactparser.cpp backends/efaxmlparser.cpp backends/hafasbackend.cpp backends/hafasparser.cpp backends/hafasmgatebackend.cpp backends/hafasmgateparser.cpp backends/hafasquerybackend.cpp backends/hafasqueryparser.cpp backends/navitiabackend.cpp backends/navitiaparser.cpp + backends/opentripplannerbackend.cpp backends/otp/otp.qrc backends/scopedxmlstreamreader.cpp datatypes/attribution.cpp datatypes/attributionutil.cpp datatypes/backend.cpp datatypes/departure.cpp datatypes/departureutil.cpp datatypes/disruption.cpp datatypes/journey.cpp datatypes/journeyutil.cpp datatypes/json.cpp datatypes/line.cpp datatypes/location.cpp datatypes/locationutil.cpp datatypes/mergeutil.cpp datatypes/notesutil.cpp datatypes/platform.cpp datatypes/platformutils.cpp datatypes/vehicle.cpp models/abstractquerymodel.cpp models/backendmodel.cpp models/departurequerymodel.cpp models/journeyquerymodel.cpp models/locationquerymodel.cpp models/vehiclelayoutquerymodel.cpp networks/networks.qrc ) ecm_qt_declare_logging_category(kpublictransport_srcs HEADER logging.h IDENTIFIER KPublicTransport::Log CATEGORY_NAME org.kde.kpublictransport) add_library(KPublicTransport ${kpublictransport_srcs}) generate_export_header(KPublicTransport BASE_NAME KPublicTransport) set_target_properties(KPublicTransport PROPERTIES VERSION ${KPUBLICTRANSPORT_VERSION_STRING} SOVERSION ${KPUBLICTRANSPORT_SOVERSION} EXPORT_NAME KPublicTransport ) target_include_directories(KPublicTransport PUBLIC "$") target_link_libraries(KPublicTransport PUBLIC Qt5::Gui PRIVATE + KGraphQL Qt5::Network ZLIB::ZLIB ) ecm_generate_headers(KPublicTransport_FORWARDING_HEADERS HEADER_NAMES DepartureReply DepartureRequest JourneyReply JourneyRequest LocationReply LocationRequest Manager Reply VehicleLayoutReply VehicleLayoutRequest PREFIX KPublicTransport REQUIRED_HEADERS KPublicTransport_HEADERS ) # # ### for testing only ecm_generate_headers(KPublicTransport_Backends_FORWARDING_HEADERS HEADER_NAMES Cache HafasMgateParser NavitiaParser PREFIX KPublicTransport REQUIRED_HEADERS KPublicTransport_Backends_HEADERS RELATIVE backends ) ecm_generate_headers(KPublicTransport_Datatypes_FORWARDING_HEADERS HEADER_NAMES Attribution Backend Datatypes Departure Disruption Journey Line Location Platform Vehicle PREFIX KPublicTransport REQUIRED_HEADERS KPublicTransport_Datatypes_HEADERS RELATIVE datatypes ) ecm_generate_headers(KPublicTransport_Models_FORWARDING_HEADERS HEADER_NAMES AbstractQueryModel BackendModel DepartureQueryModel JourneyQueryModel LocationQueryModel VehicleLayoutQueryModel PREFIX KPublicTransport REQUIRED_HEADERS KPublicTransport_Models_HEADERS RELATIVE models ) install(TARGETS KPublicTransport EXPORT KPublicTransportTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES org_kde_kpublictransport.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) install(FILES ${KPublicTransport_FORWARDING_HEADERS} ${KPublicTransport_Datatypes_FORWARDING_HEADERS} ${KPublicTransport_Models_FORWARDING_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPublicTransport ) install(FILES ${KPublicTransport_HEADERS} ${KPublicTransport_Datatypes_HEADERS} ${KPublicTransport_Models_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kpublictransport_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kpublictransport ) diff --git a/src/lib/backends/opentripplannerbackend.cpp b/src/lib/backends/opentripplannerbackend.cpp new file mode 100644 index 0000000..7baf4a6 --- /dev/null +++ b/src/lib/backends/opentripplannerbackend.cpp @@ -0,0 +1,106 @@ +/* + Copyright (C) 2020 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "opentripplannerbackend.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace KPublicTransport; + +OpenTripPlannerBackend::OpenTripPlannerBackend() = default; +OpenTripPlannerBackend::~OpenTripPlannerBackend() = default; + +AbstractBackend::Capabilities OpenTripPlannerBackend::capabilities() const +{ + return m_endpoint.startsWith(QLatin1String("https://")) ? Secure : NoCapability; +} + +bool OpenTripPlannerBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const +{ + Q_UNUSED(type); + return !loc.hasCoordinate(); +} + +bool OpenTripPlannerBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const +{ + KGraphQLRequest gqlReq(QUrl(m_endpoint + QLatin1String("index/graphql"))); + + if (req.hasCoordinate()) { + gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/stationByCoordinate.graphql")); + gqlReq.setVariable(QStringLiteral("lat"), req.latitude()); + gqlReq.setVariable(QStringLiteral("lon"), req.longitude()); + } else { + gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/stationByName.graphql")); + gqlReq.setVariable(QStringLiteral("name"), req.name()); + } + + KGraphQL::query(gqlReq, nam, [this, reply](const KGraphQLReply &gqlReply) { + if (gqlReply.error() != KGraphQLReply::NoError) { + addError(reply, this, Reply::NetworkError, gqlReply.errorString()); + return; + } + // TODO + qDebug() << backendId() << gqlReply.data(); + addError(reply, this, Reply::NetworkError, {}); + }); + + qDebug() << backendId() << "starting"; + return true; +} + +bool OpenTripPlannerBackend::queryDeparture(const DepartureRequest &req, DepartureReply *reply, QNetworkAccessManager *nam) const +{ + KGraphQLRequest gqlReq(QUrl(m_endpoint + QLatin1String("index/graphql"))); + gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/departure.graphql")); + + // TODO + return false; +} + +bool OpenTripPlannerBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const +{ + KGraphQLRequest gqlReq(QUrl(m_endpoint + QLatin1String("index/graphql"))); + gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/journey.graphql")); + gqlReq.setVariable(QStringLiteral("fromLat"), req.from().latitude()); + gqlReq.setVariable(QStringLiteral("fromLon"), req.from().longitude()); + gqlReq.setVariable(QStringLiteral("toLat"), req.to().latitude()); + gqlReq.setVariable(QStringLiteral("toLon"), req.to().longitude()); + // TODO time, arrival/departure + + KGraphQL::query(gqlReq, nam, [this, reply](const KGraphQLReply &gqlReply) { + if (gqlReply.error() != KGraphQLReply::NoError) { + addError(reply, this, Reply::NetworkError, gqlReply.errorString()); + return; + } + // TODO + qDebug() << backendId() << gqlReply.data(); + addError(reply, this, Reply::NetworkError, {}); + }); + + return true; +} diff --git a/src/lib/backends/opentripplannerbackend.h b/src/lib/backends/opentripplannerbackend.h new file mode 100644 index 0000000..1bf26cf --- /dev/null +++ b/src/lib/backends/opentripplannerbackend.h @@ -0,0 +1,47 @@ +/* + Copyright (C) 2020 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef KPUBLICTRANSPORT_OPENTRIPPLANNERBACKEND_H +#define KPUBLICTRANSPORT_OPENTRIPPLANNERBACKEND_H + +#include "abstractbackend.h" + +namespace KPublicTransport { + +/** Access to OpenTripPlanner GraphQL based backends. */ +class OpenTripPlannerBackend : public AbstractBackend +{ + Q_GADGET + Q_PROPERTY(QString endpoint MEMBER m_endpoint) + +public: + OpenTripPlannerBackend(); + ~OpenTripPlannerBackend(); + + Capabilities capabilities() const override; + bool needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const override; + bool queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const override; + bool queryDeparture(const DepartureRequest &req, DepartureReply *reply, QNetworkAccessManager *nam) const override; + bool queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const override; + +private: + QString m_endpoint; +}; + +} + +#endif // KPUBLICTRANSPORT_OPENTRIPPLANNERBACKEND_H diff --git a/src/lib/manager.cpp b/src/lib/manager.cpp index efbde06..04a43ac 100644 --- a/src/lib/manager.cpp +++ b/src/lib/manager.cpp @@ -1,763 +1,768 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "manager.h" #include "departurereply.h" #include "departurerequest.h" #include "journeyreply.h" #include "journeyrequest.h" #include "requestcontext_p.h" #include "locationreply.h" #include "locationrequest.h" #include "logging.h" #include "vehiclelayoutrequest.h" #include "vehiclelayoutreply.h" #include "datatypes/attributionutil_p.h" #include "datatypes/backend.h" #include "datatypes/disruption.h" #include #include #include #include "backends/cache.h" #include "backends/deutschebahnbackend.h" #include "backends/efabackend.h" #include "backends/hafasmgatebackend.h" #include "backends/hafasquerybackend.h" #include "backends/navitiabackend.h" +#include "backends/opentripplannerbackend.h" #include #include #include #include #include #include #include #include #include #include using namespace KPublicTransport; enum { DefaultBackendState = true }; static inline void initResources() { Q_INIT_RESOURCE(networks); } namespace KPublicTransport { class ManagerPrivate { public: QNetworkAccessManager* nam(); void loadNetworks(); std::unique_ptr loadNetwork(const QJsonObject &obj); template std::unique_ptr loadNetwork(const QJsonObject &obj); template bool shouldSkipBackend(const AbstractBackend *backend, const RequestT &req) const; void resolveLocation(const LocationRequest &locReq, const AbstractBackend *backend, const std::function &callback); bool queryJourney(const AbstractBackend *backend, const JourneyRequest &req, JourneyReply *reply); bool queryDeparture(const AbstractBackend *backend, const DepartureRequest &req, DepartureReply *reply); template RepT* makeReply(const ReqT &request); void readCachedAttributions(); Manager *q = nullptr; QNetworkAccessManager *m_nam = nullptr; std::vector> m_backends; std::vector m_attributions; std::vector m_backendMetaData; // we store both explicitly to have a third state, backends with the enabled state being the "default" (whatever that might eventually be) QStringList m_enabledBackends; QStringList m_disabledBackends; bool m_allowInsecure = false; bool m_hasReadCachedAttributions = false; private: bool shouldSkipBackend(const AbstractBackend *backend) const; }; } QNetworkAccessManager* ManagerPrivate::nam() { if (!m_nam) { m_nam = new QNetworkAccessManager(q); m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); m_nam->setStrictTransportSecurityEnabled(true); m_nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/org.kde.kpublictransport/hsts/")); } return m_nam; } static QString translatedValue(const QJsonObject &obj, const QString &key) { auto languageWithCountry = QLocale().name(); auto it = obj.constFind(key + QLatin1Char('[') + languageWithCountry + QLatin1Char(']')); if (it != obj.constEnd()) { return it.value().toString(); } const auto language = languageWithCountry.midRef(0, languageWithCountry.indexOf(QLatin1Char('_'))); it = obj.constFind(key + QLatin1Char('[') + language + QLatin1Char(']')); if (it != obj.constEnd()) { return it.value().toString(); } return obj.value(key).toString(); } void ManagerPrivate::loadNetworks() { QDirIterator it(QStringLiteral(":/org.kde.pim/kpublictransport/networks")); while (it.hasNext()) { QFile f(it.next()); if (!f.open(QFile::ReadOnly)) { qCWarning(Log) << "Failed to open public transport network configuration:" << f.errorString(); continue; } QJsonParseError error; const auto doc = QJsonDocument::fromJson(f.readAll(), &error); if (error.error != QJsonParseError::NoError) { qCWarning(Log) << "Failed to parse public transport network configuration:" << error.errorString() << it.fileName(); continue; } auto net = loadNetwork(doc.object()); if (net) { net->setBackendId(it.fileInfo().baseName()); net->init(); if (!net->attribution().isEmpty()) { m_attributions.push_back(net->attribution()); } Backend metaData; metaData.setIdentifier(net->backendId()); const auto jsonMetaData = doc.object().value(QLatin1String("KPlugin")).toObject(); metaData.setName(translatedValue(jsonMetaData, QStringLiteral("Name"))); metaData.setDescription(translatedValue(jsonMetaData, QStringLiteral("Description"))); metaData.setIsSecure(net->capabilities() & AbstractBackend::Secure); m_backendMetaData.push_back(std::move(metaData)); m_backends.push_back(std::move(net)); } else { qCWarning(Log) << "Failed to load public transport network configuration config:" << it.fileName(); } } std::stable_sort(m_backends.begin(), m_backends.end(), [](const auto &lhs, const auto &rhs) { return lhs->backendId() < rhs->backendId(); }); std::stable_sort(m_backendMetaData.begin(), m_backendMetaData.end(), [](const auto &lhs, const auto &rhs) { return lhs.identifier() < rhs.identifier(); }); AttributionUtil::sort(m_attributions); qCDebug(Log) << m_backends.size() << "public transport network configurations loaded"; } std::unique_ptr ManagerPrivate::loadNetwork(const QJsonObject &obj) { const auto type = obj.value(QLatin1String("type")).toString(); if (type == QLatin1String("navitia")) { return loadNetwork(obj); } + if (type == QLatin1String("otp")) { + //return loadNetwork(obj); + return nullptr; + } if (type == QLatin1String("hafas_mgate")) { return loadNetwork(obj); } if (type == QLatin1String("hafas_query")) { return loadNetwork(obj); } if (type == QLatin1String("efa")) { return loadNetwork(obj); } if (type == QLatin1String("deutschebahn")) { return loadNetwork(obj); } qCWarning(Log) << "Unknown backend type:" << type; return {}; } static void applyBackendOptions(AbstractBackend *backend, const QMetaObject *mo, const QJsonObject &obj) { const auto opts = obj.value(QLatin1String("options")).toObject(); for (auto it = opts.begin(); it != opts.end(); ++it) { const auto idx = mo->indexOfProperty(it.key().toUtf8().constData()); if (idx < 0) { qCWarning(Log) << "Unknown backend setting:" << it.key(); continue; } const auto mp = mo->property(idx); if (it.value().isObject()) { mp.writeOnGadget(backend, it.value().toObject()); } else if (it.value().isArray()) { mp.writeOnGadget(backend, it.value().toArray()); } else { mp.writeOnGadget(backend, it.value().toVariant()); } } const auto filter = obj.value(QLatin1String("filter")).toObject(); const auto geoFilter = filter.value(QLatin1String("geo")).toArray(); if (!geoFilter.isEmpty()) { QPolygonF poly; poly.reserve(geoFilter.size()); for (const auto &coordV : geoFilter) { const auto coordA = coordV.toArray(); poly.push_back({coordA[0].toDouble(), coordA[1].toDouble()}); } backend->setGeoFilter(poly); } const auto attrObj = obj.value(QLatin1String("attribution")).toObject(); const auto attr = Attribution::fromJson(attrObj); backend->setAttribution(attr); const auto tzId = obj.value(QLatin1String("timezone")).toString(); if (!tzId.isEmpty()) { backend->setTimeZone(QTimeZone(tzId.toUtf8())); } } template std::unique_ptr ManagerPrivate::loadNetwork(const QJsonObject &obj) { std::unique_ptr backend(new T); applyBackendOptions(backend.get(), &T::staticMetaObject, obj); return backend; } bool ManagerPrivate::shouldSkipBackend(const AbstractBackend *backend) const { if (!backend->hasCapability(AbstractBackend::Secure) && !m_allowInsecure) { qCDebug(Log) << "Skipping insecure backend:" << backend->backendId(); return true; } return !q->isBackendEnabled(backend->backendId()); } template bool ManagerPrivate::shouldSkipBackend(const AbstractBackend *backend, const RequestT &req) const { if (!req.backendIds().isEmpty() && !req.backendIds().contains(backend->backendId())) { qCDebug(Log) << "Skipping backend" << backend->backendId() << "due to explicit request"; return true; } return shouldSkipBackend(backend); } // IMPORTANT callback must not be called directly, but only via queued invocation, // our callers rely on that to not mess up sync/async response handling void ManagerPrivate::resolveLocation(const LocationRequest &locReq, const AbstractBackend *backend, const std::function &callback) { // check if this location query is cached already const auto cacheEntry = Cache::lookupLocation(backend->backendId(), locReq.cacheKey()); switch (cacheEntry.type) { case CacheHitType::Negative: QTimer::singleShot(0, q, [callback]() { callback({}); }); return; case CacheHitType::Positive: if (!cacheEntry.data.empty()) { const auto loc = cacheEntry.data[0]; QTimer::singleShot(0, q, [callback, loc]() { callback(loc); }); return; } break; case CacheHitType::Miss: break; } // actually do the location query auto locReply = new LocationReply(locReq, q); if (backend->queryLocation(locReq, locReply, nam())) { locReply->setPendingOps(1); } else { locReply->setPendingOps(0); } QObject::connect(locReply, &Reply::finished, q, [callback, locReply]() { locReply->deleteLater(); if (locReply->result().empty()) { callback({}); } else { callback(locReply->result()[0]); } }); } bool ManagerPrivate::queryJourney(const AbstractBackend* backend, const JourneyRequest &req, JourneyReply *reply) { if (shouldSkipBackend(backend, req)) { return false; } if (backend->isLocationExcluded(req.from()) && backend->isLocationExcluded(req.to())) { qCDebug(Log) << "Skipping backend based on location filter:" << backend->backendId(); return false; } reply->addAttribution(backend->attribution()); auto cache = Cache::lookupJourney(backend->backendId(), req.cacheKey()); switch (cache.type) { case CacheHitType::Negative: qCDebug(Log) << "Negative cache hit for backend" << backend->backendId(); return false; case CacheHitType::Positive: qCDebug(Log) << "Positive cache hit for backend" << backend->backendId(); reply->addAttributions(std::move(cache.attributions)); reply->addResult(backend, std::move(cache.data)); return false; case CacheHitType::Miss: qCDebug(Log) << "Cache miss for backend" << backend->backendId(); break; } // resolve locations if needed if (backend->needsLocationQuery(req.from(), AbstractBackend::QueryType::Journey)) { LocationRequest fromReq; fromReq.setCoordinate(req.from().latitude(), req.from().longitude()); fromReq.setName(req.from().name()); resolveLocation(fromReq, backend, [reply, backend, req, this](const Location &loc) { auto jnyRequest = req; const auto fromLoc = Location::merge(jnyRequest.from(), loc); jnyRequest.setFrom(fromLoc); if (backend->needsLocationQuery(jnyRequest.to(), AbstractBackend::QueryType::Journey)) { LocationRequest toReq; toReq.setCoordinate(jnyRequest.to().latitude(), jnyRequest.to().longitude()); toReq.setName(jnyRequest.to().name()); resolveLocation(toReq, backend, [jnyRequest, reply, backend, this](const Location &loc) { auto jnyReq = jnyRequest; const auto toLoc = Location::merge(jnyRequest.to(), loc); jnyReq.setTo(toLoc); if (!backend->queryJourney(jnyReq, reply, nam())) { reply->addError(Reply::NotFoundError, {}); } }); return; } if (!backend->queryJourney(jnyRequest, reply, nam())) { reply->addError(Reply::NotFoundError, {}); } }); return true; } if (backend->needsLocationQuery(req.to(), AbstractBackend::QueryType::Journey)) { LocationRequest toReq; toReq.setCoordinate(req.to().latitude(), req.to().longitude()); toReq.setName(req.to().name()); resolveLocation(toReq, backend, [req, toReq, reply, backend, this](const Location &loc) { const auto toLoc = Location::merge(req.to(), loc); auto jnyRequest = req; jnyRequest.setTo(toLoc); if (!backend->queryJourney(jnyRequest, reply, nam())) { reply->addError(Reply::NotFoundError, {}); } }); return true; } return backend->queryJourney(req, reply, nam()); } bool ManagerPrivate::queryDeparture(const AbstractBackend *backend, const DepartureRequest &req, DepartureReply *reply) { if (shouldSkipBackend(backend, req)) { return false; } if (req.mode() == DepartureRequest::QueryArrival && (backend->capabilities() & AbstractBackend::CanQueryArrivals) == 0) { qCDebug(Log) << "Skipping backend due to not supporting arrival queries:" << backend->backendId(); return false; } if (backend->isLocationExcluded(req.stop())) { qCDebug(Log) << "Skipping backend based on location filter:" << backend->backendId(); return false; } reply->addAttribution(backend->attribution()); auto cache = Cache::lookupDeparture(backend->backendId(), req.cacheKey()); switch (cache.type) { case CacheHitType::Negative: qCDebug(Log) << "Negative cache hit for backend" << backend->backendId(); return false; case CacheHitType::Positive: qCDebug(Log) << "Positive cache hit for backend" << backend->backendId(); reply->addAttributions(std::move(cache.attributions)); reply->addResult(backend, std::move(cache.data)); return false; case CacheHitType::Miss: qCDebug(Log) << "Cache miss for backend" << backend->backendId(); break; } // check if we first need to resolve the location first if (backend->needsLocationQuery(req.stop(), AbstractBackend::QueryType::Departure)) { qCDebug(Log) << "Backend needs location query first:" << backend->backendId(); LocationRequest locReq; locReq.setCoordinate(req.stop().latitude(), req.stop().longitude()); locReq.setName(req.stop().name()); resolveLocation(locReq, backend, [reply, req, backend, this](const Location &loc) { const auto depLoc = Location::merge(req.stop(), loc); auto depRequest = req; depRequest.setStop(depLoc); if (!backend->queryDeparture(depRequest, reply, nam())) { reply->addError(Reply::NotFoundError, {}); } }); return true; } return backend->queryDeparture(req, reply, nam()); } void ManagerPrivate::readCachedAttributions() { if (m_hasReadCachedAttributions) { return; } Cache::allCachedAttributions(m_attributions); m_hasReadCachedAttributions = true; } template RepT* ManagerPrivate::makeReply(const ReqT &request) { auto reply = new RepT(request, q); QObject::connect(reply, &Reply::finished, q, [this, reply]() { AttributionUtil::merge(m_attributions, reply->attributions()); }); return reply; } Manager::Manager(QObject *parent) : QObject(parent) , d(new ManagerPrivate) { initResources(); qRegisterMetaType(); d->q = this; d->loadNetworks(); Cache::expire(); } Manager::~Manager() = default; void Manager::setNetworkAccessManager(QNetworkAccessManager *nam) { if (d->m_nam == nam) { return; } if (d->m_nam->parent() == this) { delete d->m_nam; } d->m_nam = nam; } bool Manager::allowInsecureBackends() const { return d->m_allowInsecure; } void Manager::setAllowInsecureBackends(bool insecure) { if (d->m_allowInsecure == insecure) { return; } d->m_allowInsecure = insecure; emit configurationChanged(); } JourneyReply* Manager::queryJourney(const JourneyRequest &req) const { auto reply = d->makeReply(req); int pendingOps = 0; // validate input if (!req.isValid()) { reply->addError(Reply::InvalidRequest, {}); reply->setPendingOps(pendingOps); return reply; } // first time/direct query if (req.contexts().empty()) { for (const auto &backend : d->m_backends) { if (d->queryJourney(backend.get(), req, reply)) { ++pendingOps; } } // subsequent earlier/later query } else { for (const auto &context : req.contexts()) { // backend supports this itself if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextJourney)) ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousJourney))) { if (d->queryJourney(context.backend, req, reply)) { ++pendingOps; continue; } } // backend doesn't support this, let's try to emulate if (context.type == RequestContext::Next && req.dateTimeMode() == JourneyRequest::Departure) { auto r = req; r.setDepartureTime(context.dateTime); if (d->queryJourney(context.backend, r, reply)) { ++pendingOps; continue; } } else if (context.type == RequestContext::Previous && req.dateTimeMode() == JourneyRequest::Departure) { auto r = req; r.setArrivalTime(context.dateTime); if (d->queryJourney(context.backend, r, reply)) { ++pendingOps; continue; } } } } reply->setPendingOps(pendingOps); return reply; } DepartureReply* Manager::queryDeparture(const DepartureRequest &req) const { auto reply = d->makeReply(req); int pendingOps = 0; // validate input if (!req.isValid()) { reply->addError(Reply::InvalidRequest, {}); reply->setPendingOps(pendingOps); return reply; } // first time/direct query if (req.contexts().empty()) { for (const auto &backend : d->m_backends) { if (d->queryDeparture(backend.get(), req, reply)) { ++pendingOps; } } // subsequent earlier/later query } else { for (const auto &context : req.contexts()) { // backend supports this itself if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextDeparture)) ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousDeparture))) { if (d->queryDeparture(context.backend, req, reply)) { ++pendingOps; continue; } } // backend doesn't support this, let's try to emulate if (context.type == RequestContext::Next && req.mode() == DepartureRequest::QueryDeparture) { auto r = req; r.setDateTime(context.dateTime); if (d->queryDeparture(context.backend, r, reply)) { ++pendingOps; continue; } } } } reply->setPendingOps(pendingOps); return reply; } LocationReply* Manager::queryLocation(const LocationRequest &req) const { auto reply = d->makeReply(req); int pendingOps = 0; // validate input if (!req.isValid()) { reply->addError(Reply::InvalidRequest, {}); reply->setPendingOps(pendingOps); return reply; } for (const auto &backend : d->m_backends) { if (d->shouldSkipBackend(backend.get(), req)) { continue; } if (req.hasCoordinate() && backend->isCoordinateExcluded(req.latitude(), req.longitude())) { qCDebug(Log) << "Skipping backend based on location filter:" << backend->backendId(); continue; } reply->addAttribution(backend->attribution()); auto cache = Cache::lookupLocation(backend->backendId(), req.cacheKey()); switch (cache.type) { case CacheHitType::Negative: qCDebug(Log) << "Negative cache hit for backend" << backend->backendId(); break; case CacheHitType::Positive: qCDebug(Log) << "Positive cache hit for backend" << backend->backendId(); reply->addAttributions(std::move(cache.attributions)); reply->addResult(std::move(cache.data)); break; case CacheHitType::Miss: qCDebug(Log) << "Cache miss for backend" << backend->backendId(); if (backend->queryLocation(req, reply, d->nam())) { ++pendingOps; } break; } } reply->setPendingOps(pendingOps); return reply; } VehicleLayoutReply* Manager::queryVehicleLayout(const VehicleLayoutRequest &req) const { auto reply = d->makeReply(req); int pendingOps = 0; // validate input if (!req.isValid()) { reply->addError(Reply::InvalidRequest, {}); reply->setPendingOps(pendingOps); return reply; } for (const auto &backend : d->m_backends) { if (d->shouldSkipBackend(backend.get(), req)) { continue; } if (req.departure().stopPoint().hasCoordinate() && backend->isLocationExcluded(req.departure().stopPoint())) { qCDebug(Log) << "Skipping backend based on location filter:" << backend->backendId(); continue; } reply->addAttribution(backend->attribution()); // TODO check cache if (backend->queryVehicleLayout(req, reply, d->nam())) { ++pendingOps; } } reply->setPendingOps(pendingOps); return reply; } const std::vector& Manager::attributions() const { d->readCachedAttributions(); return d->m_attributions; } QVariantList Manager::attributionsVariant() const { d->readCachedAttributions(); QVariantList l; l.reserve(d->m_attributions.size()); std::transform(d->m_attributions.begin(), d->m_attributions.end(), std::back_inserter(l), [](const auto &attr) { return QVariant::fromValue(attr); }); return l; } const std::vector& Manager::backends() const { return d->m_backendMetaData; } bool Manager::isBackendEnabled(const QString &backendId) const { if (std::binary_search(d->m_disabledBackends.cbegin(), d->m_disabledBackends.cend(), backendId)) { return false; } if (std::binary_search(d->m_enabledBackends.cbegin(), d->m_enabledBackends.cend(), backendId)) { return true; } return DefaultBackendState; } static void sortedInsert(QStringList &l, const QString &value) { const auto it = std::lower_bound(l.begin(), l.end(), value); if (it == l.end() || (*it) != value) { l.insert(it, value); } } static void sortedRemove(QStringList &l, const QString &value) { const auto it = std::lower_bound(l.begin(), l.end(), value); if (it != l.end() && (*it) == value) { l.erase(it); } } void Manager::setBackendEnabled(const QString &backendId, bool enabled) { if (enabled) { sortedInsert(d->m_enabledBackends, backendId); sortedRemove(d->m_disabledBackends, backendId); } else { sortedRemove(d->m_enabledBackends, backendId); sortedInsert(d->m_disabledBackends, backendId); } emit configurationChanged(); } QStringList Manager::enabledBackends() const { return d->m_enabledBackends; } void Manager::setEnabledBackends(const QStringList &backendIds) { QSignalBlocker blocker(this); // no change signals during settings restore for (const auto &backendId : backendIds) { setBackendEnabled(backendId, true); } } QStringList Manager::disabledBackends() const { return d->m_disabledBackends; } void Manager::setDisabledBackends(const QStringList &backendIds) { QSignalBlocker blocker(this); // no change signals during settings restore for (const auto &backendId : backendIds) { setBackendEnabled(backendId, false); } } diff --git a/src/lib/networks/networks.qrc b/src/lib/networks/networks.qrc index e28bc8a..48cbc0e 100644 --- a/src/lib/networks/networks.qrc +++ b/src/lib/networks/networks.qrc @@ -1,49 +1,60 @@ navitia.json railteam.json at_oebb.json be_sncb.json ch_sbb.json de_db.json de_dbwagenreihung.json dk_dsb.json + fi_digitransit.json + fi_waltti.json fr_sncf.json ie_tfi.json lu_mobiliteitszentral.json nl_ns.json pl_pkp.json se_resrobot.json uk_traveline.json at_3_vor.json at_4_linz.json at_4_ooevv.json at_5_svv.json at_6_vvst.json at_7_vvt.json at_8_vvv.json + au_nsw.json de_bb_vbb.json de_be_bvg.json de_bw_kvv.json de_bw_vvs.json de_by_bayern.json de_by_mvv.json de_by_vgn.json de_he_rmv.json de_ni_gvh.json de_nw_avv.json + de_nw_muenster.json de_nw_vrr.json de_sh_sh.json de_st_insa.json + fi_17_helsinki.json + + it_21_piemonte.json + it_21_torino.json + us_ca_bart.json + us_ca_la_metro.json + us_ga_marta.json us_il_chicago.json diff --git a/tests/TestLocationsModel.qml b/tests/TestLocationsModel.qml index 5b7bb81..689d6a8 100644 --- a/tests/TestLocationsModel.qml +++ b/tests/TestLocationsModel.qml @@ -1,81 +1,86 @@ /* Copyright (C) 2018-2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQml.Models 2.10 ListModel { ListElement { label: "Paris Charles de Gaulle Airport"; lat: 49.00406; lon: 2.57110 } ListElement { label: "Paris Gare de Lyon"; lat: 48.84388; lon: 2.37708 } ListElement { label: "Zürich Flughafen"; lat: 47.45050; lon: 8.56275 } ListElement { label: "Randa"; lat: 46.09901; lon: 7.78315 } ListElement { label: "Brussels Gare du Midi"; lat: 50.83588; lon: 4.33620 } ListElement { label: "FOSDEM"; name: "ULB"; lat: 50.8139; lon: 4.38464 } ListElement { label: "Brussels - Zaventem Airport"; lat: 50.8978; lon: 4.48216 } ListElement { label: "Akademy 2008 (Mechelen)"; name: "Mechelen"; lat: 51.0175; lon: 4.48347 } ListElement { label: "Wien Flughafen"; lat: 48.12083; lon: 16.56312 } ListElement { label: "Wien Hauptbahnhof"; lat: 48.18282; lon: 16.37859 } ListElement { label: "Akademy 2018 BBQ (Vienna)"; name: "?"; lat: 48.21612; lon: 16.43191 } ListElement { label: "Almeria Airport"; lat: 36.84774; lon: -2.37251 } ListElement { label: "Akademy 2017 Accomodation (Almeria)"; name: "?"; lat: 36.83731; lon: -2.44788 } ListElement { label: "Akademy 2017 Venue (Almeria)"; name: "?"; lat: 36.82784; lon: -2.40377 } ListElement { label: "Berlin Flughafen Tegel"; lat: 52.55420; lon: 13.29281 } ListElement { label: "Berlin Flughafen Schönefeld"; lat: 52.38841; lon: 13.51870 } ListElement { label: "Akademy 2016 Venue (Berlin)"; name: "Berlin Alexanderplatz"; lat: 52.52068; lon: 13.41644 } ListElement { label: "Berlin Südkreuz"; lat: 52.47577; lon: 13.36535 } ListElement { label: "Brno central station"; lat: 49.19069; lon: 16.61287 } ListElement { label: "Akademy 2014 Venue (Brno)"; name: "?"; lat: 49.22462; lon: 16.57564 } ListElement { label: "Glasgow Airport"; lat: 55.8641; lon: -4.43157 } ListElement { label: "Akademy 2007 (Glasgow)"; name: "Glasgow Queen Street Rail Station"; lat: 55.8624; lon: -4.25082 } ListElement { label: "Akademy 2004 Accomodation (Ludwigsburg)"; name: "Schlösslesfeld, Ludwigsburg"; lat: 48.9032; lon: 9.22097 } ListElement { label: "Akademy 2004 Venue (Ludwigsburg)"; name: "Ludwigsburg, Rathaus"; lat: 48.8929; lon: 9.18721 } ListElement { label: "Copenhagen Central"; lat: 55.67238; lon: 12.56489 } ListElement { label: "Frankfurt (Main) Hauptbahnhof"; lat: 50.106944; lon: 8.6625 } ListElement { label: "Amsterdam Schiphol Airport"; lat: 52.309; lon: 4.7611 } ListElement { label: "Leipzig Hauptbahnhof"; lat: 51.3455; lon: 12.3821 } ListElement { label: "Leipzig Messe"; lat: 51.39737; lon: 12.39528 } ListElement { label: "Privacy Sprint Venue (Leipzig)"; name: "Leipzig, Lindenauer Markt"; lat: 51.3346; lon: 12.3249 } ListElement { label: "Toulouse-Blagnac Airport"; lat: 43.6301; lon: 1.37546 } ListElement { label: "Toulouse-Matabiau"; lat: 43.6114; lon: 1.45394 } ListElement { label: "PIM Sprint Venue (Toulouse)"; name: "Toulouse Jean Jaures"; lat: 43.6054; lon: 1.4489 } ListElement { label: "Avignon Centre"; lat: 43.9419; lon: 4.80552 } ListElement { label: "Hagfors flygplats"; lat: 60.0262; lon: 13.582 } ListElement { label: "Milano Centrale"; lat: 45.4863466378704; lon: 9.20452826501544 } ListElement { label: "Milano Porta Garibaldi"; lat: 45.4849263704046; lon: 9.18768320503651 } ListElement { label: "Malpensa Aeroporto"; lat: 45.6279397021696; lon: 8.71101861371495 } ListElement { label: "Milano Bicocca"; lat: 45.51466; lon: 9.20529 } ListElement { label: "Akademy 2019"; name: "Universitia Biocca Scienza"; lat: 45.51362; lon: 9.21116 } ListElement { label: "Nürnberg Flughafen"; lat: 49.49400; lon: 11.07854 } ListElement { label: "Nürnberg Hauptbahnhof"; lat: 49.44553; lon: 11.08228 } ListElement { label: "SUSE office Nürnberg"; name: "Nürnberg Maxtor"; lat: 49.45960; lon: 11.08203 } ListElement { label: "Barcelona Sants"; lat: 41.37910; lon: 2.13983 } ListElement { label: "Strasbourg Gare Centrale"; lat: 48.58507; lon: 7.73368 } + + ListElement { label: "Tampere Station (Akademy 2010)"; name: "Tampere"; lat: 61.49859; lon: 23.77392 } + ListElement { label: "Helsinki Vantaa Airport (HEL)"; name: "Lentosema"; lat: 60.31621; lon: 24.96906 } + ListElement { label: "Helsinki central station"; name: "Helsinki"; lat: 60.17170; lon: 24.94094 } + ListElement { label: "Leppävaara"; lat: 60.21946; lon: 24.81309 } }