diff --git a/engine/datasource.cpp b/engine/datasource.cpp index e76e48f..a354f1f 100644 --- a/engine/datasource.cpp +++ b/engine/datasource.cpp @@ -1,304 +1,304 @@ /* * Copyright 2012 Friedrich Pülz * * 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 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Own includes #include "datasource.h" #include "serviceprovider.h" #include "serviceproviderglobal.h" // KDE includes -#include +#include // Qt includes #include #include DataSource::DataSource( const QString &dataSource ) : m_name(dataSource) { } DataSource::~DataSource() { } ProvidersDataSource::ProvidersDataSource( const QString &dataSource, const QHash &providerData ) : DataSource(dataSource), m_dirty(true), m_providerData(providerData) { } SimpleDataSource::SimpleDataSource( const QString &dataSource, const QVariantHash &data ) : DataSource(dataSource), m_data(data) { } TimetableDataSource::TimetableDataSource( const QString &dataSource, const QVariantHash &data ) : SimpleDataSource(dataSource, data), m_updateTimer(0), m_cleanupTimer(0), m_updateAdditionalDataDelayTimer(0) { } TimetableDataSource::~TimetableDataSource() { delete m_updateTimer; delete m_cleanupTimer; delete m_updateAdditionalDataDelayTimer; } QString ProvidersDataSource::toStaticState( const QString &dynamicStateId ) { if ( dynamicStateId == QLatin1String("importing_gtfs_feed") ) { return "gtfs_feed_import_pending"; } else { return dynamicStateId; } } ProvidersDataSource::ProviderData::ProviderData( const QVariantHash &data, const QString &_stateId, const QVariantHash &_stateData ) : dataWithoutState(data), stateId(ProvidersDataSource::toStaticState(_stateId)), stateData(_stateData) { } QVariantHash ProvidersDataSource::ProviderData::data() const { // Combine all provider data with state ID and state data QVariantHash data = dataWithoutState; data[ "state" ] = stateId; data[ "stateData" ] = stateData; return data; } QVariantHash ProvidersDataSource::data() const { // Combine all provider data QVariantHash's into one QVariantHash data; for ( QHash::ConstIterator it = m_providerData.constBegin(); it != m_providerData.constEnd(); ++it ) { data.insert( it.key(), it.value().data() ); } return data; } void TimetableDataSource::addUsingDataSource( const QSharedPointer< AbstractRequest > &request, const QString &sourceName, const QDateTime &dateTime, int count ) { m_dataSources[ sourceName ] = SourceData( request, dateTime, count ); } void TimetableDataSource::removeUsingDataSource( const QString &sourceName ) { m_dataSources.remove( sourceName ); } bool TimetableDataSource::enoughDataAvailable( const QDateTime &dateTime, int count ) { bool foundTime = false; int foundCount = 0; const QVariantList items = timetableItems(); for ( int i = 0; i < items.count(); ++i ) { const QVariantHash data = items[i].toHash(); const QDateTime itemDateTime = data["DepartureDateTime"].toDateTime(); if ( itemDateTime < dateTime ) { foundTime = true; } else if ( itemDateTime >= dateTime ) { if ( dateTime.secsTo(itemDateTime) < 2 * 60 ) { foundTime = true; } if ( !foundTime ) { break; } foundCount = items.count() - i; break; } } return foundTime && foundCount > qMax(1, int(count * 0.8)); } QString TimetableDataSource::timetableItemKey() const { return m_data.contains("departures") ? "departures" : (m_data.contains("arrivals") ? "arrivals" : (m_data.contains("journeys") ? "journeys" : "stops")); } void TimetableDataSource::setTimetableItems( const QVariantList &items ) { m_data[ timetableItemKey() ] = items; } UpdateFlags TimetableDataSource::updateFlags() const { UpdateFlags flags = NoUpdateFlags; if ( hasConstantTime() ) { flags |= SourceHasConstantTime; } return flags; } void TimetableDataSource::setUpdateTimer( QTimer *timer ) { // Delete old timer (if any) and replace with the new timer delete m_updateTimer; m_updateTimer = timer; } void TimetableDataSource::stopUpdateTimer() { if ( m_updateTimer ) { m_updateTimer->stop(); } } void TimetableDataSource::setUpdateAdditionalDataDelayTimer( QTimer *timer ) { // Delete old timer (if any) and replace with the new timer delete m_updateAdditionalDataDelayTimer; m_updateAdditionalDataDelayTimer = timer; } void TimetableDataSource::setCleanupTimer( QTimer *timer ) { // Delete old timer (if any) and replace with the new timer delete m_cleanupTimer; m_cleanupTimer = timer; } void TimetableDataSource::cleanup() { // Get a list of hash values for all currently available timetable items QList< uint > itemHashes; const QVariantList items = timetableItems(); const bool isDeparture = timetableItemKey() != QLatin1String("arrivals"); foreach ( const QVariant &item, items ) { itemHashes << hashForDeparture( item.toHash(), isDeparture ); } // Remove cached additional data for no longer present timetable items QHash< uint, TimetableData >::Iterator it = m_additionalData.begin(); while ( it != m_additionalData.end() ) { if ( itemHashes.contains(it.key()) ) { // The associated timetable item is still available ++it; } else { // The cached additional data is for a no longer present timetable item, remove it qDebug() << "Discard old additional data" << it.key(); it = m_additionalData.erase( it ); } } } QSharedPointer< AbstractRequest > TimetableDataSource::request( const QString &sourceName ) const { return m_dataSources[ sourceName ].request; } void ProvidersDataSource::addProvider( const QString &providerId, const ProvidersDataSource::ProviderData &providerData ) { if ( !m_changedProviders.contains(providerId) ) { // Provider was not already marked as changed if ( m_providerData.contains(providerId) ) { // Provider data is already available if ( m_providerData[providerId].data() != providerData.data() ) { // Provider data has changed m_changedProviders << providerId; } } else { // No provider data available m_changedProviders << providerId; } } m_providerData.insert( providerId, providerData ); } void ProvidersDataSource::removeProvider( const QString &providerId ) { m_changedProviders << providerId; m_providerData.remove( providerId ); } QStringList ProvidersDataSource::markUninstalledProviders() { // Get a list of provider IDs for all installed providers const QStringList installedProviderPaths = ServiceProviderGlobal::installedProviders(); QStringList installedProviderIDs; foreach ( const QString &installedProviderPath, installedProviderPaths ) { installedProviderIDs << ServiceProviderGlobal::idFromFileName( installedProviderPath ); } QStringList providerIDs; QHash::ConstIterator it = m_providerData.constBegin(); while ( it != m_providerData.constEnd() ) { if ( !installedProviderIDs.contains(it.key()) ) { // Provider was uninstalled, ie. removed from the installation directory m_changedProviders << it.key(); providerIDs << it.key(); } ++it; } return providerIDs; } QVariantHash ProvidersDataSource::providerData( const QString &providerId ) const { return m_providerData.value( providerId ).data(); } QString ProvidersDataSource::providerState( const QString &providerId ) const { return m_providerData.value( providerId ).stateId; } QVariantHash ProvidersDataSource::providerStateData( const QString &providerId ) const { return m_providerData.value( providerId ).stateData; } void ProvidersDataSource::setProviderState( const QString &providerId, const QString &stateId ) { if ( mayProviderBeNewlyChanged(providerId) ) { // Provider data available and not marked as changed alraedy ProviderData providerData = m_providerData[ providerId ]; if ( providerData.stateId != stateId ) { m_changedProviders << providerId; } } m_providerData[ providerId ].stateId = stateId; } void ProvidersDataSource::setProviderStateData( const QString &providerId, const QVariantHash &stateData ) { if ( mayProviderBeNewlyChanged(providerId) ) { // Provider data available and not marked as changed alraedy ProviderData providerData = m_providerData[ providerId ]; if ( providerData.stateData != stateData ) { m_changedProviders << providerId; } } m_providerData[ providerId ].stateData = stateData; } diff --git a/engine/global.cpp b/engine/global.cpp index 00ab5f3..3efd2e9 100644 --- a/engine/global.cpp +++ b/engine/global.cpp @@ -1,439 +1,439 @@ /* * Copyright 2012 Friedrich Pülz * * 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 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "global.h" // KDE includes -#include +#include // Qt includes #include #include #include Enums::VehicleType Global::vehicleTypeFromString( QString sVehicleType ) { return Enums::stringToVehicleType( sVehicleType.toAscii().data() ); } QString Global::vehicleTypeToString( const Enums::VehicleType& vehicleType, bool plural ) { switch ( vehicleType ) { case Enums::Tram: return plural ? i18nc( "@info/plain", "trams" ) : i18nc( "@info/plain", "tram" ); case Enums::Bus: return plural ? i18nc( "@info/plain", "buses" ) : i18nc( "@info/plain", "bus" ); case Enums::Subway: return plural ? i18nc( "@info/plain", "subways" ) : i18nc( "@info/plain", "subway" ); case Enums::InterurbanTrain: return plural ? i18nc( "@info/plain", "interurban trains" ) : i18nc( "@info/plain", "interurban train" ); case Enums::Metro: return plural ? i18nc( "@info/plain", "metros" ) : i18nc( "@info/plain", "metro" ); case Enums::TrolleyBus: return plural ? i18nc( "@info/plain", "trolley buses" ) : i18nc( "@info/plain", "trolley bus" ); case Enums::RegionalTrain: return plural ? i18nc( "@info/plain", "regional trains" ) : i18nc( "@info/plain", "regional train" ); case Enums::RegionalExpressTrain: return plural ? i18nc( "@info/plain", "regional express trains" ) : i18nc( "@info/plain", "regional express train" ); case Enums::InterregionalTrain: return plural ? i18nc( "@info/plain", "interregional trains" ) : i18nc( "@info/plain", "interregional train" ); case Enums::IntercityTrain: return plural ? i18nc( "@info/plain", "intercity / eurocity trains" ) : i18nc( "@info/plain", "intercity / eurocity train" ); case Enums::HighSpeedTrain: return plural ? i18nc( "@info/plain", "intercity express trains" ) : i18nc( "@info/plain", "intercity express train" ); case Enums::Feet: return i18nc( "@info/plain", "Footway" ); case Enums::Ferry: return plural ? i18nc( "@info/plain", "ferries" ) : i18nc( "@info/plain", "ferry" ); case Enums::Ship: return plural ? i18nc( "@info/plain", "ships" ) : i18nc( "@info/plain", "ship" ); case Enums::Plane: return plural ? i18nc( "@info/plain airplanes", "planes" ) : i18nc( "@info/plain an airplane", "plane" ); case Enums::UnknownVehicleType: default: return i18nc( "@info/plain Unknown type of vehicle", "Unknown" ); } } QString Global::vehicleTypeToIcon( const Enums::VehicleType& vehicleType ) { switch ( vehicleType ) { case Enums::Tram: return "vehicle_type_tram"; case Enums::Bus: return "vehicle_type_bus"; case Enums::Subway: return "vehicle_type_subway"; case Enums::Metro: return "vehicle_type_metro"; case Enums::TrolleyBus: return "vehicle_type_trolleybus"; case Enums::Feet: return "vehicle_type_feet"; case Enums::InterurbanTrain: return "vehicle_type_train_interurban"; case Enums::RegionalTrain: // aIcon not done yet, using this for now case Enums::RegionalExpressTrain: return "vehicle_type_train_regional"; case Enums::InterregionalTrain: return "vehicle_type_train_interregional"; case Enums::IntercityTrain: return "vehicle_type_train_intercity"; case Enums::HighSpeedTrain: return "vehicle_type_train_highspeed"; case Enums::Ferry: case Enums::Ship: return "vehicle_type_ferry"; case Enums::Plane: return "vehicle_type_plane"; case Enums::UnknownVehicleType: default: return "status_unknown"; } } Enums::TimetableInformation Global::timetableInformationFromString( const QString& sTimetableInformation ) { const QString sInfo = sTimetableInformation.toLower(); if ( sInfo == QLatin1String("nothing") ) { return Enums::Nothing; } else if ( sInfo == QLatin1String("departuredatetime") ) { return Enums::DepartureDateTime; } else if ( sInfo == QLatin1String("departuredate") ) { return Enums::DepartureDate; } else if ( sInfo == QLatin1String("departuretime") ) { return Enums::DepartureTime; } else if ( sInfo == QLatin1String("typeofvehicle") ) { return Enums::TypeOfVehicle; } else if ( sInfo == QLatin1String("transportline") ) { return Enums::TransportLine; } else if ( sInfo == QLatin1String("flightnumber") ) { return Enums::FlightNumber; } else if ( sInfo == QLatin1String("target") ) { return Enums::Target; } else if ( sInfo == QLatin1String("targetshortened") ) { return Enums::TargetShortened; } else if ( sInfo == QLatin1String("platform") ) { return Enums::Platform; } else if ( sInfo == QLatin1String("delay") ) { return Enums::Delay; } else if ( sInfo == QLatin1String("delayreason") ) { return Enums::DelayReason; } else if ( sInfo == QLatin1String("journeynews") ) { return Enums::JourneyNews; } else if ( sInfo == QLatin1String("journeynewsother") ) { // DEPRECATED qWarning() << "JourneyNewsOther is deprecated, use JourneyNews instead"; return Enums::JourneyNewsOther; } else if ( sInfo == QLatin1String("journeynewslink") ) { // DEPRECATED qWarning() << "JourneyNewsLink is deprecated, use JourneyNewsUrl instead"; return Enums::JourneyNewsUrl; } else if ( sInfo == QLatin1String("journeynewsurl") ) { return Enums::JourneyNewsUrl; } else if ( sInfo == QLatin1String("status") ) { return Enums::Status; } else if ( sInfo == QLatin1String("routestops") ) { return Enums::RouteStops; } else if ( sInfo == QLatin1String("routestopsshortened") ) { return Enums::RouteStopsShortened; } else if ( sInfo == QLatin1String("routetimes") ) { return Enums::RouteTimes; } else if ( sInfo == QLatin1String("routetimesdeparture") ) { return Enums::RouteTimesDeparture; } else if ( sInfo == QLatin1String("routetimesarrival") ) { return Enums::RouteTimesArrival; } else if ( sInfo == QLatin1String("routeexactstops") ) { return Enums::RouteExactStops; } else if ( sInfo == QLatin1String("routetypesofvehicles") ) { return Enums::RouteTypesOfVehicles; } else if ( sInfo == QLatin1String("routetransportlines") ) { return Enums::RouteTransportLines; } else if ( sInfo == QLatin1String("routeplatformsdeparture") ) { return Enums::RoutePlatformsDeparture; } else if ( sInfo == QLatin1String("routeplatformsarrival") ) { return Enums::RoutePlatformsArrival; } else if ( sInfo == QLatin1String("routetimesdeparturedelay") ) { return Enums::RouteTimesDepartureDelay; } else if ( sInfo == QLatin1String("routetimesarrivaldelay") ) { return Enums::RouteTimesArrivalDelay; } else if ( sInfo == QLatin1String("routenews") ) { return Enums::RouteNews; } else if ( sInfo == QLatin1String("routesubjourneys") ) { return Enums::RouteSubJourneys; } else if ( sInfo == QLatin1String("routedataurl") ) { return Enums::RouteDataUrl; } else if ( sInfo == QLatin1String("operator") ) { return Enums::Operator; } else if ( sInfo == QLatin1String("duration") ) { return Enums::Duration; } else if ( sInfo == QLatin1String("startstopname") ) { return Enums::StartStopName; } else if ( sInfo == QLatin1String("startstopid") ) { return Enums::StartStopID; } else if ( sInfo == QLatin1String("targetstopname") ) { return Enums::TargetStopName; } else if ( sInfo == QLatin1String("targetstopid") ) { return Enums::TargetStopID; } else if ( sInfo == QLatin1String("arrivaldatetime") ) { return Enums::ArrivalDateTime; } else if ( sInfo == QLatin1String("arrivaldate") ) { return Enums::ArrivalDate; } else if ( sInfo == QLatin1String("arrivaltime") ) { return Enums::ArrivalTime; } else if ( sInfo == QLatin1String("changes") ) { return Enums::Changes; } else if ( sInfo == QLatin1String("typesofvehicleinjourney") ) { return Enums::TypesOfVehicleInJourney; } else if ( sInfo == QLatin1String("pricing") ) { return Enums::Pricing; } else if ( sInfo == QLatin1String("isnightline") ) { return Enums::IsNightLine; } else if ( sInfo == QLatin1String("stopname") ) { return Enums::StopName; } else if ( sInfo == QLatin1String("stopid") ) { return Enums::StopID; } else if ( sInfo == QLatin1String("stopweight") ) { return Enums::StopWeight; } else if ( sInfo == QLatin1String("stopcity") ) { return Enums::StopCity; } else if ( sInfo == QLatin1String("stopcountrycode") ) { return Enums::StopCountryCode; } else if ( sInfo == QLatin1String("stoplongitude") ) { return Enums::StopLongitude; } else if ( sInfo == QLatin1String("stoplatitude") ) { return Enums::StopLatitude; } else if ( sInfo == QLatin1String("requestdata") ) { return Enums::RequestData; } else { qDebug() << sTimetableInformation << "is an unknown timetable information value! Assuming value Nothing."; return Enums::Nothing; } } QString Global::timetableInformationToString( Enums::TimetableInformation timetableInformation ) { return Enums::toString( timetableInformation ); } bool Global::checkTimetableInformation( Enums::TimetableInformation info, const QVariant &value ) { if ( !value.isValid() ) { return false; } switch ( info ) { case Enums::DepartureDateTime: case Enums::ArrivalDateTime: return value.toDateTime().isValid(); case Enums::DepartureDate: case Enums::ArrivalDate: return value.toDate().isValid(); case Enums::DepartureTime: case Enums::ArrivalTime: return value.toTime().isValid(); case Enums::TypeOfVehicle: return vehicleTypeFromString( value.toString() ) != Enums::UnknownVehicleType; case Enums::TransportLine: case Enums::Target: case Enums::TargetShortened: case Enums::Platform: case Enums::DelayReason: case Enums::JourneyNews: case Enums::JourneyNewsOther: // DEPRECATED case Enums::JourneyNewsUrl: case Enums::Operator: case Enums::Status: case Enums::StartStopName: case Enums::StartStopID: case Enums::StopCity: case Enums::StopCountryCode: case Enums::TargetStopName: case Enums::TargetStopID: case Enums::Pricing: case Enums::StopName: case Enums::StopID: case Enums::RouteDataUrl: return !value.toString().trimmed().isEmpty(); case Enums::StopLongitude: case Enums::StopLatitude: if ( !value.canConvert(QVariant::Double) ) { return false; } else { bool ok; (void) value.toReal( &ok ); return ok; } case Enums::Delay: return value.canConvert( QVariant::Int ) && value.toInt() >= -1; case Enums::Duration: case Enums::StopWeight: case Enums::Changes: case Enums::RouteExactStops: return value.canConvert( QVariant::Int ) && value.toInt() >= 0; case Enums::TypesOfVehicleInJourney: case Enums::RouteTimes: case Enums::RouteTimesDeparture: case Enums::RouteTimesArrival: case Enums::RouteTypesOfVehicles: case Enums::RouteTimesDepartureDelay: case Enums::RouteTimesArrivalDelay: case Enums::RouteSubJourneys: return !value.toList().isEmpty(); case Enums::IsNightLine: return value.canConvert( QVariant::Bool ); case Enums::RouteStops: case Enums::RouteStopsShortened: case Enums::RouteTransportLines: case Enums::RoutePlatformsDeparture: case Enums::RoutePlatformsArrival: case Enums::RouteNews: return !value.toStringList().isEmpty(); case Enums::RequestData: default: return true; } } QString Global::decodeHtmlEntities( const QString& html ) { if ( html.isEmpty() ) { return html; } QString ret = html; QRegExp rx( "(?:&#)([0-9]+)(?:;)" ); rx.setMinimal( true ); int pos = 0; while ( (pos = rx.indexIn(ret, pos)) != -1 ) { const int charCode = rx.cap( 1 ).toInt(); ret.replace( QString("&#%1;").arg(charCode), QChar(charCode) ); } return ret.replace( QLatin1String(" "), QLatin1String(" ") ) .replace( QLatin1String("&"), QLatin1String("&") ) .replace( QLatin1String("<"), QLatin1String("<") ) .replace( QLatin1String(">"), QLatin1String(">") ) .replace( QLatin1String("ß"), QLatin1String("ß") ) .replace( QLatin1String("ä"), QLatin1String("ä") ) .replace( QLatin1String("Ä"), QLatin1String("Ä") ) .replace( QLatin1String("ö"), QLatin1String("ö") ) .replace( QLatin1String("Ö"), QLatin1String("Ö") ) .replace( QLatin1String("ü"), QLatin1String("ü") ) .replace( QLatin1String("Ü"), QLatin1String("Ü") ); } QString Global::encodeHtmlEntities( const QString &html, HtmlEntityEncodeFlags flags ) { if ( html.isEmpty() ) { return html; } QString ret = html; if ( flags.testFlag(EncodeLessThan) ) { ret.replace( QLatin1String("<"), QLatin1String("<") ); } if ( flags.testFlag(EncodeGreaterThan) ) { ret.replace( QLatin1String(">"), QLatin1String(">") ); } if ( flags.testFlag(EncodeAmpersand) ) { ret.replace( QLatin1String("&"), QLatin1String("&") ); } if ( flags.testFlag(EncodeUmlauts) ) { ret.replace( QLatin1String("ß"), QLatin1String("ß") ) .replace( QLatin1String("ä"), QLatin1String("ä") ) .replace( QLatin1String("Ä"), QLatin1String("Ä") ) .replace( QLatin1String("ö"), QLatin1String("ö") ) .replace( QLatin1String("Ö"), QLatin1String("Ö") ) .replace( QLatin1String("ü"), QLatin1String("ü") ) .replace( QLatin1String("Ü"), QLatin1String("Ü") ); } if ( flags.testFlag(EncodeSpace) ) { ret.replace( QLatin1String(" "), QLatin1String(" ") ); } return ret; } QString Global::decodeHtml( const QByteArray& document, const QByteArray& fallbackCharset ) { // Get charset of the received document and convert it to a unicode QString // First parse the charset with a regexp to get a fallback charset // if QTextCodec::codecForHtml doesn't find the charset QTextCodec *textCodec = QTextCodec::codecForHtml( document, 0 ); if ( textCodec ) { return textCodec->toUnicode( document ); } else { if ( !fallbackCharset.isEmpty() ) { textCodec = QTextCodec::codecForName( fallbackCharset ); if ( !textCodec ) { qDebug() << "Fallback charset" << fallbackCharset << "not found! Using utf8 now."; textCodec = QTextCodec::codecForName( "UTF-8" ); } } else { QString sDocument = QString( document ); QRegExp rxCharset( "(?:.*]*>)", Qt::CaseInsensitive ); rxCharset.setMinimal( true ); if ( rxCharset.indexIn(sDocument) != -1 ) { textCodec = QTextCodec::codecForName( rxCharset.cap(1).trimmed().toUtf8() ); } else { qDebug() << "No fallback charset specified and manual codec search failed, using utf8"; textCodec = QTextCodec::codecForName( "UTF-8" ); } } return textCodec ? textCodec->toUnicode(document) : QString::fromUtf8(document); } } QString Global::decode( const QByteArray &document, const QByteArray &charset ) { if ( !charset.isEmpty() ) { QTextCodec *textCodec = QTextCodec::codecForName( charset ); if ( !textCodec ) { qDebug() << "Charset" << charset << "not found! Using utf8 now."; textCodec = QTextCodec::codecForName( "UTF-8" ); } return textCodec->toUnicode( document ); } else { return QString::fromUtf8( document ); } } diff --git a/engine/publictransportdataengine.cpp b/engine/publictransportdataengine.cpp index 7af437e..748b47d 100644 --- a/engine/publictransportdataengine.cpp +++ b/engine/publictransportdataengine.cpp @@ -1,2744 +1,2747 @@ /* * Copyright 2013 Friedrich Pülz * * 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 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "publictransportdataengine.h" // Own includes #include "serviceprovider.h" #include "serviceproviderdata.h" #include "serviceprovidertestdata.h" #include "serviceproviderglobal.h" #include "global.h" #include "request.h" #include "timetableservice.h" #include "datasource.h" #ifdef BUILD_PROVIDER_TYPE_SCRIPT #include "script/serviceproviderscript.h" #endif #ifdef BUILD_PROVIDER_TYPE_GTFS #include "gtfs/serviceprovidergtfs.h" #include "gtfs/gtfsservice.h" #endif // KDE/Plasma includes #include -#include +#include +#include +#include +#include // Qt includes #include #include #include #include -#include +#include const int PublicTransportEngine::DEFAULT_TIME_OFFSET = 0; const int PublicTransportEngine::PROVIDER_CLEANUP_TIMEOUT = 10000; // 10 seconds Plasma::Service* PublicTransportEngine::serviceForSource( const QString &name ) { #ifdef BUILD_PROVIDER_TYPE_GTFS // Return the GTFS service for "GTFS" or "gtfs" names if ( name.toLower() == QLatin1String("gtfs") ) { GtfsService *service = new GtfsService( name, this ); service->setDestination( name ); connect( service, SIGNAL(finished(Plasma::ServiceJob*)), this, SLOT(gtfsServiceJobFinished(Plasma::ServiceJob*)) ); return service; } #endif // If the name of a data requesting source is given, return the timetable service const SourceType type = sourceTypeFromName( name ); if ( isDataRequestingSourceType(type) ) { const QString nonAmbiguousName = disambiguateSourceName( name ); if ( m_dataSources.contains(nonAmbiguousName) ) { // Data source exists TimetableService *service = new TimetableService( this, name, this ); service->setDestination( name ); return service; } } // No service for the given name found return 0; } void PublicTransportEngine::publishData( DataSource *dataSource, const QString &newlyRequestedProviderId ) { Q_ASSERT( dataSource ); TimetableDataSource *timetableDataSource = dynamic_cast< TimetableDataSource* >( dataSource ); if ( timetableDataSource ) { foreach ( const QString &usingDataSource, timetableDataSource->usingDataSources() ) { setData( usingDataSource, timetableDataSource->data() ); } return; } setData( dataSource->name(), dataSource->data() ); ProvidersDataSource *providersSource = dynamic_cast< ProvidersDataSource* >( dataSource ); if ( providersSource ) { // Update "ServiceProvider " sources, which are also contained in the // DataSource object for the "ServiceProviders" data source. // Check which data sources of changed providers of that type are connected QStringList sources = Plasma::DataEngine::sources(); const QStringList changedProviders = providersSource->takeChangedProviders(); foreach ( const QString changedProviderId, changedProviders ) { const QString providerSource = sourceTypeKeyword(ServiceProviderSource) + ' ' + changedProviderId; if ( sources.contains(providerSource) ) { // Found a data source for the current changed provider, clear it and set new data removeAllData( providerSource ); setData( providerSource, providersSource->providerData(changedProviderId) ); } } if ( !newlyRequestedProviderId.isEmpty() ) { const QString providerSource = sourceTypeKeyword(ServiceProviderSource) + ' ' + newlyRequestedProviderId; removeAllData( providerSource ); setData( providerSource, providersSource->providerData(newlyRequestedProviderId) ); } } } ProvidersDataSource *PublicTransportEngine::providersDataSource() const { const QString name = sourceTypeKeyword( ServiceProvidersSource ); ProvidersDataSource *dataSource = dynamic_cast< ProvidersDataSource* >( m_dataSources[name] ); Q_ASSERT_X( dataSource, "PublicTransportEngine::providersDataSource()", "ProvidersDataSource is not available in m_dataSources!" ); return dataSource; } #ifdef BUILD_PROVIDER_TYPE_GTFS bool PublicTransportEngine::tryToStartGtfsFeedImportJob( Plasma::ServiceJob *job ) { Q_ASSERT( job ); updateServiceProviderSource(); ProvidersDataSource *dataSource = providersDataSource(); Q_ASSERT( dataSource ); const QString providerId = job->property( "serviceProviderId" ).toString(); if ( dataSource->providerState(providerId) == QLatin1String("importing_gtfs_feed") ) { // GTFS feed already gets imported, cannot start another import job return false; } // Update provider state in service provider data source(s) QVariantHash stateData = dataSource->providerStateData( providerId ); const QString databasePath = GtfsDatabase::databasePath( providerId ); stateData[ "gtfsDatabasePath" ] = databasePath; stateData[ "gtfsDatabaseSize" ] = 0; stateData[ "progress" ] = 0; QString state; if ( job->operationName() == QLatin1String("importGtfsFeed") ) { state = "importing_gtfs_feed"; } else if ( job->operationName() == QLatin1String("deleteGtfsDatabase") ) { state = "gtfs_feed_import_pending"; } else { // The operations "updateGtfsDatabase" and "updateGtfsFeedInfo" can run in the background state = "ready"; } dataSource->setProviderState( providerId, state, stateData ); // Store the state in the cache QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); KConfigGroup group = cache->group( providerId ); group.writeEntry( "state", state ); KConfigGroup stateGroup = group.group( "stateData" ); for ( QVariantHash::ConstIterator it = stateData.constBegin(); it != stateData.constEnd(); ++it ) { if ( isStateDataCached(it.key()) ) { stateGroup.writeEntry( it.key(), it.value() ); } } publishData( dataSource ); // Connect to messages of the job to update the provider state connect( job, SIGNAL(infoMessage(KJob*,QString,QString)), this, SLOT(gtfsImportJobInfoMessage(KJob*,QString,QString)) ); connect( job, SIGNAL(percent(KJob*,ulong)), this, SLOT(gtfsImportJobPercent(KJob*,ulong)) ); // The import job can be started return true; } void PublicTransportEngine::gtfsServiceJobFinished( Plasma::ServiceJob *job ) { // Disconnect messages of the job disconnect( job, SIGNAL(infoMessage(KJob*,QString,QString)), this, SLOT(gtfsImportJobInfoMessage(KJob*,QString,QString)) ); disconnect( job, SIGNAL(percent(KJob*,ulong)), this, SLOT(gtfsImportJobPercent(KJob*,ulong)) ); // Check that the job was not canceled because another database job was already running const bool canAccessGtfsDatabase = job->property("canAccessGtfsDatabase").toBool(); const bool isAccessingGtfsDatabase = job->property("isAccessingGtfsDatabase").toBool(); const QString providerId = job->property( "serviceProviderId" ).toString(); if ( (!canAccessGtfsDatabase && isAccessingGtfsDatabase) || providerId.isEmpty() ) { // Invalid job or cancelled, because another import job is already running // for the provider return; } // Reset state in "ServiceProviders", "ServiceProvider " data sources // Do not read and give the current feed URL for the provider to updateProviderState(), // because the feed URL should not have changed since the beginning of the feed import QVariantHash stateData; const QString state = updateProviderState( providerId, &stateData, "GTFS", QString(), false ); ProvidersDataSource *dataSource = providersDataSource(); dataSource->setProviderState( providerId, state, stateData ); publishData( dataSource ); } void PublicTransportEngine::gtfsImportJobInfoMessage( KJob *job, const QString &plain, const QString &rich ) { // Update "ServiceProviders", "ServiceProvider " data sources const QString providerId = job->property( "serviceProviderId" ).toString(); ProvidersDataSource *dataSource = providersDataSource(); QVariantHash stateData = dataSource->providerStateData( providerId ); stateData[ "statusMessage" ] = plain; if ( rich.isEmpty() ) { stateData.remove( "statusMessageRich" ); } else { stateData[ "statusMessageRich" ] = rich; } dataSource->setProviderStateData( providerId, stateData ); publishData( dataSource ); } void PublicTransportEngine::gtfsImportJobPercent( KJob *job, ulong percent ) { // Update "ServiceProviders", "ServiceProvider " data sources const QString providerId = job->property( "serviceProviderId" ).toString(); ProvidersDataSource *dataSource = providersDataSource(); QVariantHash stateData = dataSource->providerStateData( providerId ); stateData[ "progress" ] = int( percent ); dataSource->setProviderStateData( providerId, stateData ); publishData( dataSource ); } #endif // BUILD_PROVIDER_TYPE_GTFS PublicTransportEngine::PublicTransportEngine( QObject* parent, const QVariantList& args ) : Plasma::DataEngine( parent, args ), m_fileSystemWatcher(0), m_providerUpdateDelayTimer(0), m_cleanupTimer(0) { // We ignore any arguments - data engines do not have much use for them Q_UNUSED( args ) // This prevents applets from setting an unnecessarily high update interval // and using too much CPU. // 60 seconds should be enough, departure / arrival times have minute precision (except for GTFS). setMinimumPollingInterval( 60000 ); // Cleanup the cache from obsolete data for providers that were uninstalled // while the engine was not running ServiceProviderGlobal::cleanupCache(); // Get notified when data sources are no longer used connect( this, SIGNAL(sourceRemoved(QString)), this, SLOT(slotSourceRemoved(QString)) ); // Get notified when the network state changes to update data sources, // which update timers were missed because of missing network connection QDBusConnection::sessionBus().connect( "org.kde.kded", "/modules/networkstatus", "org.kde.Solid.Networking.Client", "statusChanged", this, SLOT(networkStateChanged(uint)) ); // Create "ServiceProviders" and "ServiceProvider [providerId]" data source object const QString name = sourceTypeKeyword( ServiceProvidersSource ); m_dataSources.insert( name, new ProvidersDataSource(name) ); updateServiceProviderSource(); // Ensure the local provider installation directory exists in the users HOME and will not // get removed, by creating a small file in it so that the directory is never empty. // If an installation directory gets removed, the file system watcher would need to watch the // parent directory instead to get notified when it gets created again. const QString installationSubDirectory = ServiceProviderGlobal::installationSubDirectory(); const QString saveDir = KGlobal::dirs()->saveLocation( "data", installationSubDirectory ); QFile saveDirKeeper( saveDir + "Do not remove this directory" ); saveDirKeeper.open( QIODevice::WriteOnly ); saveDirKeeper.write( "If this directory gets removed, PublicTransport will not get notified " "about installed provider files in that directory." ); saveDirKeeper.close(); // Create a file system watcher for the provider plugin installation directories // to get notified about new/modified/removed providers const QStringList directories = KGlobal::dirs()->findDirs( "data", installationSubDirectory ); m_fileSystemWatcher = new QFileSystemWatcher( directories ); connect( m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(serviceProviderDirChanged(QString)) ); } PublicTransportEngine::~PublicTransportEngine() { if ( !m_runningSources.isEmpty() || !m_providers.isEmpty() ) { qDebug() << m_runningSources.count() << "data sources are still being updated," << m_providers.count() << "providers used, abort and delete all providers"; QStringList providerIds = m_providers.keys(); foreach ( const QString &providerId, providerIds ) { deleteProvider( providerId, false ); } } delete m_fileSystemWatcher; delete m_providerUpdateDelayTimer; qDeleteAll( m_dataSources ); m_dataSources.clear(); // Providers cached in m_cachedProviders get deleted automatically (QSharedPointer) // and need no special handling, since they are not used currently } QStringList PublicTransportEngine::sources() const { QStringList sources = Plasma::DataEngine::sources(); sources << sourceTypeKeyword(LocationsSource) << sourceTypeKeyword(ServiceProvidersSource) << sourceTypeKeyword(ErroneousServiceProvidersSource) << sourceTypeKeyword(VehicleTypesSource); sources.removeDuplicates(); return sources; } void PublicTransportEngine::networkStateChanged( uint state ) { if ( state != 4 ) { // 4 => Connected, see Solid::Networking::Status return; } // Network is connected again, check for missed update timers in connected data sources for ( QHash::ConstIterator it = m_dataSources.constBegin(); it != m_dataSources.constEnd(); ++it ) { // Check if the current data source is a timetable data source, without running update TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( *it ); if ( !dataSource || m_runningSources.contains(it.key()) ) { continue; } // Check if the next automatic update time was missed (stored in the data source) const QDateTime nextAutomaticUpdate = dataSource->value("nextAutomaticUpdate").toDateTime(); if ( nextAutomaticUpdate <= QDateTime::currentDateTime() ) { // Found a timetable data source that should have been updated already and // is not currently being updated (not in m_runningSources). // This happens if there was no network connection while an automatic update // was triggered. If the system is suspended the QTimer's for automatic updates // are not triggered at all. // Do now manually request updates for all connected sources, ie. do what should // have been done in updateTimeout(). foreach ( const QString &sourceName, dataSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } } } } bool PublicTransportEngine::isProviderUsed( const QString &providerId ) { // Check if a request is currently running for the provider foreach ( const QString &runningSource, m_runningSources ) { if ( runningSource.contains(providerId) ) { return true; } } // Check if a data source is connected that uses the provider for ( QHash< QString, DataSource* >::ConstIterator it = m_dataSources.constBegin(); it != m_dataSources.constEnd(); ++it ) { Q_ASSERT( *it ); TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( *it ); if ( dataSource && dataSource->providerId() == providerId ) { return true; } } // The provider is not used any longer by the engine return false; } void PublicTransportEngine::slotSourceRemoved( const QString &sourceName ) { const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( m_dataSources.contains(nonAmbiguousName) ) { // If this is a timetable data source, which might be associated with multiple // ambiguous source names, check if this data source is still connected under other names TimetableDataSource *timetableDataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( timetableDataSource ) { timetableDataSource->removeUsingDataSource( sourceName ); if ( timetableDataSource->usageCount() > 0 ) { // The TimetableDataSource object is still used by other connected data sources return; } } if ( sourceName == sourceTypeKeyword(ServiceProvidersSource) ) { // Do not remove ServiceProviders data source return; } // If a provider was used by the source, // remove the provider if it is not used by another source const DataSource *dataSource = m_dataSources.take( nonAmbiguousName ); Q_ASSERT( dataSource ); if ( dataSource->data().contains("serviceProvider") ) { const QString providerId = dataSource->value("serviceProvider").toString(); if ( !providerId.isEmpty() && !isProviderUsed(providerId) ) { // Move provider from the list of used providers to the list of cached providers m_cachedProviders.insert( providerId, m_providers.take(providerId) ); } } // Start the cleanup timer, will delete cached providers after a timeout startCleanupLater(); // The data source is no longer used, delete it delete dataSource; } else if ( nonAmbiguousName.startsWith(sourceTypeKeyword(ServiceProviderSource)) ) { // A "ServiceProvider xx_xx" data source was removed, data for these sources // is stored in the "ServiceProviders" data source object in m_dataSources. // Remove not installed providers also from the "ServiceProviders" data source. const QString providerId = nonAmbiguousName.mid( QString(sourceTypeKeyword(ServiceProviderSource)).length() + 1 ); if ( !providerId.isEmpty() && !isProviderUsed(providerId) ) { // Test if the provider is installed const QStringList providerPaths = ServiceProviderGlobal::installedProviders(); bool isInstalled = false; foreach ( const QString &providerPath, providerPaths ) { const QString &installedProviderId = ServiceProviderGlobal::idFromFileName( providerPath ); if ( providerId == installedProviderId ) { isInstalled = true; break; } } if ( !isInstalled ) { qDebug() << "Remove provider" << providerId; // Provider is not installed, remove it from the "ServiceProviders" data source providersDataSource()->removeProvider( providerId ); removeData( sourceTypeKeyword(ServiceProvidersSource), providerId ); } } } } void PublicTransportEngine::startCleanupLater() { // Create the timer if it is not currently running if ( !m_cleanupTimer ) { m_cleanupTimer = new QTimer( this ); m_cleanupTimer->setInterval( PROVIDER_CLEANUP_TIMEOUT ); connect( m_cleanupTimer, SIGNAL(timeout()), this, SLOT(cleanup()) ); } // (Re)start the timer m_cleanupTimer->start(); } void PublicTransportEngine::cleanup() { // Delete the timer delete m_cleanupTimer; m_cleanupTimer = 0; // Remove all shared pointers of unused cached providers, // ie. delete the provider objects m_cachedProviders.clear(); } QVariantHash PublicTransportEngine::serviceProviderData( const ServiceProvider *provider ) { Q_ASSERT( provider ); return serviceProviderData( *(provider->data()), provider ); } QVariantHash PublicTransportEngine::serviceProviderData( const ServiceProviderData &data, const ServiceProvider *provider ) { QVariantHash dataServiceProvider; dataServiceProvider.insert( "id", data.id() ); dataServiceProvider.insert( "fileName", data.fileName() ); dataServiceProvider.insert( "type", ServiceProviderGlobal::typeName(data.type()) ); #ifdef BUILD_PROVIDER_TYPE_GTFS if ( data.type() == Enums::GtfsProvider ) { dataServiceProvider.insert( "feedUrl", data.feedUrl() ); } #endif #ifdef BUILD_PROVIDER_TYPE_SCRIPT if ( data.type() == Enums::ScriptedProvider ) { dataServiceProvider.insert( "scriptFileName", data.scriptFileName() ); } #endif dataServiceProvider.insert( "name", data.name() ); dataServiceProvider.insert( "url", data.url() ); dataServiceProvider.insert( "shortUrl", data.shortUrl() ); dataServiceProvider.insert( "country", data.country() ); dataServiceProvider.insert( "cities", data.cities() ); dataServiceProvider.insert( "credit", data.credit() ); dataServiceProvider.insert( "useSeparateCityValue", data.useSeparateCityValue() ); dataServiceProvider.insert( "onlyUseCitiesInList", data.onlyUseCitiesInList() ); dataServiceProvider.insert( "author", data.author() ); dataServiceProvider.insert( "shortAuthor", data.shortAuthor() ); dataServiceProvider.insert( "email", data.email() ); dataServiceProvider.insert( "description", data.description() ); dataServiceProvider.insert( "version", data.version() ); QStringList changelog; foreach ( const ChangelogEntry &entry, data.changelog() ) { changelog << QString( "%2 (%1): %3" ).arg( entry.version ).arg( entry.author ).arg( entry.description ); } dataServiceProvider.insert( "changelog", changelog ); // To get the list of features, ServiceProviderData is not enough // A given ServiceProvider or cached data gets used if available. Otherwise the ServiceProvider // gets created just to get the list of features const QSharedPointer cache = ServiceProviderGlobal::cache(); KConfigGroup providerGroup = cache->group( data.id() ); if ( provider ) { // Write features to the return value const QList< Enums::ProviderFeature > features = provider->features(); const QStringList featureStrings = ServiceProviderGlobal::featureStrings( features ); dataServiceProvider.insert( "features", featureStrings ); dataServiceProvider.insert( "featureNames", ServiceProviderGlobal::featureNames(features) ); // Make sure, features have been written to the cache providerGroup.writeEntry( "features", featureStrings ); } else { // Check stored feature strings and re-read features if an invalid string was found bool ok; QStringList featureStrings = providerGroup.readEntry("features", QStringList()); const bool featureListIsEmpty = featureStrings.removeOne("(none)"); QList< Enums::ProviderFeature > features = ServiceProviderGlobal::featuresFromFeatureStrings( featureStrings, &ok ); if ( (featureListIsEmpty || !featureStrings.isEmpty()) && ok ) { // Feature list could be read from cache dataServiceProvider.insert( "features", featureStrings ); dataServiceProvider.insert( "featureNames", ServiceProviderGlobal::featureNames(features) ); } else { qDebug() << "No cached feature data was found for provider" << data.id(); // No cached feature data was found for the provider, // create the provider to get the feature list and store it in the cache bool newlyCreated; ProviderPointer _provider = providerFromId( data.id(), &newlyCreated ); features = _provider->features(); featureStrings = ServiceProviderGlobal::featureStrings( features ); const QStringList featuresNames = ServiceProviderGlobal::featureNames( features ); // Remove provider from the list again to delete it if ( newlyCreated ) { m_providers.remove( data.id() ); } // If no features are supported write "(none)" to the cache // to indicate that features have been written to the cache if ( featureStrings.isEmpty() ) { featureStrings.append( "(none)" ); } // Write features to the return value dataServiceProvider.insert( "features", featureStrings ); dataServiceProvider.insert( "featureNames", featuresNames ); // Write features to cache providerGroup.writeEntry( "features", featureStrings ); } } return dataServiceProvider; } QVariantHash PublicTransportEngine::locations() { QVariantHash ret; const QStringList providers = ServiceProviderGlobal::installedProviders(); // Update ServiceProviders source to fill m_erroneousProviders updateServiceProviderSource(); foreach( const QString &provider, providers ) { if ( QFileInfo(provider).isSymLink() ) { // Service provider XML file is a symlink for a default service provider, skip it continue; } const QString providerFileName = QFileInfo( provider ).fileName(); const QString providerId = ServiceProviderGlobal::idFromFileName( providerFileName ); if ( m_erroneousProviders.contains(providerId) ) { // Service provider is erroneous continue; } const int pos = providerFileName.indexOf('_'); if ( pos > 0 ) { // Found an underscore (not the first character) // Cut location code from the service providers XML filename const QString location = providerFileName.mid( 0, pos ).toLower(); if ( !ret.contains(location) ) { // Location is not already added to [ret] // Get the filename of the default provider for the current location const QString defaultProviderFileName = ServiceProviderGlobal::defaultProviderForLocation( location ); // Extract service provider ID from the filename const QString defaultProviderId = ServiceProviderGlobal::idFromFileName( defaultProviderFileName ); // Store location values in a hash and insert it into [ret] QVariantHash locationHash; locationHash.insert( "name", location ); if ( location == "international" ) { locationHash.insert( "description", i18n("International providers. " "There is one for getting flight departures/arrivals.") ); } else { locationHash.insert( "description", i18n("Service providers for %1.", KGlobal::locale()->countryCodeToName(location)) ); } locationHash.insert( "defaultProvider", defaultProviderId ); ret.insert( location, locationHash ); } } } return ret; } PublicTransportEngine::ProviderPointer PublicTransportEngine::providerFromId( const QString &id, bool *newlyCreated ) { if ( m_providers.contains(id) ) { // The provider was already created and is currently used by the engine if ( newlyCreated ) { *newlyCreated = false; } return m_providers[ id ]; } else if ( m_cachedProviders.contains(id) ) { // The provider was already created, is now unused by the engine, // but is still cached (cleanup timeout not reached yet) if ( newlyCreated ) { *newlyCreated = false; } // Move provider back from the list of cached providers to the list of used providers const ProviderPointer provider = m_cachedProviders.take( id ); m_providers.insert( id, provider ); return provider; } else { // Provider not currently used or cached if ( newlyCreated ) { *newlyCreated = true; } // Try to create the provider ServiceProvider *provider = createProvider( id, this ); if ( !provider ) { // Return an invalid ProviderPointer, when the provider could not be created return ProviderPointer::create(); } // Check the state of the provider, it needs to be "ready" ProvidersDataSource *dataSource = providersDataSource(); if ( dataSource ) { const QString state = dataSource->providerState( id ); if ( state != QLatin1String("ready") ) { qWarning() << "Provider" << id << "is not ready, state is" << state; return ProviderPointer::create(); } } // Connect provider, when it was created successfully connect( provider, SIGNAL(departuresReceived(ServiceProvider*,QUrl,DepartureInfoList,GlobalTimetableInfo,DepartureRequest)), this, SLOT(departuresReceived(ServiceProvider*,QUrl,DepartureInfoList,GlobalTimetableInfo,DepartureRequest)) ); connect( provider, SIGNAL(arrivalsReceived(ServiceProvider*,QUrl,ArrivalInfoList,GlobalTimetableInfo,ArrivalRequest)), this, SLOT(arrivalsReceived(ServiceProvider*,QUrl,ArrivalInfoList,GlobalTimetableInfo,ArrivalRequest)) ); connect( provider, SIGNAL(journeysReceived(ServiceProvider*,QUrl,JourneyInfoList,GlobalTimetableInfo,JourneyRequest)), this, SLOT(journeysReceived(ServiceProvider*,QUrl,JourneyInfoList,GlobalTimetableInfo,JourneyRequest)) ); connect( provider, SIGNAL(stopsReceived(ServiceProvider*,QUrl,StopInfoList,StopSuggestionRequest)), this, SLOT(stopsReceived(ServiceProvider*,QUrl,StopInfoList,StopSuggestionRequest)) ); connect( provider, SIGNAL(additionalDataReceived(ServiceProvider*,QUrl,TimetableData,AdditionalDataRequest)), this, SLOT(additionalDataReceived(ServiceProvider*,QUrl,TimetableData,AdditionalDataRequest)) ); connect( provider, SIGNAL(requestFailed(ServiceProvider*,ErrorCode,QString,QUrl,const AbstractRequest*)), this, SLOT(requestFailed(ServiceProvider*,ErrorCode,QString,QUrl,const AbstractRequest*)) ); // Create a ProviderPointer for the created provider and // add it to the list of currently used providers const ProviderPointer pointer( provider ); m_providers.insert( id, pointer ); return pointer; } } bool PublicTransportEngine::updateServiceProviderForCountrySource( const SourceRequestData &data ) { QString providerId; if ( data.defaultParameter.contains('_') ) { // Seems that a service provider ID is given providerId = data.defaultParameter; } else { // Assume a country code in name if ( !updateServiceProviderSource() || !updateLocationSource() ) { return false; } // The defaultParameter stored in data is a location code // (ie. "international" or a two letter country code) QVariantHash locations = m_dataSources[ sourceTypeKeyword(LocationsSource) ]->data(); QVariantHash locationCountry = locations[ data.defaultParameter.toLower() ].toHash(); QString defaultProvider = locationCountry[ "defaultProvider" ].toString(); if ( defaultProvider.isEmpty() ) { // No provider for the location found return false; } providerId = defaultProvider; } updateProviderData( providerId ); publishData( providersDataSource(), providerId ); return true; } bool PublicTransportEngine::updateProviderData( const QString &providerId, const QSharedPointer &cache ) { QVariantHash providerData; QString errorMessage; ProvidersDataSource *providersSource = providersDataSource(); // Test if the provider is valid if ( testServiceProvider(providerId, &providerData, &errorMessage, cache) ) { QVariantHash stateData; const QString state = updateProviderState( providerId, &stateData, providerData["type"].toString(), providerData.value("feedUrl").toString() ); providerData["error"] = false; providersSource->addProvider( providerId, ProvidersDataSource::ProviderData(providerData, state, stateData) ); return true; } else { // Invalid provider providerData["id"] = providerId; providerData["error"] = true; providerData["errorMessage"] = errorMessage; // Prepare state data with the error message and a boolean whether or not the provider // is installed or could not be found (only possible if the provider data source was // manually requested) QVariantHash stateData; stateData["statusMessage"] = errorMessage; stateData["isInstalled"] = ServiceProviderGlobal::isProviderInstalled( providerId ); providersSource->addProvider( providerId, ProvidersDataSource::ProviderData(providerData, "error", stateData) ); return false; } } bool PublicTransportEngine::updateServiceProviderSource() { const QString name = sourceTypeKeyword( ServiceProvidersSource ); ProvidersDataSource *providersSource = providersDataSource(); if ( providersSource->isDirty() ) { const QStringList providers = ServiceProviderGlobal::installedProviders(); if ( providers.isEmpty() ) { qWarning() << "Could not find any service provider plugins"; } else { QStringList loadedProviders; m_erroneousProviders.clear(); QSharedPointer cache = ServiceProviderGlobal::cache(); foreach( const QString &provider, providers ) { const QString providerId = ServiceProviderGlobal::idFromFileName( QUrl(provider).fileName() ); if ( updateProviderData(providerId, cache) ) { loadedProviders << providerId; } } // Print information about loaded/erroneous providers qDebug() << "Loaded" << loadedProviders.count() << "service providers"; if ( !m_erroneousProviders.isEmpty() ) { qWarning() << "Erroneous service provider plugins, that could not be loaded:" << m_erroneousProviders; } } // Mark and update all providers that are no longer installed QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); const QStringList uninstalledProviderIDs = providersSource->markUninstalledProviders(); foreach ( const QString &uninstalledProviderID, uninstalledProviderIDs ) { // Clear all values stored in the cache for the provider ServiceProviderGlobal::clearCache( uninstalledProviderID, cache ); // Delete the provider and update it's state and other data deleteProvider( uninstalledProviderID ); updateProviderData( uninstalledProviderID, cache ); } // Insert the data source m_dataSources.insert( name, providersSource ); } // Remove all old data, some service providers may have been updated and are now erroneous removeAllData( name ); publishData( providersSource ); return true; } QString PublicTransportEngine::updateProviderState( const QString &providerId, QVariantHash *stateData, const QString &providerType, const QString &feedUrl, bool readFromCache ) { Q_ASSERT( stateData ); QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); KConfigGroup group = cache->group( providerId ); const QString cachedState = readFromCache ? group.readEntry("state", QString()) : QString(); #ifdef BUILD_PROVIDER_TYPE_GTFS // Currently type is only used for GTFS const Enums::ServiceProviderType type = ServiceProviderGlobal::typeFromString( providerType ); #endif // BUILD_PROVIDER_TYPE_GTFS // Test if there is an error if ( m_erroneousProviders.contains(providerId) ) { stateData->insert( "statusMessage", m_erroneousProviders[providerId].toString() ); return "error"; } if ( !cachedState.isEmpty() ) { // State is stored in the cache, // also read state data from cache KConfigGroup stateGroup = group.group( "stateData" ); foreach ( const QString &key, stateGroup.keyList() ) { stateData->insert( key, stateGroup.readEntry(key) ); } #ifdef BUILD_PROVIDER_TYPE_GTFS if ( type == Enums::GtfsProvider ) { // Update state and add dynamic state data const QString state = ServiceProviderGtfs::updateGtfsDatabaseState( providerId, feedUrl, cache, stateData ); if ( state != QLatin1String("ready") ) { // The database is invalid/deleted, but the cache says the import was finished deleteProvider( providerId ); } return state; } else #endif // BUILD_PROVIDER_TYPE_GTFS { if ( cachedState != QLatin1String("ready") ) { // Provider not ready, cannot use it deleteProvider( providerId ); } // State of non-GTFS providers does not need more tests, // if there is an error only the fields "error" and "errorMessage" are available // in the data source, no fields "state" or "stateData" return cachedState; } } // !state.isEmpty() // State is not stored in the cache or is out of date QString state = "ready"; #ifdef BUILD_PROVIDER_TYPE_GTFS if ( type == Enums::GtfsProvider ) { state = ServiceProviderGtfs::updateGtfsDatabaseState( providerId, feedUrl, cache, stateData ); } #endif // BUILD_PROVIDER_TYPE_GTFS // Ensure a status message is given (or at least warn if not) if ( stateData->value("statusMessage").toString().isEmpty() ) { if ( state == QLatin1String("ready") ) { stateData->insert( "statusMessage", i18nc("@info/plain", "The provider is ready to use") ); } else { qWarning() << "Missing status message explaining why the provider" << providerId << "is not ready"; } } // Store the state in the cache group.writeEntry( "state", state ); // Write state data to the cache KConfigGroup stateGroup = group.group( "stateData" ); for ( QVariantHash::ConstIterator it = stateData->constBegin(); it != stateData->constEnd(); ++it ) { if ( isStateDataCached(it.key()) ) { stateGroup.writeEntry( it.key(), it.value() ); } } return state; } bool PublicTransportEngine::isStateDataCached( const QString &stateDataKey ) { return stateDataKey != QLatin1String("progress") && stateDataKey != QLatin1String("gtfsDatabasePath") && stateDataKey != QLatin1String("gtfsDatabaseSize") && stateDataKey != QLatin1String("gtfsDatabaseModifiedTime") && stateDataKey != QLatin1String("gtfsFeedImported") && stateDataKey != QLatin1String("gtfsFeedSize") && stateDataKey != QLatin1String("gtfsFeedModifiedTime"); } bool PublicTransportEngine::testServiceProvider( const QString &providerId, QVariantHash *providerData, QString *errorMessage, const QSharedPointer &_cache ) { const bool providerUsed = m_providers.contains( providerId ); const bool providerCached = m_cachedProviders.contains( providerId ); if ( providerUsed || providerCached ) { // The provider is cached in the engine, ie. it is valid, // use it's ServiceProviderData object const ProviderPointer provider = providerUsed ? m_providers[providerId] : m_cachedProviders[providerId]; *providerData = serviceProviderData( provider.data() ); errorMessage->clear(); return true; } // Read cached data for the provider QSharedPointer cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; ServiceProviderTestData testData = ServiceProviderTestData::read( providerId, cache ); // TODO Needs to be done for each provider sub class here foreach ( Enums::ServiceProviderType type, ServiceProviderGlobal::availableProviderTypes() ) { switch ( type ) { #ifdef BUILD_PROVIDER_TYPE_SCRIPT case Enums::ScriptedProvider: if ( !ServiceProviderScript::isTestResultUnchanged(providerId, cache) ) { qDebug() << "Script changed" << providerId; testData.setSubTypeTestStatus( ServiceProviderTestData::Pending ); testData.write( providerId, cache ); } break; #endif case Enums::GtfsProvider: break; case Enums::InvalidProvider: default: qWarning() << "Provider type unknown" << type; break; } } // Check the cache if the provider plugin .pts file can be read if ( testData.xmlStructureTestStatus() == ServiceProviderTestData::Failed ) { // The XML structure of the provider plugin .pts file is marked as failed in the cache // Cannot add provider data to data sources, the file needs to be fixed first qWarning() << "Provider plugin" << providerId << "is invalid."; qDebug() << "Fix the provider file at" << ServiceProviderGlobal::fileNameFromId(providerId); qDebug() << testData.errorMessage(); qDebug() << "************************************"; providerData->clear(); *errorMessage = testData.errorMessage(); m_erroneousProviders.insert( providerId, testData.errorMessage() ); updateErroneousServiceProviderSource(); return false; } // The sub type test may already be marked as failed in the cache, // but when provider data can be read it should be added to provider data source // also when the provider plugin is invalid // Read provider data from the XML file QString _errorMessage; const QScopedPointer data( ServiceProviderDataReader::read(providerId, &_errorMessage) ); if ( data.isNull() ) { // Could not read provider data if ( testData.isXmlStructureTestPending() ) { // Store error message in cache and do not reread unchanged XMLs everytime testData.setXmlStructureTestStatus( ServiceProviderTestData::Failed, _errorMessage ); testData.write( providerId, cache ); } providerData->clear(); *errorMessage = _errorMessage; m_erroneousProviders.insert( providerId, _errorMessage ); updateErroneousServiceProviderSource(); return false; } // Check if support for the used provider type has been build into the engine if ( !ServiceProviderGlobal::isProviderTypeAvailable(data->type()) ) { providerData->clear(); _errorMessage = i18nc("@info/plain", "Support for provider type %1 is not available", ServiceProviderGlobal::typeName(data->type(), ServiceProviderGlobal::ProviderTypeNameWithoutUnsupportedHint)); *errorMessage = _errorMessage; m_erroneousProviders.insert( providerId, _errorMessage ); updateErroneousServiceProviderSource(); return false; } // Mark the XML test as passed if not done already if ( testData.isXmlStructureTestPending() ) { testData.setXmlStructureTestStatus( ServiceProviderTestData::Passed ); testData.write( providerId, cache ); } // XML file structure test is passed, run provider type test if not done already switch ( testData.subTypeTestStatus() ) { case ServiceProviderTestData::Pending: { // Need to create the provider to run tests in derived classes (in the constructor) const QScopedPointer provider( createProviderForData(data.data(), this, cache) ); data->setParent( 0 ); // Prevent deletion of data when the provider gets deleted // Read test data again, because it may have been changed in the provider constructor testData = ServiceProviderTestData::read( providerId, cache ); // Run the sub type test if it is still pending testData = provider->runSubTypeTest( testData, cache ); // Read test data again, updated in the ServiceProvider constructor if ( testData.results().testFlag(ServiceProviderTestData::SubTypeTestFailed) ) { // Sub-type test failed qWarning() << "Test failed for" << providerId << testData.errorMessage(); providerData->clear(); *errorMessage = testData.errorMessage(); m_erroneousProviders.insert( providerId, testData.errorMessage() ); updateErroneousServiceProviderSource(); return false; } // The provider is already created, use it in serviceProviderData(), if needed *providerData = serviceProviderData( provider.data() ); break; } case ServiceProviderTestData::Failed: // Test is marked as failed in the cache *errorMessage = testData.errorMessage(); m_erroneousProviders.insert( providerId, testData.errorMessage() ); updateErroneousServiceProviderSource(); *providerData = serviceProviderData( *data ); return false; case ServiceProviderTestData::Passed: // Test is marked as passed in the cache *providerData = serviceProviderData( *data ); break; } m_erroneousProviders.remove( providerId ); const QLatin1String name = sourceTypeKeyword( ErroneousServiceProvidersSource ); removeData( name, providerId ); errorMessage->clear(); return true; } bool PublicTransportEngine::updateErroneousServiceProviderSource() { const QLatin1String name = sourceTypeKeyword( ErroneousServiceProvidersSource ); setData( name, static_cast(m_erroneousProviders) ); return true; } bool PublicTransportEngine::updateLocationSource() { const QLatin1String name = sourceTypeKeyword( LocationsSource ); if ( m_dataSources.contains(name) ) { setData( name, m_dataSources[name]->data() ); } else { SimpleDataSource *dataSource = new SimpleDataSource( name, locations() ); m_dataSources.insert( name, dataSource ); setData( name, dataSource->data() ); } return true; } QString PublicTransportEngine::providerIdFromSourceName( const QString &sourceName ) { const int pos = sourceName.indexOf( ' ' ); if ( pos == -1 ) { //|| pos = sourceName.length() - 1 ) { return QString(); } const int endPos = sourceName.indexOf( '|', pos + 1 ); return fixProviderId( sourceName.mid(pos + 1, endPos - pos - 1).trimmed() ); } ParseDocumentMode PublicTransportEngine::parseModeFromSourceType( PublicTransportEngine::SourceType type ) { switch ( type ) { case DeparturesSource: return ParseForDepartures; case ArrivalsSource: return ParseForArrivals; case StopsSource: return ParseForStopSuggestions; case JourneysDepSource: return ParseForJourneysByDepartureTime; case JourneysArrSource: return ParseForJourneysByArrivalTime; case JourneysSource: return ParseForJourneysByDepartureTime; default: return ParseInvalid; } } bool PublicTransportEngine::enoughDataAvailable( DataSource *dataSource, const SourceRequestData &sourceData ) const { TimetableDataSource *timetableDataSource = dynamic_cast< TimetableDataSource* >( dataSource ); if ( !timetableDataSource ) { return true; } AbstractTimetableItemRequest *request = sourceData.request; return timetableDataSource->enoughDataAvailable( request->dateTime(), request->count() ); } bool PublicTransportEngine::updateTimetableDataSource( const SourceRequestData &data ) { const QString nonAmbiguousName = disambiguateSourceName( data.name ); bool containsDataSource = m_dataSources.contains( nonAmbiguousName ); if ( containsDataSource && isSourceUpToDate(nonAmbiguousName) && enoughDataAvailable(m_dataSources[nonAmbiguousName], data) ) { // Data is stored in the map and up to date TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); dataSource->addUsingDataSource( QSharedPointer(data.request->clone()), data.name, data.request->dateTime(), data.request->count() ); setData( data.name, dataSource->data() ); } else if ( m_runningSources.contains(nonAmbiguousName) ) { // Source gets already processed qDebug() << "Source already gets processed, please wait" << data.name; } else if ( data.parseMode == ParseInvalid || !data.request ) { qWarning() << "Invalid source" << data.name; return false; } else { // Request new data TimetableDataSource *dataSource = containsDataSource ? dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ) : new TimetableDataSource(nonAmbiguousName); dataSource->clear(); dataSource->addUsingDataSource( QSharedPointer(data.request->clone()), data.name, data.request->dateTime(), data.request->count() ); m_dataSources[ nonAmbiguousName ] = dataSource; // Start the request request( data ); } return true; } void PublicTransportEngine::requestAdditionalData( const QString &sourceName, int updateItem, int count ) { TimetableDataSource *dataSource = testDataSourceForAdditionalDataRequests( sourceName ); if ( dataSource ) { // Start additional data requests bool dataChanged = false; for ( int itemNumber = updateItem; itemNumber < updateItem + count; ++itemNumber ) { dataChanged = requestAdditionalData(sourceName, itemNumber, dataSource) || dataChanged; } if ( dataChanged ) { // Publish changes to "additionalDataState" fields publishData( dataSource ); } } } TimetableDataSource *PublicTransportEngine::testDataSourceForAdditionalDataRequests( const QString &sourceName ) { // Try to get a pointer to the provider with the provider ID from the source name const QString providerId = providerIdFromSourceName( sourceName ); const ProviderPointer provider = providerFromId( providerId ); if ( provider.isNull() || provider->type() == Enums::InvalidProvider ) { emit additionalDataRequestFinished( sourceName, -1, false, QString("Service provider %1 could not be created").arg(providerId) ); return 0; // Service provider couldn't be created } // Test if the provider supports additional data if ( !provider->features().contains(Enums::ProvidesAdditionalData) ) { emit additionalDataRequestFinished( sourceName, -1, false, i18nc("@info/plain", "Additional data not supported") ); qWarning() << "Additional data not supported by" << provider->id(); return 0; // Service provider does not support additional data } // Test if the source with the given name is cached const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { emit additionalDataRequestFinished( sourceName, -1, false, "Data source to update not found: " + sourceName ); return 0; } // Get the data list, currently only for departures/arrivals TODO: journeys TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { emit additionalDataRequestFinished( sourceName, -1, false, "Data source is not a timetable data source: " + sourceName ); return 0; } return dataSource; } bool PublicTransportEngine::requestAdditionalData( const QString &sourceName, int itemNumber, TimetableDataSource *dataSource ) { QVariantList items = dataSource->timetableItems(); if ( itemNumber >= items.count() || itemNumber < 0 ) { emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Item to update (%1) not found in data source").arg(itemNumber) ); return false; } // Get the timetable item stored in the data source at the given index QVariantHash item = items[ itemNumber ].toHash(); // Check if additional data is already included or was already requested const QString additionalDataState = item["additionalDataState"].toString(); if ( additionalDataState == QLatin1String("included") ) { emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Additional data is already included for item %1").arg(itemNumber) ); return false; } else if ( additionalDataState == QLatin1String("busy") ) { qDebug() << "Additional data for item" << itemNumber << "already requested, please wait"; emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Additional data was already requested for item %1, please wait") .arg(itemNumber) ); return false; } // Check if the timetable item is valid, // extract values needed for the additional data request job const QDateTime dateTime = item[ "DepartureDateTime" ].toDateTime(); const QString transportLine = item[ "TransportLine" ].toString(); const QString target = item[ "Target" ].toString(); const QString routeDataUrl = item[ "RouteDataUrl" ].toString(); if ( routeDataUrl.isEmpty() && (!dateTime.isValid() || transportLine.isEmpty() || target.isEmpty()) ) { emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Item to update is invalid: %1, %2, %3") .arg(dateTime.toString()).arg(transportLine, target) ); return false; } // Store state of additional data in the timetable item item["additionalDataState"] = "busy"; items[ itemNumber ] = item; dataSource->setTimetableItems( items ); // Found data of the timetable item to update const SourceRequestData sourceData( dataSource->name() ); const ProviderPointer provider = providerFromId( dataSource->providerId() ); Q_ASSERT( provider ); provider->requestAdditionalData( AdditionalDataRequest(dataSource->name(), itemNumber, sourceData.request->stop(), sourceData.request->stopId(), dateTime, transportLine, target, sourceData.request->city(), routeDataUrl) ); return true; } QString PublicTransportEngine::fixProviderId( const QString &providerId ) { if ( !providerId.isEmpty() ) { return providerId; } // No service provider ID given, use the default one for the users country const QString country = KGlobal::locale()->country(); // Try to find the XML filename of the default accessor for [country] const QString filePath = ServiceProviderGlobal::defaultProviderForLocation( country ); if ( filePath.isEmpty() ) { return 0; } // Extract service provider ID from filename const QString defaultProviderId = ServiceProviderGlobal::idFromFileName( filePath ); qDebug() << "No service provider ID given, using the default one for country" << country << "which is" << defaultProviderId; return defaultProviderId; } QString PublicTransportEngine::disambiguateSourceName( const QString &sourceName ) { // Remove count argument QString ret = sourceName; ret.remove( QRegExp("(count=[^\\|]+)") ); // Round time parameter values to 15 minutes precision QRegExp rx( "(time=[^\\|]+|datetime=[^\\|]+)" ); if ( rx.indexIn(ret) != -1 ) { // Get the time value const QString timeParameter = rx.cap( 1 ); QDateTime time; if ( timeParameter.startsWith(QLatin1String("time=")) ) { time = QDateTime( QDate::currentDate(), QTime::fromString(timeParameter.mid(5), "hh:mm") ); } else { // startsWith "datetime=" time = QDateTime::fromString( timeParameter.mid(9), Qt::ISODate ); if ( !time.isValid() ) { time = QDateTime::fromString( timeParameter.mid(9) ); } } // Round 15 minutes qint64 msecs = time.toMSecsSinceEpoch(); time = QDateTime::fromMSecsSinceEpoch( msecs - msecs % (1000 * 60 * 15) ); // Replace old time parameter with a new one ret.replace( rx.pos(), rx.matchedLength(), QLatin1String("datetime=") + time.toString(Qt::ISODate) ); } // Read parameters to reorder them afterwards const SourceType type = sourceTypeFromName( ret ); const QString typeKeyword = sourceTypeKeyword( type ); const QStringList parameterPairs = ret.mid( typeKeyword.length() ) .trimmed().split( '|', QString::SkipEmptyParts ); QString defaultParameter; QHash< QString, QString > parameters; for ( int i = 0; i < parameterPairs.length(); ++i ) { const QString parameter = parameterPairs.at( i ).trimmed(); const int pos = parameter.indexOf( '=' ); if ( pos == -1 ) { // No parameter name given, this is the default parameter, eg. the provider ID defaultParameter = fixProviderId( parameter ); } else { // Only add parameters with non-empty parameter name and value, // make parameter value lower case (eg. stop names) const QString parameterName = parameter.left( pos ); const QString parameterValue = parameter.mid( pos + 1 ).trimmed(); if ( !parameterName.isEmpty() && !parameterValue.isEmpty() ) { parameters[ parameterName ] = parameterValue; } } } // Build non-ambiguous source name with standardized parameter order ret = typeKeyword + ' ' + defaultParameter; if ( parameters.contains(QLatin1String("city")) ) { ret += "|city=" + parameters["city"].toLower(); } if ( parameters.contains(QLatin1String("stop")) ) { ret += "|stop=" + parameters["stop"].toLower(); } if ( parameters.contains(QLatin1String("stopid")) ) { ret += "|stopid=" + parameters["stopid"]; } if ( parameters.contains(QLatin1String("originstop")) ) { ret += "|originstop=" + parameters["originstop"].toLower(); } if ( parameters.contains(QLatin1String("originstopid")) ) { ret += "|originstopid=" + parameters["originstopid"]; } if ( parameters.contains(QLatin1String("targetstop")) ) { ret += "|targetstop=" + parameters["targetstop"].toLower(); } if ( parameters.contains(QLatin1String("targetstopid")) ) { ret += "|targetstopid=" + parameters["targetstopid"]; } if ( parameters.contains(QLatin1String("timeoffset")) ) { ret += "|timeoffset=" + parameters["timeoffset"]; } if ( parameters.contains(QLatin1String("time")) ) { ret += "|time=" + parameters["time"]; } if ( parameters.contains(QLatin1String("datetime")) ) { ret += "|datetime=" + parameters["datetime"]; } if ( parameters.contains(QLatin1String("count")) ) { ret += "|count=" + parameters["count"]; } if ( parameters.contains(QLatin1String("longitude")) ) { ret += "|longitude=" + parameters["longitude"]; } if ( parameters.contains(QLatin1String("latitude")) ) { ret += "|latitude=" + parameters["latitude"]; } return ret; } void PublicTransportEngine::serviceProviderDirChanged( const QString &path ) { Q_UNUSED( path ) // Use a timer to prevent loading all service providers again and again, for every changed file // in a possibly big list of files. It reloads the providers maximally every 250ms. // Otherwise it can freeze plasma for a while if eg. all provider files are changed at once. if ( !m_providerUpdateDelayTimer ) { m_providerUpdateDelayTimer = new QTimer( this ); connect( m_providerUpdateDelayTimer, SIGNAL(timeout()), this, SLOT(reloadChangedProviders()) ); } m_providerUpdateDelayTimer->start( 250 ); } void PublicTransportEngine::deleteProvider( const QString &providerId, bool keepProviderDataSources ) { // Clear all cached data const QStringList cachedSources = m_dataSources.keys(); foreach( const QString &cachedSource, cachedSources ) { const QString currentProviderId = providerIdFromSourceName( cachedSource ); if ( currentProviderId == providerId ) { // Disconnect provider and abort all running requests, // take provider from the provider list without caching it ProviderPointer provider = m_providers.take( providerId ); if ( provider ) { disconnect( provider.data(), 0, this, 0 ); provider->abortAllRequests(); } m_runningSources.removeOne( cachedSource ); if ( keepProviderDataSources ) { // Update data source for the provider TimetableDataSource *timetableSource = dynamic_cast< TimetableDataSource* >( m_dataSources[cachedSource] ); if ( timetableSource ) { // Stop automatic updates timetableSource->stopUpdateTimer(); // Update manually foreach ( const QString &sourceName, timetableSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } } } else { // Delete data source for the provider delete m_dataSources.take( cachedSource ); } } } } void PublicTransportEngine::reloadChangedProviders() { qDebug() << "Reload service providers (the service provider dir changed)"; delete m_providerUpdateDelayTimer; m_providerUpdateDelayTimer = 0; // Notify the providers data source about the changed provider directory. // Do not remove it here, so that it can track which providers have changed ProvidersDataSource *providersSource = providersDataSource(); if ( providersSource ) { providersSource->providersHaveChanged(); } // Remove cached locations source to have it updated delete m_dataSources.take( sourceTypeKeyword(LocationsSource) ); // Clear all cached data (use the new provider to parse the data again) const QStringList cachedSources = m_dataSources.keys(); const QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); foreach( const QString &cachedSource, cachedSources ) { const QString providerId = providerIdFromSourceName( cachedSource ); if ( !providerId.isEmpty() && (ServiceProviderGlobal::isSourceFileModified(providerId, cache) #ifdef BUILD_PROVIDER_TYPE_SCRIPT || !ServiceProviderScript::isTestResultUnchanged(providerId, cache) #endif ) ) { m_providers.remove( providerId ); m_cachedProviders.remove( providerId ); m_erroneousProviders.remove( providerId ); updateProviderData( providerId, cache ); TimetableDataSource *timetableSource = dynamic_cast< TimetableDataSource* >( m_dataSources[cachedSource] ); if ( timetableSource ) { // Stop automatic updates timetableSource->stopUpdateTimer(); // Update manually foreach ( const QString &sourceName, timetableSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } } } } updateLocationSource(); updateServiceProviderSource(); updateErroneousServiceProviderSource(); } const QLatin1String PublicTransportEngine::sourceTypeKeyword( SourceType sourceType ) { switch ( sourceType ) { case ServiceProviderSource: return QLatin1String("ServiceProvider"); case ServiceProvidersSource: return QLatin1String("ServiceProviders"); case ErroneousServiceProvidersSource: return QLatin1String("ErroneousServiceProviders"); case LocationsSource: return QLatin1String("Locations"); case VehicleTypesSource: return QLatin1String("VehicleTypes"); case DeparturesSource: return QLatin1String("Departures"); case ArrivalsSource: return QLatin1String("Arrivals"); case StopsSource: return QLatin1String("Stops"); case JourneysSource: return QLatin1String("Journeys"); case JourneysDepSource: return QLatin1String("JourneysDep"); case JourneysArrSource: return QLatin1String("JourneysArr"); default: return QLatin1String(""); } } PublicTransportEngine::SourceType PublicTransportEngine::sourceTypeFromName( const QString &sourceName ) { // Get type of the source, do not match case insensitive, otherwise there can be multiple // sources with the same data but only different case if ( sourceName.startsWith(sourceTypeKeyword(ServiceProviderSource) + ' ') ) { return ServiceProviderSource; } else if ( sourceName.compare(sourceTypeKeyword(ServiceProvidersSource)) == 0 ) { return ServiceProvidersSource; } else if ( sourceName.compare(sourceTypeKeyword(ErroneousServiceProvidersSource)) == 0 ) { return ErroneousServiceProvidersSource; } else if ( sourceName.compare(sourceTypeKeyword(LocationsSource)) == 0 ) { return LocationsSource; } else if ( sourceName.compare(sourceTypeKeyword(VehicleTypesSource)) == 0 ) { return VehicleTypesSource; } else if ( sourceName.startsWith(sourceTypeKeyword(DeparturesSource)) ) { return DeparturesSource; } else if ( sourceName.startsWith(sourceTypeKeyword(ArrivalsSource)) ) { return ArrivalsSource; } else if ( sourceName.startsWith(sourceTypeKeyword(StopsSource)) ) { return StopsSource; } else if ( sourceName.startsWith(sourceTypeKeyword(JourneysDepSource)) ) { return JourneysDepSource; } else if ( sourceName.startsWith(sourceTypeKeyword(JourneysArrSource)) ) { return JourneysArrSource; } else if ( sourceName.startsWith(sourceTypeKeyword(JourneysSource)) ) { return JourneysSource; } else { return InvalidSourceName; } } PublicTransportEngine::SourceRequestData::SourceRequestData( const QString &name ) : name(name), type(sourceTypeFromName(name)), parseMode(parseModeFromSourceType(type)), request(0) { if ( isDataRequestingSourceType(type) ) { // Extract parameters, which follow after the source type keyword in name // and are delimited with '|' QStringList parameters = name.mid( QString(sourceTypeKeyword(type)).length() ) .trimmed().split( '|', QString::SkipEmptyParts ); switch ( parseMode ) { case ParseForDepartures: request = new DepartureRequest( name, parseMode ); break; case ParseForArrivals: request = new ArrivalRequest( name, parseMode ); break; case ParseForStopSuggestions: { bool hasLongitude = false, hasLatitude = false; for ( int i = 0; i < parameters.length(); ++i ) { const QString parameter = parameters.at( i ).trimmed(); const QString parameterName = parameter.left( parameter.indexOf('=') ); if ( parameterName == QLatin1String("longitude") ) { hasLongitude = true; } else if ( parameterName == QLatin1String("latitude") ) { hasLatitude = true; } } if ( hasLongitude && hasLatitude ) { request = new StopsByGeoPositionRequest( name, parseMode ); } else { request = new StopSuggestionRequest( name, parseMode ); } break; } case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: request = new JourneyRequest( name, parseMode ); break; default: qWarning() << "Cannot create a request for parse mode" << parseMode; return; } // Read parameters for ( int i = 0; i < parameters.length(); ++i ) { const QString parameter = parameters.at( i ).trimmed(); const int pos = parameter.indexOf( '=' ); if ( pos == -1 ) { if ( !defaultParameter.isEmpty() ) { qWarning() << "More than one parameters without name given:" << defaultParameter << parameter; } // No parameter name given, assume the service provider ID defaultParameter = parameter; } else { const QString parameterName = parameter.left( pos ); const QString parameterValue = parameter.mid( pos + 1 ).trimmed(); if ( parameterValue.isEmpty() ) { qWarning() << "Empty parameter value for parameter" << parameterName; } else if ( parameterName == QLatin1String("city") ) { request->setCity( parameterValue ); } else if ( parameterName == QLatin1String("stop") ) { request->setStop( parameterValue ); } else if ( parameterName == QLatin1String("stopid") ) { request->setStopId( parameterValue ); } else if ( parameterName == QLatin1String("targetstop") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'targetstop' parameter is only used for journey requests"; } else { journeyRequest->setTargetStop( parameterValue ); } } else if ( parameterName == QLatin1String("targetstopid") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'targetstopId' parameter is only used for journey requests"; } else { journeyRequest->setTargetStopId( parameterValue ); } } else if ( parameterName == QLatin1String("originstop") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'originstop' parameter is only used for journey requests"; } else { journeyRequest->setStop( parameterValue ); } } else if ( parameterName == QLatin1String("originstopid") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'originstopId' parameter is only used for journey requests"; } else { journeyRequest->setStopId( parameterValue ); } } else if ( parameterName == QLatin1String("timeoffset") ) { request->setDateTime( QDateTime::currentDateTime().addSecs(parameterValue.toInt() * 60) ); } else if ( parameterName == QLatin1String("time") ) { request->setDateTime( QDateTime(QDate::currentDate(), QTime::fromString(parameterValue, "hh:mm")) ); } else if ( parameterName == QLatin1String("datetime") ) { request->setDateTime( QDateTime::fromString(parameterValue, Qt::ISODate) ); if ( !request->dateTime().isValid() ) { request->setDateTime( QDateTime::fromString(parameterValue) ); } } else if ( parameterName == QLatin1String("count") ) { bool ok; request->setCount( parameterValue.toInt(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'count' in source name:" << parameterValue; request->setCount( 20 ); } } else if ( dynamic_cast(request) ) { StopsByGeoPositionRequest *stopRequest = dynamic_cast< StopsByGeoPositionRequest* >( request ); bool ok; if ( parameterName == QLatin1String("longitude") ) { stopRequest->setLongitude( parameterValue.toFloat(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'longitude' in source name:" << parameterValue; } } else if ( parameterName == QLatin1String("latitude") ) { stopRequest->setLatitude( parameterValue.toFloat(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'latitude' in source name:" << parameterValue; } } else if ( parameterName == QLatin1String("distance") ) { stopRequest->setDistance( parameterValue.toInt(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'distance' in source name:" << parameterValue; } } else { qWarning() << "Unknown argument" << parameterName; } } else { qWarning() << "Unknown argument" << parameterName; } } } if ( !request->dateTime().isValid() ) { // No date/time value given, use default offset from now request->setDateTime( QDateTime::currentDateTime().addSecs(DEFAULT_TIME_OFFSET * 60) ); } // The default parameter is the provider ID for data requesting sources, // use the default provider for the users country if no ID is given defaultParameter = PublicTransportEngine::fixProviderId( defaultParameter ); } else { // Extract provider ID or country code, which follow after the source type keyword in name defaultParameter = name.mid( QString(sourceTypeKeyword(type)).length() ).trimmed(); } } PublicTransportEngine::SourceRequestData::~SourceRequestData() { delete request; } bool PublicTransportEngine::SourceRequestData::isValid() const { if ( type == InvalidSourceName ) { qWarning() << "Invalid source name" << name; return false; } if ( isDataRequestingSourceType(type) ) { if ( parseMode == ParseForDepartures || parseMode == ParseForArrivals ) { // Check if the stop name/ID is missing if ( !request || (request->stop().isEmpty() && request->stopId().isEmpty()) ) { qWarning() << "No stop ID or name in data source name" << name; return false; } } else if ( parseMode == ParseForStopSuggestions ) { // Check if the stop name or geo coordinates are missing if ( !request || (!dynamic_cast(request) && request->stop().isEmpty()) ) { qWarning() << "Stop name (part) is missing in data source name" << name; return false; } } else if ( parseMode == ParseForJourneysByArrivalTime || parseMode == ParseForJourneysByDepartureTime ) { // Make sure non empty originstop(id) and targetstop(id) parameters are available JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "Internal error: Not a JourneyRequest object but parseMode is" << parseMode; return false; } if ( journeyRequest->stop().isEmpty() && journeyRequest->stopId().isEmpty() ) { qWarning() << "No stop ID or name for the origin stop in data source name" << name; return false; } if ( journeyRequest->targetStop().isEmpty() && journeyRequest->targetStopId().isEmpty() ) { qWarning() << "No stop ID or name for the target stop in data source name" << name; return false; } } } return true; } bool PublicTransportEngine::sourceRequestEvent( const QString &name ) { // If name is associated with a data source that runs asynchronously, // create the source first with empty data, gets updated once the request has finished SourceRequestData data( name ); if ( data.isValid() && isDataRequestingSourceType(data.type) ) { setData( name, DataEngine::Data() ); } return requestOrUpdateSourceEvent( data ); } bool PublicTransportEngine::updateSourceEvent( const QString &name ) { return requestOrUpdateSourceEvent( SourceRequestData(name), true ); } bool PublicTransportEngine::requestOrUpdateSourceEvent( const SourceRequestData &sourceData, bool update ) { if ( !sourceData.isValid() ) { return false; } switch ( sourceData.type ) { case ServiceProviderSource: return updateServiceProviderForCountrySource( sourceData ); case ServiceProvidersSource: return updateServiceProviderSource(); case ErroneousServiceProvidersSource: return updateErroneousServiceProviderSource(); case LocationsSource: return updateLocationSource(); case DeparturesSource: case ArrivalsSource: case StopsSource: case JourneysSource: case JourneysArrSource: case JourneysDepSource: { return updateTimetableDataSource( sourceData ); } // This data source never changes, ie. needs no updates case VehicleTypesSource: if ( !update ) { initVehicleTypesSource(); } return true; case InvalidSourceName: default: qDebug() << "Source name incorrect" << sourceData.name; return false; } } void PublicTransportEngine::initVehicleTypesSource() { QVariantHash vehicleTypes; const int index = Enums::staticMetaObject.indexOfEnumerator("VehicleType"); const QMetaEnum enumerator = Enums::staticMetaObject.enumerator( index ); // Start at i = 1 to skip Enums::InvalidVehicleType for ( int i = 1; i < enumerator.keyCount(); ++i ) { Enums::VehicleType vehicleType = static_cast< Enums::VehicleType >( enumerator.value(i) ); QVariantHash vehicleTypeData; vehicleTypeData.insert( "id", enumerator.key(i) ); vehicleTypeData.insert( "name", Global::vehicleTypeToString(vehicleType) ); vehicleTypeData.insert( "namePlural", Global::vehicleTypeToString(vehicleType, true) ); vehicleTypeData.insert( "iconName", Global::vehicleTypeToIcon(vehicleType) ); vehicleTypes.insert( QString::number(enumerator.value(i)), vehicleTypeData ); } setData( sourceTypeKeyword(VehicleTypesSource), vehicleTypes ); } void PublicTransportEngine::timetableDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &items, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool deleteDepartureInfos, bool isDepartureData ) { Q_UNUSED( requestUrl ); Q_UNUSED( provider ); Q_UNUSED( deleteDepartureInfos ); const QString sourceName = request.sourceName(); DEBUG_ENGINE_JOBS( items.count() << (isDepartureData ? "departures" : "arrivals") << "received" << sourceName ); const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Data source already removed"; return; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); Q_ASSERT( dataSource ); m_runningSources.removeOne( nonAmbiguousName ); QVariantList departuresData; const QString itemKey = isDepartureData ? "departures" : "arrivals"; foreach( const DepartureInfoPtr &departureInfo, items ) { QVariantHash departureData; TimetableData departure = departureInfo->data(); for ( TimetableData::ConstIterator it = departure.constBegin(); it != departure.constEnd(); ++it ) { if ( it.value().isValid() ) { departureData.insert( Global::timetableInformationToString(it.key()), it.value() ); } } departureData.insert( "Nightline", departureInfo->isNightLine() ); departureData.insert( "Expressline", departureInfo->isExpressLine() ); // Add existing additional data const uint hash = TimetableDataSource::hashForDeparture( departure, isDepartureData ); if ( dataSource->additionalData().contains(hash) ) { // Found already downloaded additional data, add it to the updated departure data const TimetableData additionalData = dataSource->additionalData( hash ); if ( !additionalData.isEmpty() ) { bool hasAdditionalData = false; for ( TimetableData::ConstIterator it = additionalData.constBegin(); it != additionalData.constEnd(); ++it ) { if ( it.key() != Enums::RouteDataUrl ) { hasAdditionalData = true; } departureData.insert( Global::timetableInformationToString(it.key()), it.value() ); } departureData[ "additionalDataState" ] = hasAdditionalData ? "included" : "error"; } } if ( !departureData.contains("additionalDataState") ) { departureData[ "additionalDataState" ] = provider->features().contains(Enums::ProvidesAdditionalData) ? "notrequested" : "notsupported"; } departuresData << departureData; } // Cleanup the data source later startDataSourceCleanupLater( dataSource ); dataSource->setValue( itemKey, departuresData ); // if ( deleteDepartureInfos ) { // qDebug() << "Delete" << items.count() << "departures/arrivals"; // qDeleteAll( departures ); // } // Fill the data source with general information const QDateTime dateTime = QDateTime::currentDateTime(); dataSource->setValue( "serviceProvider", provider->id() ); dataSource->setValue( "delayInfoAvailable", globalInfo.delayInfoAvailable ); dataSource->setValue( "requestUrl", requestUrl ); dataSource->setValue( "parseMode", request.parseModeName() ); dataSource->setValue( "error", false ); dataSource->setValue( "updated", dateTime ); // Store a proposal for the next download time QDateTime last = items.isEmpty() ? dateTime : items.last()->value(Enums::DepartureDateTime).toDateTime(); dataSource->setNextDownloadTimeProposal( dateTime.addSecs(dateTime.secsTo(last) / 3) ); const QDateTime nextUpdateTime = provider->nextUpdateTime( dataSource->updateFlags(), dateTime, dataSource->nextDownloadTimeProposal(), dataSource->data() ); const QDateTime minManualUpdateTime = provider->nextUpdateTime( dataSource->updateFlags() | UpdateWasRequestedManually, dateTime, dataSource->nextDownloadTimeProposal(), dataSource->data() ); // Store update times in the data source dataSource->setValue( "nextAutomaticUpdate", nextUpdateTime ); dataSource->setValue( "minManualUpdateTime", minManualUpdateTime ); // Publish the data source and cache it publishData( dataSource ); m_dataSources[ nonAmbiguousName ] = dataSource; int msecsUntilUpdate = dateTime.msecsTo( nextUpdateTime ); Q_ASSERT( msecsUntilUpdate >= 10000 ); // Make sure to not produce too many updates by mistake DEBUG_ENGINE_JOBS( "Update data source in" << KGlobal::locale()->prettyFormatDuration(msecsUntilUpdate) ); if ( !dataSource->updateTimer() ) { QTimer *updateTimer = new QTimer( this ); connect( updateTimer, SIGNAL(timeout()), this, SLOT(updateTimeout()) ); dataSource->setUpdateTimer( updateTimer ); } dataSource->updateTimer()->start( msecsUntilUpdate ); } void PublicTransportEngine::startDataSourceCleanupLater( TimetableDataSource *dataSource ) { if ( !dataSource->cleanupTimer() ) { QTimer *cleanupTimer = new QTimer( this ); cleanupTimer->setInterval( 2500 ); connect( cleanupTimer, SIGNAL(timeout()), this, SLOT(cleanupTimeout()) ); dataSource->setCleanupTimer( cleanupTimer ); } dataSource->cleanupTimer()->start(); } void PublicTransportEngine::updateTimeout() { // Find the timetable data source to which the timer belongs which timeout() signal was emitted QTimer *timer = qobject_cast< QTimer* >( sender() ); TimetableDataSource *dataSource = dataSourceFromTimer( timer ); if ( !dataSource ) { // No data source found that started the timer, // should not happen the data source should have deleted the timer on destruction qWarning() << "Timeout received from an unknown update timer"; return; } // Found the timetable data source of the timer, // requests updates for all connected sources (possibly multiple combined stops) foreach ( const QString &sourceName, dataSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } // TODO FIXME Do not update while running additional data requests? } void PublicTransportEngine::cleanupTimeout() { // Find the timetable data source to which the timer belongs which timeout() signal was emitted QTimer *timer = qobject_cast< QTimer* >( sender() ); TimetableDataSource *dataSource = dataSourceFromTimer( timer ); if ( !dataSource ) { // No data source found that started the timer, // should not happen the data source should have deleted the timer on destruction qWarning() << "Timeout received from an unknown cleanup timer"; return; } // Found the timetable data source of the timer dataSource->cleanup(); dataSource->setCleanupTimer( 0 ); // Deletes the timer } void PublicTransportEngine::additionalDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const TimetableData &data, const AdditionalDataRequest &request ) { Q_UNUSED( provider ); Q_UNUSED( requestUrl ); // Check if the destination data source exists const QString nonAmbiguousName = disambiguateSourceName( request.sourceName() ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Additional data received for a source that was already removed:" << nonAmbiguousName; emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), false, "Data source to update was already removed" ); return; } // Get the list of timetable items from the existing data source TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); Q_ASSERT( dataSource ); QVariantList items = dataSource->timetableItems(); if ( request.itemNumber() >= items.count() ) { emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), false, QString("Item %1 not found in the data source (with %2 items)") .arg(request.itemNumber()).arg(items.count()) ); return; } // Get the timetable item for which additional data was received // and insert the new data into it bool newDataInserted = false; TimetableData _data = data; QVariantHash item = items[ request.itemNumber() ].toHash(); for ( TimetableData::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { // Check if there already is data in the current additional data field const QString key = Global::timetableInformationToString( it.key() ); const bool isRouteDataUrl = key == Enums::toString(Enums::RouteDataUrl); if ( item.contains(key) && item[key].isValid() && !isRouteDataUrl ) { // The timetable item already contains data for the current field, // do not allow updates here, only additional data qWarning() << "Cannot update timetable data in additional data requests"; qWarning() << "Tried to update field" << key << "to value" << it.value() << "from value" << item[key] << "in data source" << request.sourceName(); } else { // Allow to add the additional data field item.insert( key, it.value() ); if ( !isRouteDataUrl ) { newDataInserted = true; if ( key == Enums::toString(Enums::RouteStops) ) { // Added a RouteStops field, automatically generate RouteStopsShortened const QString routeStopsShortenedKey = Global::timetableInformationToString( Enums::RouteStopsShortened ); if ( !item.contains(routeStopsShortenedKey) ) { const QStringList routeStopsShortened = removeCityNameFromStops( it.value().toStringList() ); item.insert( routeStopsShortenedKey, routeStopsShortened ); _data.insert( Enums::RouteStopsShortened, routeStopsShortened ); } } } } } if ( !newDataInserted ) { const QString errorMessage = i18nc("@info/plain", "No additional data found"); emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), false, errorMessage ); item[ "additionalDataState" ] = "error"; item[ "additionalDataError" ] = errorMessage; items[ request.itemNumber() ] = item; dataSource->setTimetableItems( items ); return; } item[ "additionalDataState" ] = "included"; // Store the changed item back into the item list of the data source items[ request.itemNumber() ] = item; dataSource->setTimetableItems( items ); // Also store received additional data separately // to not loose additional data after updating the data source const uint hash = TimetableDataSource::hashForDeparture( item, dataSource->timetableItemKey() != QLatin1String("arrivals") ); dataSource->setAdditionalData( hash, _data ); startDataSourceCleanupLater( dataSource ); QTimer *updateDelayTimer; if ( dataSource->updateAdditionalDataDelayTimer() ) { // Use existing timer, restart it with a longer interval and // publish the new data of the data source after the timeout updateDelayTimer = dataSource->updateAdditionalDataDelayTimer(); updateDelayTimer->setInterval( 250 ); } else { // Create timer with a shorter interval, but directly publish the new data of the // data source. The timer is used here to delay further publishing of new data, // ie. combine multiple updates and publish them at once. publishData( dataSource ); updateDelayTimer = new QTimer( this ); updateDelayTimer->setInterval( 150 ); connect( updateDelayTimer, SIGNAL(timeout()), this, SLOT(updateDataSourcesWithNewAdditionData()) ); dataSource->setUpdateAdditionalDataDelayTimer( updateDelayTimer ); } // (Re-)start the additional data update timer updateDelayTimer->start(); // Emit result emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), true ); } TimetableDataSource *PublicTransportEngine::dataSourceFromTimer( QTimer *timer ) const { for ( QHash< QString, DataSource* >::ConstIterator it = m_dataSources.constBegin(); it != m_dataSources.constEnd(); ++it ) { TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( *it ); if ( dataSource && (dataSource->updateAdditionalDataDelayTimer() == timer || dataSource->updateTimer() == timer || dataSource->cleanupTimer() == timer) ) { return dataSource; } } // The timer is not used any longer return 0; } void PublicTransportEngine::updateDataSourcesWithNewAdditionData() { QTimer *updateDelayTimer = qobject_cast< QTimer* >( sender() ); Q_ASSERT( updateDelayTimer ); TimetableDataSource *dataSource = dataSourceFromTimer( updateDelayTimer ); if ( !dataSource ) { qWarning() << "Data source was already removed"; return; } const int interval = updateDelayTimer->interval(); dataSource->setUpdateAdditionalDataDelayTimer( 0 ); // Deletes the timer // Do the upate, but only after long delays, // because the update is already done before short delays if ( interval > 150 ) { // Was delayed for a longer time publishData( dataSource ); } } void PublicTransportEngine::departuresReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &departures, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool deleteDepartureInfos ) { timetableDataReceived( provider, requestUrl, departures, globalInfo, request, deleteDepartureInfos, true ); } void PublicTransportEngine::arrivalsReceived( ServiceProvider *provider, const QUrl &requestUrl, const ArrivalInfoList &arrivals, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool deleteDepartureInfos ) { timetableDataReceived( provider, requestUrl, arrivals, globalInfo, request, deleteDepartureInfos, false ); } void PublicTransportEngine::journeysReceived( ServiceProvider* provider, const QUrl &requestUrl, const JourneyInfoList &journeys, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool deleteJourneyInfos ) { Q_UNUSED( provider ); Q_UNUSED( deleteJourneyInfos ); const QString sourceName = request.sourceName(); DEBUG_ENGINE_JOBS( journeys.count() << "journeys received" << sourceName ); const QString nonAmbiguousName = disambiguateSourceName( sourceName ); m_runningSources.removeOne( nonAmbiguousName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Data source already removed" << nonAmbiguousName; return; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Data source already deleted" << nonAmbiguousName; return; } dataSource->clear(); QVariantList journeysData; foreach( const JourneyInfoPtr &journeyInfo, journeys ) { if ( !journeyInfo->isValid() ) { continue; } QVariantHash journeyData; TimetableData journey = journeyInfo->data(); for ( TimetableData::ConstIterator it = journey.constBegin(); it != journey.constEnd(); ++it ) { if ( it.value().isValid() ) { journeyData.insert( Global::timetableInformationToString(it.key()), it.value() ); } } journeysData << journeyData; } dataSource->setValue( "journeys", journeysData ); int journeyCount = journeys.count(); QDateTime first, last; if ( journeyCount > 0 ) { first = journeys.first()->value(Enums::DepartureDateTime).toDateTime(); last = journeys.last()->value(Enums::DepartureDateTime).toDateTime(); } else { first = last = QDateTime::currentDateTime(); } // if ( deleteJourneyInfos ) { // qDeleteAll( journeys ); TODO // } // Store a proposal for the next download time int secs = ( journeyCount / 3 ) * first.secsTo( last ); QDateTime downloadTime = QDateTime::currentDateTime().addSecs( secs ); dataSource->setNextDownloadTimeProposal( downloadTime ); // Store received data in the data source map dataSource->setValue( "serviceProvider", provider->id() ); dataSource->setValue( "delayInfoAvailable", globalInfo.delayInfoAvailable ); dataSource->setValue( "requestUrl", requestUrl ); dataSource->setValue( "parseMode", request.parseModeName() ); dataSource->setValue( "error", false ); dataSource->setValue( "updated", QDateTime::currentDateTime() ); dataSource->setValue( "nextAutomaticUpdate", downloadTime ); dataSource->setValue( "minManualUpdateTime", downloadTime ); // TODO setData( sourceName, dataSource->data() ); m_dataSources[ nonAmbiguousName ] = dataSource; } void PublicTransportEngine::stopsReceived( ServiceProvider *provider, const QUrl &requestUrl, const StopInfoList &stops, const StopSuggestionRequest &request, bool deleteStopInfos ) { Q_UNUSED( provider ); Q_UNUSED( deleteStopInfos ); const QString sourceName = request.sourceName(); m_runningSources.removeOne( disambiguateSourceName(sourceName) ); DEBUG_ENGINE_JOBS( stops.count() << "stop suggestions received" << sourceName ); QVariantList stopsData; foreach( const StopInfoPtr &stopInfo, stops ) { QVariantHash stopData; TimetableData stop = stopInfo->data(); for ( TimetableData::ConstIterator it = stop.constBegin(); it != stop.constEnd(); ++it ) { if ( it.value().isValid() ) { stopData.insert( Global::timetableInformationToString(it.key()), it.value() ); } } stopsData << stopData; } setData( sourceName, "stops", stopsData ); setData( sourceName, "serviceProvider", provider->id() ); setData( sourceName, "requestUrl", requestUrl ); setData( sourceName, "parseMode", request.parseModeName() ); setData( sourceName, "error", false ); setData( sourceName, "updated", QDateTime::currentDateTime() ); // if ( deleteStopInfos ) { // qDeleteAll( stops ); TODO // } } void PublicTransportEngine::requestFailed( ServiceProvider *provider, ErrorCode errorCode, const QString &errorMessage, const QUrl &requestUrl, const AbstractRequest *request ) { Q_UNUSED( provider ); qDebug() << errorCode << errorMessage << "- Requested URL:" << requestUrl; const AdditionalDataRequest *additionalDataRequest = dynamic_cast< const AdditionalDataRequest* >( request ); if ( additionalDataRequest ) { // A request for additional data failed emit additionalDataRequestFinished( request->sourceName(), additionalDataRequest->itemNumber(), false, errorMessage ); // Check if the destination data source exists const QString nonAmbiguousName = disambiguateSourceName( request->sourceName() ); if ( m_dataSources.contains(nonAmbiguousName) ) { // Get the list of timetable items from the existing data source TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); Q_ASSERT( dataSource ); QVariantList items = dataSource->timetableItems(); if ( additionalDataRequest->itemNumber() < items.count() ) { // Found the item for which the request failed, // reset additional data included/waiting fields QVariantHash item = items[ additionalDataRequest->itemNumber() ].toHash(); item[ "additionalDataState" ] = "error"; item[ "additionalDataError" ] = errorMessage; items[ additionalDataRequest->itemNumber() ] = item; dataSource->setTimetableItems( items ); // Publish updated fields publishData( dataSource ); } else { qWarning() << "Timetable item" << additionalDataRequest->itemNumber() << "not found in data source" << request->sourceName() << "additional data error discarded"; } } else { qWarning() << "Data source" << request->sourceName() << "not found, additional data error discarded"; } // Do not mark the whole timetable data source as erroneous // when a request for additional data has failed return; } // Remove erroneous source from running sources list m_runningSources.removeOne( disambiguateSourceName(request->sourceName()) ); const QString sourceName = request->sourceName(); setData( sourceName, "serviceProvider", provider->id() ); setData( sourceName, "requestUrl", requestUrl ); setData( sourceName, "parseMode", request->parseModeName() ); setData( sourceName, "receivedData", "nothing" ); setData( sourceName, "error", true ); setData( sourceName, "errorCode", errorCode ); setData( sourceName, "errorMessage", errorMessage ); setData( sourceName, "updated", QDateTime::currentDateTime() ); } bool PublicTransportEngine::requestUpdate( const QString &sourceName ) { // Find the TimetableDataSource object for sourceName const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Not an existing timetable data source:" << sourceName; emit updateRequestFinished( sourceName, false, i18nc("@info", "Data source is not an existing timetable data source: %1", sourceName) ); return false; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Internal error: Invalid pointer to the data source stored"; emit updateRequestFinished( sourceName, false, "Internal error: Invalid pointer to the data source stored" ); return false; } // Check if updates are blocked to prevent to many useless updates const QDateTime nextUpdateTime = sourceUpdateTime( dataSource, UpdateWasRequestedManually ); if ( QDateTime::currentDateTime() < nextUpdateTime ) { qDebug() << "Too early to update again, update request rejected" << nextUpdateTime; emit updateRequestFinished( sourceName, false, i18nc("@info", "Update request rejected, earliest update time: %1", nextUpdateTime.toString()) ); return false; } // Stop automatic updates dataSource->stopUpdateTimer(); // Start the request const bool result = request( sourceName ); if ( result ) { emit updateRequestFinished( sourceName ); } else { emit updateRequestFinished( sourceName, false, i18nc("@info", "Request failed") ); } return result; } bool PublicTransportEngine::requestMoreItems( const QString &sourceName, Enums::MoreItemsDirection direction ) { // Find the TimetableDataSource object for sourceName const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Not an existing timetable data source:" << sourceName; emit moreItemsRequestFinished( sourceName, direction, false, i18nc("@info", "Data source is not an existing timetable data source: %1", sourceName) ); return false; } else if ( m_runningSources.contains(nonAmbiguousName) ) { // The data source currently gets processed or updated, including requests for more items qDebug() << "Source currently gets processed, please wait" << sourceName; emit moreItemsRequestFinished( sourceName, direction, false, i18nc("@info", "Source currently gets processed, please try again later") ); return false; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Internal error: Invalid pointer to the data source stored"; emit moreItemsRequestFinished( sourceName, direction, false, "Internal error" ); return false; } const SourceRequestData data( sourceName ); qDebug() << data.defaultParameter; const ProviderPointer provider = providerFromId( data.defaultParameter ); if ( provider.isNull() ) { // Service provider couldn't be created, should only happen after provider updates, // where the provider worked to get timetable items, but the new provider version // does not work any longer and no more items can be requested emit moreItemsRequestFinished( sourceName, direction, false, "Provider could not be created" ); return false; } // Start the request QSharedPointer< AbstractRequest > request = dataSource->request( sourceName ); const QVariantList items = dataSource->timetableItems(); if ( items.isEmpty() ) { // TODO qWarning() << "No timetable items in data source" << sourceName; emit moreItemsRequestFinished( sourceName, direction, false, i18nc("@info", "No timetable items in data source") ); return false; } const QVariantHash item = (direction == Enums::EarlierItems ? items.first() : items.last()).toHash(); const QVariantMap _requestData = item[Enums::toString(Enums::RequestData)].toMap(); // TODO Convert to hash from map.. QVariantHash requestData; for ( QVariantMap::ConstIterator it = _requestData.constBegin(); it != _requestData.constEnd(); ++it ) { requestData.insert( it.key(), it.value() ); } // Start the request m_runningSources << nonAmbiguousName; provider->requestMoreItems( MoreItemsRequest(sourceName, request, requestData, direction) ); emit moreItemsRequestFinished( sourceName, direction ); return true; } ErrorCode PublicTransportEngine::errorCodeFromState( const QString &stateId ) { if ( stateId == QLatin1String("ready") ) { return NoError; } else if ( stateId == QLatin1String("gtfs_feed_import_pending") || stateId == QLatin1String("importing_gtfs_feed") ) { return ErrorNeedsImport; } else { return ErrorParsingFailed; } } bool PublicTransportEngine::request( const SourceRequestData &data ) { // Try to get the specific provider from m_providers (if it's not in there it is created) const ProviderPointer provider = providerFromId( data.defaultParameter ); if ( provider.isNull() || provider->type() == Enums::InvalidProvider ) { // Service provider couldn't be created // Remove erroneous source from running sources list const QString sourceName = data.request->sourceName(); m_runningSources.removeOne( disambiguateSourceName(sourceName) ); ProvidersDataSource *dataSource = providersDataSource(); const QString state = dataSource->providerState( data.defaultParameter ); if ( state != QLatin1String("ready") ) { const QVariantHash stateData = dataSource->providerStateData( data.defaultParameter ); setData( sourceName, "serviceProvider", data.defaultParameter ); setData( sourceName, "parseMode", data.request->parseModeName() ); setData( sourceName, "receivedData", "nothing" ); setData( sourceName, "error", true ); setData( sourceName, "errorCode", errorCodeFromState(state) ); setData( sourceName, "errorMessage", stateData["statusMessage"].toString() ); setData( sourceName, "updated", QDateTime::currentDateTime() ); } return false; } else if ( provider->useSeparateCityValue() && data.request->city().isEmpty() && !dynamic_cast(data.request) ) { qDebug() << QString("Service provider %1 needs a separate city value. Add to source name " "'|city=X', where X stands for the city name.") .arg( data.defaultParameter ); return false; // Service provider needs a separate city value } else if ( !provider->features().contains(Enums::ProvidesJourneys) && (data.parseMode == ParseForJourneysByDepartureTime || data.parseMode == ParseForJourneysByArrivalTime) ) { qDebug() << QString("Service provider %1 doesn't support journey searches.") .arg( data.defaultParameter ); return false; // Service provider doesn't support journey searches } // Store source name as currently being processed, to not start another // request if there is already a running one m_runningSources << disambiguateSourceName( data.name ); // Start the request provider->request( data.request ); return true; } int PublicTransportEngine::secsUntilUpdate( const QString &sourceName, QString *errorMessage ) { return getSecsUntilUpdate( sourceName, errorMessage ); } int PublicTransportEngine::minSecsUntilUpdate( const QString &sourceName, QString *errorMessage ) { return getSecsUntilUpdate( sourceName, errorMessage, UpdateWasRequestedManually ); } int PublicTransportEngine::getSecsUntilUpdate( const QString &sourceName, QString *errorMessage, UpdateFlags updateFlags ) { const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { // The data source is not a timetable data source or does not exist if ( errorMessage ) { *errorMessage = i18nc("@info", "Data source is not an existing timetable " "data source: %1", sourceName); } return -1; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Internal error: Invalid pointer to the data source stored"; return -1; } const QDateTime time = sourceUpdateTime( dataSource, updateFlags ); return qMax( -1, QDateTime::currentDateTime().secsTo(time) ); } QDateTime PublicTransportEngine::sourceUpdateTime( TimetableDataSource *dataSource, UpdateFlags updateFlags ) { const QString providerId = fixProviderId( dataSource->providerId() ); const ProviderPointer provider = providerFromId( providerId ); if ( provider.isNull() || provider->type() == Enums::InvalidProvider ) { return QDateTime(); } return provider->nextUpdateTime( dataSource->updateFlags() | updateFlags, dataSource->lastUpdate(), dataSource->nextDownloadTimeProposal(), dataSource->data() ); } bool PublicTransportEngine::isSourceUpToDate( const QString &nonAmbiguousName, UpdateFlags updateFlags ) { if ( !m_dataSources.contains(nonAmbiguousName) ) { return false; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { // The data source is not a timetable data source, no periodic updates needed return true; } const QDateTime nextUpdateTime = sourceUpdateTime( dataSource, updateFlags ); DEBUG_ENGINE_JOBS( "Wait time until next download:" << KGlobal::locale()->prettyFormatDuration( (QDateTime::currentDateTime().msecsTo(nextUpdateTime))) ); return QDateTime::currentDateTime() < nextUpdateTime; } ServiceProvider *PublicTransportEngine::createProvider( const QString &serviceProviderId, QObject *parent, const QSharedPointer &cache ) { ServiceProviderData *data = ServiceProviderDataReader::read( serviceProviderId ); return data ? createProviderForData(data, parent, cache) : 0; } ServiceProvider *PublicTransportEngine::createProviderForData( const ServiceProviderData *data, QObject *parent, const QSharedPointer &cache ) { if ( !ServiceProviderGlobal::isProviderTypeAvailable(data->type()) ) { qWarning() << "Cannot create provider of type" << data->typeName() << "because the engine " "has been built without support for that provider type"; return 0; } // Warn if the format of the provider plugin is unsupported if ( data->fileFormatVersion() != QLatin1String("1.1") ) { qWarning() << "The Provider" << data->id() << "was designed for an unsupported " "provider plugin format version, update to version 1.1"; return 0; } switch ( data->type() ) { #ifdef BUILD_PROVIDER_TYPE_SCRIPT case Enums::ScriptedProvider: return new ServiceProviderScript( data, parent, cache ); #endif #ifdef BUILD_PROVIDER_TYPE_GTFS case Enums::GtfsProvider: return new ServiceProviderGtfs( data, parent, cache ); #endif case Enums::InvalidProvider: default: qWarning() << "Invalid/unknown provider type" << data->type(); return 0; } } QStringList PublicTransportEngine::removeCityNameFromStops( const QStringList &stopNames ) { // Find words at the beginning/end of target and route stop names that have many // occurrences. These words are most likely the city names where the stops are in. // But the timetable becomes easier to read and looks nicer, if not each stop name // includes the same city name. QHash< QString, int > firstWordCounts; // Counts occurrences of words at the beginning QHash< QString, int > lastWordCounts; // Counts occurrences of words at the end // minWordOccurrence is the minimum occurence count of a word in stop names to have the word // removed. maxWordOccurrence is the maximum number of occurences to count, if this number // of occurences has been found, just remove that word. const int minWordOccurrence = qMax( 2, stopNames.count() ); const int maxWordOccurrence = qMax( 3, stopNames.count() / 2 ); // This regular expression gets used to search for word at the end, possibly including // a colon before the last word QRegExp rxLastWord( ",?\\s+\\S+$" ); // These strings store the words with the most occurrences in stop names at the beginning/end QString removeFirstWord; QString removeLastWord; // Analyze stop names foreach ( const QString &stopName, stopNames ) { // Find word to remove from beginning/end of stop names, if not already found if ( !removeFirstWord.isEmpty() || !removeLastWord.isEmpty() ) { break; } int pos = stopName.indexOf( ' ' ); const QString newFirstWord = stopName.left( pos ); if ( pos > 0 && ++firstWordCounts[newFirstWord] >= maxWordOccurrence ) { removeFirstWord = newFirstWord; } if ( rxLastWord.indexIn(stopName) != -1 && ++lastWordCounts[rxLastWord.cap()] >= maxWordOccurrence ) { removeLastWord = rxLastWord.cap(); } } // Remove word with most occurrences from beginning/end of stop names if ( removeFirstWord.isEmpty() && removeLastWord.isEmpty() ) { // If no first/last word with at least maxWordOccurrence occurrences was found, // find the word with the most occurrences int max = 0; // Find word at the beginning with most occurrences for ( QHash< QString, int >::ConstIterator it = firstWordCounts.constBegin(); it != firstWordCounts.constEnd(); ++it ) { if ( it.value() > max ) { max = it.value(); removeFirstWord = it.key(); } } // Find word at the end with more occurrences for ( QHash< QString, int >::ConstIterator it = lastWordCounts.constBegin(); it != lastWordCounts.constEnd(); ++it ) { if ( it.value() > max ) { max = it.value(); removeLastWord = it.key(); } } if ( max < minWordOccurrence ) { // The first/last word with the most occurrences has too few occurrences // Do not remove any word removeFirstWord.clear(); removeLastWord.clear(); } else if ( !removeLastWord.isEmpty() ) { // removeLastWord has more occurrences than removeFirstWord removeFirstWord.clear(); } } if ( !removeFirstWord.isEmpty() ) { // Remove removeFirstWord from all stop names QStringList returnStopNames; foreach ( const QString &stopName, stopNames ) { if ( stopName.startsWith(removeFirstWord) ) { returnStopNames << stopName.mid( removeFirstWord.length() + 1 ); } else { returnStopNames << stopName; } } return returnStopNames; } else if ( !removeLastWord.isEmpty() ) { // Remove removeLastWord from all stop names QStringList returnStopNames; foreach ( const QString &stopName, stopNames ) { if ( stopName.endsWith(removeLastWord) ) { returnStopNames << stopName.left( stopName.length() - removeLastWord.length() ); } else { returnStopNames << stopName; } } return returnStopNames; } else { // Nothing to remove found return stopNames; } } // This does the magic that allows Plasma to load // this plugin. The first argument must match // the X-Plasma-EngineName in the .desktop file. K_EXPORT_PLASMA_DATAENGINE( publictransport, PublicTransportEngine ) // this is needed since PublicTransportEngine is a QObject #include "build/publictransportdataengine.moc" diff --git a/engine/script/scriptapi.h b/engine/script/scriptapi.h index 112c14a..b48d42e 100644 --- a/engine/script/scriptapi.h +++ b/engine/script/scriptapi.h @@ -1,1748 +1,1749 @@ /* * Copyright 2012 Friedrich Pülz * * 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 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file * @brief This file contains helper classes to be used from service provider plugin scripts. * * @author Friedrich Pülz */ #ifndef SCRIPTING_HEADER #define SCRIPTING_HEADER // Own includes #include "enums.h" #include "departureinfo.h" // TODO for eg. PublicTransportInfoList typedef // Qt includes #include #include // Base class +#include #include class PublicTransportInfo; class ServiceProviderData; class KConfigGroup; class QScriptContextInfo; class QNetworkRequest; class QReadWriteLock; class QNetworkReply; class QNetworkAccessManager; class QMutex; /** @brief Stores information about a departure/arrival/journey/stop suggestion. */ typedef QHash TimetableData; /** @brief Namespace for classes exposed to scripts. */ namespace ScriptApi { class Network; /** @ingroup scriptApi * @{ */ /** * @brief Represents one asynchronous request, created with Network::createRequest(). * * To get notified about new data, connect to either the finished() or the readyRead() signal. **/ class NetworkRequest : public QObject { Q_OBJECT Q_PROPERTY( QString url READ url ) Q_PROPERTY( QUrl redirectedUrl READ redirectedUrl ) Q_PROPERTY( bool isRunning READ isRunning ) Q_PROPERTY( bool isFinished READ isFinished ) Q_PROPERTY( bool isRedirected READ isRedirected ) Q_PROPERTY( QString postData READ postData ) friend class Network; public: /** @brief Creates an invalid request object. Needed for Q_DECLARE_METATYPE. */ NetworkRequest( QObject* parent = 0 ); /** @brief Create a new request object for @p url, managed by @p network. */ explicit NetworkRequest( const QString &url, const QString &userUrl, Network *network, QObject* parent = 0 ); /** @brief Destructor. */ virtual ~NetworkRequest(); typedef QSharedPointer< NetworkRequest > Ptr; /** * @brief The URL of this request. * * @note The URL can not be changed, a request object is only used for @em one request. **/ QString url() const; /** * @brief An URL for this request that should be shown to users. * This can be eg. an URL to an HTML document showing the data that is also available * at the requested URL, but eg. in XML format. * @see url() **/ QString userUrl() const; /** * @brief Whether or not the request is currently running. **/ bool isRunning() const; /** * @brief Whether or not the request is finished (successful or not), ie. was running. **/ bool isFinished() const; /** @brief Whether or not this request was redirected. */ bool isRedirected() const; /** @brief Get the redirected URL of this request, if any. */ QUrl redirectedUrl() const; /** * @brief Set the data to sent to the server when using Network::post(). * * This function automatically sets the "ContentType" header of the request to the used * charset. If you want to use another value for the "ContentType" header than the * data is actually encoded in, you can change the header using setHeader() after calling * this function. * @note If the request is already started, no more POST data can be set and this function * will do nothing. * * @param postData The data to be POSTed. * @param charset The charset to encode @p postData with. If charset is an empty string (the * default) the "ContentType" header gets used if it was set using setHeader(). Otherwise * utf8 gets used. * @see isRunning() **/ Q_INVOKABLE void setPostData( const QString &postData, const QString &charset = QString() ); /** @brief Get the data to sent to the server when using Network::post(). */ QString postData() const; /** @brief Get the @p header decoded using @p charset. */ Q_INVOKABLE QString header( const QString &header, const QString& charset ) const; /** * @brief Set the @p header of this request to @p value. * * @note If the request is already started, no more headers can be set and this function * will do nothing. * * @param header The name of the header to change. * @param value The new value for the @p header. * @param charset The charset to encode @p header and @p value with. If charset is an empty * string (the default) the "ContentType" header gets used if it was set using setHeader(). * Otherwise utf8 gets used. * @see isRunning() **/ Q_INVOKABLE void setHeader( const QString &header, const QString &value, const QString &charset = QString() ); quint64 uncompressedSize() const; /** * @brief Set custom @p userData for the request. * * This data is available to slots connected to the finished() signal. **/ Q_INVOKABLE void setUserData( const QVariant &userData ); /** * @brief Get custom user data stored with setUserData(). **/ Q_INVOKABLE QVariant userData() const; public Q_SLOTS: /** * @brief Aborts this (running) request. * * @note If the request has not already been started, it cannot be aborted, of course, * and this function will do nothing. **/ void abort(); Q_SIGNALS: /** * @brief Emitted when this request was started. **/ void started(); /** * @brief Emitted when this request got aborted or timed out. * @param timedOut Whether or not the request was aborted because of a timeout. **/ void aborted( bool timedOut = false ); /** * @brief Emitted when this request has finished. * * @param data The complete data downloaded for this request. * @param error @c True, if there was an error executing the request, @c false otherwise. * @param errorString A human readable description of the error if @p error is @c true. * @param statusCode The HTTP status code that was received or -1 if there was an error. * @param size The size in bytes of the received data. * @param url The URL of the request. * @param userData Custom data stored for the request, see setUserData(). **/ void finished( const QByteArray &data = QByteArray(), bool error = false, const QString &errorString = QString(), int statusCode = -1, int size = 0, const QString &url = QString(), const QVariant &userData = QVariant() ); /** * @brief Emitted when new data is available for this request. * * @param data New downloaded data for this request. **/ void readyRead( const QByteArray &data ); void redirected( const QUrl &newUrl ); protected Q_SLOTS: void slotFinished(); void slotReadyRead(); protected: void started( QNetworkReply* reply, int timeout = 0 ); QByteArray postDataByteArray() const; QNetworkRequest *request() const; QByteArray getCharset( const QString &charset = QString() ) const; bool isValid() const; private: QMutex *m_mutex; const QString m_url; const QString m_userUrl; QUrl m_redirectUrl; Network *m_network; bool m_isFinished; QNetworkRequest *m_request; QNetworkReply *m_reply; QByteArray m_data; QByteArray m_postData; quint32 m_uncompressedSize; QVariant m_userData; }; /** \} */ // @ingroup scriptApi /** @ingroup scriptApi * @{ */ /** * @brief Provides network access to scripts. * * An instance of the Network class is available for scripts as @b network. It can be used to * synchronously or asynchronously download data. It doesn't really matter what method the script * uses, because every call of a script function from the data engine happens in it's own thread * and won't block anything. For the threads ThreadWeaver gets used with jobs of type ScriptJob. * * To download a document synchronously simply call getSynchronous() with the URL to download. * When the download is finished getSynchronous(this request using the POST method) returns and the script can start parsing the * document. There is a default timeout of 30 seconds. If a requests takes more time it gets * aborted. To define your own timeout you can give it as argument to getSynchronous(). * @note downloadSynchronous() is an alias for getSynchronous(). * * @code * var document = network.getSynchronous( url ); * // Parse the downloaded document here * @endcode * @n * * To create a network request which should be executed asynchronously use createRequest() with * the URL of the request as argument. The script should then connect to either the finished() * or readyRead() signal of the network request object. The request gets started by calling * get() / download(). To only get the headers use head(). * The example below shows how to asynchronously request a document from within a script: * @code var request = network.createRequest( url ); request.finished.connect( handler ); network.get( request ); function handler( document ) { // Parse the downloaded document here }; @endcode * @n * * If a script needs to use the POST method to request data use post(). The data to be sent in * a POST request can be set using NetworkRequest::setPostData(). * * @note One request object created with createRequest() can @em not be used multiple times in * parallel. To start another request create a new request object. * @note There is a global 60 seconds timeout for all network requests to finish. **/ class Network : public QObject, public QScriptable { Q_OBJECT Q_PROPERTY( QString lastUrl READ lastUrl ) Q_PROPERTY( bool lastDownloadAborted READ lastDownloadAborted ) Q_PROPERTY( QString fallbackCharset READ fallbackCharset ) Q_PROPERTY( int runningRequestCount READ runningRequestCount ) friend class NetworkRequest; public: /** @brief The default timeout in milliseconds for network requests. */ static const int DEFAULT_TIMEOUT = 30000; /** @brief Constructor. */ explicit Network( const QByteArray &fallbackCharset = QByteArray(), QObject* parent = 0 ); /** @brief Destructor. */ virtual ~Network(); /** * @brief Get the last requested URL. * * The last URL gets updated every time a request gets started, eg. using get(), post(), * getSynchronous(), download(), downloadSynchronous(), etc. **/ Q_INVOKABLE QString lastUrl() const; /** * @brief Get an URL for the last requested URL that should be shown to users. **/ Q_INVOKABLE QString lastUserUrl() const; /** * @brief Clears the last requested URL. **/ Q_INVOKABLE void clear(); /** * @brief Returns @c true, if the last download was aborted before it was ready. * * Use lastUrl() to get the URL of the aborted download. Downloads may be aborted eg. by * closing plasma. **/ Q_INVOKABLE bool lastDownloadAborted() const; /** * @brief Download the document at @p url synchronously. * * @param url The URL to download. * @param userUrl An URL for this request that should be shown to users. * This can be eg. an URL to an HTML document showing the data that is also available * at the requested URL, but eg. in XML format. * @param timeout Maximum time in milliseconds to wait for the reply to finish. * If smaller than 0, no timeout gets used. * Note that there is a global 60 seconds timeout for all network requests to finish. **/ Q_INVOKABLE QByteArray getSynchronous( const QString &url, const QString &userUrl = QString(), int timeout = DEFAULT_TIMEOUT ); /** * @brief This is an alias for getSynchronous(). **/ Q_INVOKABLE inline QByteArray downloadSynchronous( const QString &url, const QString &userUrl = QString(), int timeout = DEFAULT_TIMEOUT ) { return getSynchronous(url, userUrl, timeout); }; /** * @brief Creates a new NetworkRequest for asynchronous network access. * * @note Each NetworkRequest object can only be used once for one download. * @see get, download, post, head **/ Q_INVOKABLE NetworkRequest *createRequest( const QString &url, const QString &userUrl = QString() ); /** * @brief Perform the network @p request asynchronously. * * @param request The NetworkRequest object created with createRequest(). * @param timeout Maximum time in milliseconds to wait for the reply to finish. * If smaller than 0, no timeout gets used. * Note that there is a global 60 seconds timeout for all network requests to finish. **/ Q_INVOKABLE void get( NetworkRequest *request, int timeout = DEFAULT_TIMEOUT ); /** * @brief Perform the network @p request asynchronously using POST method. * * @param request The NetworkRequest object created with createRequest(). * @param timeout Maximum time in milliseconds to wait for the reply to finish. * If smaller than 0, no timeout gets used. * Note that there is a global 60 seconds timeout for all network requests to finish. **/ Q_INVOKABLE void post( NetworkRequest *request, int timeout = DEFAULT_TIMEOUT ); /** * @brief Perform the network @p request asynchronously, but only get headers. * * @param request The NetworkRequest object created with createRequest(). * @param timeout Maximum time in milliseconds to wait for the reply to finish. * If smaller than 0, no timeout gets used. * Note that there is a global 60 seconds timeout for all network requests to finish. **/ Q_INVOKABLE void head( NetworkRequest *request, int timeout = DEFAULT_TIMEOUT ); /** * @brief This is an alias for get(). **/ Q_INVOKABLE inline void download( NetworkRequest *request, int timeout = DEFAULT_TIMEOUT ) { get( request, timeout ); }; /** * @brief Returns whether or not there are network requests running in the background. * @see runningRequests **/ Q_INVOKABLE bool hasRunningRequests() const; /** * @brief Get a list of all running asynchronous requests. * * If hasRunningRequests() returns @c false, this will return an empty list. * If hasRunningRequests() returns @c true, this @em may return a non-empty list (maybe only * a synchronous request is running). * @see hasRunningRequests **/ Q_INVOKABLE QList< NetworkRequest::Ptr > runningRequests() const; /** * @brief Returns the number of currently running requests. * * @note This includes running synchronous requests. The number of request objects returned * by runningRequests() may be less than the number returned here, because it only returns * running asynchronous requests. **/ Q_INVOKABLE int runningRequestCount() const; /** * @brief Returns the charset to use for decoding documents, if it cannot be detected. * * The fallback charset can be selected in the XML file, as \-tag. **/ Q_INVOKABLE QByteArray fallbackCharset() const; Q_SIGNALS: /** * @brief Emitted when an asynchronous request has been started. * * This signal is @em not emitted if the network gets accessed synchronously. * @param request The request that has been started. * @see synchronousRequestStarted() **/ void requestStarted( const NetworkRequest::Ptr &request ); /** * @brief Emitted when an asynchronous request has finished. * * This signal is @em not emitted if the network gets accessed synchronously. * @param request The request that has finished. * @param data Received data decoded to a string. * @param error @c True, if there was an error executing the request, @c false otherwise. * @param errorString A human readable description of the error if @p error is @c true. * @param timestamp The date and time on which the request was finished. * @param statusCode The HTTP status code received. * @param size The size in bytes of the received data. * @see synchronousRequestFinished() **/ void requestFinished( const NetworkRequest::Ptr &request, const QByteArray &data = QByteArray(), bool error = false, const QString &errorString = QString(), const QDateTime ×tamp = QDateTime(), int statusCode = 200, int size = 0 ); /** @brief Emitted when an asynchronous @p request was redirect to @p newUrl. */ void requestRedirected( const NetworkRequest::Ptr &request, const QUrl &newUrl ); /** * @brief Emitted when a synchronous request has been started. * * This signal is @em not emitted if the network gets accessed asynchronously. * @param url The URL of the request that has been started. * @see requestStarted() **/ void synchronousRequestStarted( const QString &url ); /** * @brief Emitted when an synchronous request has finished. * * This signal is @em not emitted if the network gets accessed asynchronously. * @param url The URL of the request that has finished. * @param data Received data decoded to a string. * @param cancelled Whether or not the request has been cancelled, eg. because of a timeout. * @param statusCode The HTTP status code if @p cancelled is @c false. * @param waitTime The time spent waiting for the download to finish. * @param size The size in bytes of the received data. * @see requestFinished() **/ void synchronousRequestFinished( const QString &url, const QByteArray &data = QByteArray(), bool cancelled = false, int statusCode = 200, int waitTime = 0, int size = 0 ); /** * @brief Emitted when a synchronous request has been redirected. * * This signal is @em not emitted if the network gets accessed asynchronously. * @param url The URL of the request that has been redirected. * @see requestRedirected() **/ void synchronousRequestRedirected( const QString &url ); /** * @brief Emitted when all running requests are finished. * * Emitted when a request finishes and there are no more running requests, * ie. hasRunningRequests() returns @c false. **/ void allRequestsFinished(); /** * @brief Emitted when an asynchronous request got aborted. * * This signal is @em not emitted if the network gets accessed synchronously. * @param request The request that was aborted. **/ void requestAborted( const NetworkRequest::Ptr &request ); /** * @brief Emitted to abort all running synchronous requests. * * This signal gets emitted by abortSynchronousRequests(). **/ void doAbortSynchronousRequests(); public Q_SLOTS: /** * @brief Abort all running requests (synchronous and asynchronous). **/ void abortAllRequests(); /** * @brief Abort all running synchronous requests. **/ void abortSynchronousRequests(); protected Q_SLOTS: void slotRequestStarted(); void slotRequestFinished( const QByteArray &data = QByteArray(), bool error = false, const QString &errorString = QString(), int statusCode = -1, int size = 0 ); void slotRequestAborted(); void slotRequestRedirected( const QUrl &newUrl ); protected: bool checkRequest( NetworkRequest *request ); NetworkRequest::Ptr getSharedRequest( NetworkRequest *request ) const; void emitSynchronousRequestFinished( const QString &url, const QByteArray &data = QByteArray(), bool cancelled = false, int statusCode = 200, int waitTime = 0, int size = 0 ); private: QMutex *m_mutex; const QByteArray m_fallbackCharset; QNetworkAccessManager *m_manager; bool m_quit; int m_synchronousRequestCount; QString m_lastUrl; QString m_lastUserUrl; bool m_lastDownloadAborted; QList< NetworkRequest::Ptr > m_requests; QList< NetworkRequest::Ptr > m_finishedRequests; }; /** \} */ // @ingroup scriptApi /** @ingroup scriptApi * @{ */ /** * @brief A helper class for scripts. * * An instance of this class gets published to scripts as "helper". * Scripts can use it's functions, like here: * @code * var stripped = helper.stripTags("
Test
"); * // stripped == "Test" * * var timeValues = helper.matchTime( "15:28" ); * // timeValues == { hour: 15, minutes: 28, error: false } * * var timeString = helper.formatTime( timeValues[0], timeValues[1] ); * // timeString == "15:28" * * var duration = helper.duration("15:20", "15:45"); * // duration == 25 * * var time2 = helper.addMinsToTime("15:20", duration); * // time2 == "15:45" * * helper.debug("Debug message, eg. something unexpected happened"); * @endcode **/ class Helper : public QObject, protected QScriptable { Q_OBJECT Q_ENUMS( ErrorSeverity ) public: /** @brief The severity of an error. */ enum ErrorSeverity { Information, /**< The message is only an information. */ Warning, /**< The message is a warning. */ Fatal /**< The message describes a fatal error. */ }; /** * @brief Creates a new helper object. * * @param serviceProviderId The ID of the service provider this Helper object is created for. * @param parent The parent object. **/ explicit Helper( const QString &serviceProviderId, QObject* parent = 0 ); virtual ~Helper(); /** * @brief Prints an information @p message on stdout and logs it in a file. * * Logs the message with the given @p failedParseText string, eg. the HTML code where parsing * failed. The message gets also send to stdout with a short version of the data. * The log file is normally located at "~/.kde4/share/apps/plasma_engine_publictransport/serviceproviders.log". * If the same message gets sent repeatedly, it gets sent only once. * * @param message The information message. * @param failedParseText The text in the source document to which the message applies. **/ Q_INVOKABLE void information( const QString &message, const QString &failedParseText = QString() ) { messageReceived(message, failedParseText, Information); }; /** * @brief Prints a warning @p message on stdout and logs it in a file. * * Logs the message with the given @p failedParseText string, eg. the HTML code where parsing * failed. The message gets also send to stdout with a short version of the data. * The log file is normally located at "~/.kde4/share/apps/plasma_engine_publictransport/serviceproviders.log". * If the same message gets sent repeatedly, it gets sent only once. * * @param message The warning message. * @param failedParseText The text in the source document to which the message applies. **/ Q_INVOKABLE void warning( const QString &message, const QString &failedParseText = QString() ) { messageReceived(message, failedParseText, Warning); }; /** * @brief Prints an error @p message on stdout and logs it in a file. * * Logs the message with the given @p failedParseText string, eg. the HTML code where parsing * failed. The message gets also send to stdout with a short version of the data. * The log file is normally located at "~/.kde4/share/apps/plasma_engine_publictransport/serviceproviders.log". * If the same message gets sent repeatedly, it gets sent only once. * * @note If the error is fatal consider using @c throw instead to create an exception and abort * script execution. * * @param message The error message. * @param failedParseText The text in the source document to which the message applies. **/ Q_INVOKABLE void error( const QString &message, const QString &failedParseText = QString() ) { messageReceived(message, failedParseText, Fatal); }; /** * @brief Decodes HTML entities in @p html. * * For example " " gets replaced by " ". * HTML entities which include a charcode, eg. "d" are also replaced, in the example * by the character for the charcode 100, ie. QChar(100). * * @param html The string to be decoded. * @return @p html with decoded HTML entities. **/ Q_INVOKABLE static QString decodeHtmlEntities( const QString &html ); /** @brief Encodes HTML entities in @p html, e.g. "<" is replaced by "<". */ Q_INVOKABLE static QString encodeHtmlEntities( const QString& html ); /** * @brief Decodes the given HTML document. * * First it tries QTextCodec::codecForHtml(). * If that doesn't work, it parses the document for the charset in a meta-tag. **/ Q_INVOKABLE static QString decodeHtml( const QByteArray& document, const QByteArray& fallbackCharset = QByteArray() ); /** @brief Decode @p document using @p charset. */ Q_INVOKABLE static QString decode( const QByteArray& document, const QByteArray& charset = QByteArray() ); /** * @brief Decode @p document using @p charset. * This is needed for scripts that use a string as argument. If the @p charset argument gets * converted to QByteArray from a script string value automatically, it will be empty. **/ Q_INVOKABLE static QString decode( const QByteArray& document, const QString& charset ) { return decode( document, charset.toAscii() ); }; /** * @brief Trims spaces from the beginning and the end of the given string @p str. * * @note The HTML entitiy   is also trimmed. * * @param str The string to be trimmed. * @return @p str without spaces at the beginning or end. **/ Q_INVOKABLE static QString trim( const QString &str ); /** * @brief Like trim(), additionally removes double whitespace in the middle of @p str. * * @note All occurrences of the HTML entitiy   are also removes. * * @param str The string to be simplified. * @return @p str without whitespace in the middle, at the beginning or end. **/ Q_INVOKABLE static QString simplify( const QString &str ); /** * @brief Removes all HTML tags from str. * * This function works with attributes which contain a closing tab as strings. * * @param str The string from which the HTML tags should be removed. * @return @p str without HTML tags. **/ Q_INVOKABLE static QString stripTags( const QString &str ); /** * @brief Makes the first letter of each word upper case, all others lower case. * * @param str The input string. * @return @p str in camel case. **/ Q_INVOKABLE static QString camelCase( const QString &str ); /** * @brief Extracts a block from @p str, which begins at the first occurrence of @p beginString * in @p str and end at the first occurrence of @p endString in @p str. * * @deprecated Does only work with fixed strings. Use eg. findFirstHtmlTag() instead. * @bug The returned string includes beginString but not endString. * * @param str The input string. * @param beginString A string to search for in @p str and to use as start position. * @param endString A string to search for in @p str and to use as end position. * @return The text block in @p str between @p beginString and @p endString. **/ Q_INVOKABLE static QString extractBlock( const QString &str, const QString &beginString, const QString &endString ); /** * @brief Get a map with the hour and minute values parsed from @p str using @p format. * * QVariantMap gets converted to an object in scripts. The result can be used in the script * like this: * @code var time = matchTime( "15:23" ); if ( !time.error ) { var hour = time.hour; var minute = time.minute; } @endcode * * @param str The string containing the time to be parsed, eg. "08:15". * @param format The format of the time string in @p str. Default is "hh:mm". * @return A map with two values: 'hour' and 'minute' parsed from @p str. On error it contains * an 'error' value of @c true. * @see formatTime **/ Q_INVOKABLE static QVariantMap matchTime( const QString &str, const QString &format = "hh:mm" ); /** * @brief Get a date object parsed from @p str using @p format. * * @param str The string containing the date to be parsed, eg. "2010-12-01". * @param format The format of the time string in @p str. Default is "YY-MM-dd". * @return The matched date. * @see formatDate **/ Q_INVOKABLE static QDate matchDate( const QString &str, const QString &format = "yyyy-MM-dd" ); /** * @brief Formats the time given by the values @p hour and @p minute * as string in the given @p format. * * @param hour The hour value of the time. * @param minute The minute value of the time. * @param format The format of the time string to return. Default is "hh:mm". * @return The formatted time string. * @see matchTime **/ Q_INVOKABLE static QString formatTime( int hour, int minute, const QString &format = "hh:mm" ); /** * @brief Formats the time given by the values @p hour and @p minute * as string in the given @p format. * * @param year The year value of the date. * @param month The month value of the date. * @param day The day value of the date. * @param format The format of the date string to return. Default is "yyyy-MM-dd". * @return The formatted date string. * @see matchTime **/ Q_INVOKABLE static QString formatDate( int year, int month, int day, const QString &format = "yyyy-MM-dd" ); /** * @brief Formats @p dateTime using @p format. **/ Q_INVOKABLE static QString formatDateTime( const QDateTime &dateTime, const QString &format = "yyyy-MM-dd" ); /** * @brief Calculates the duration in minutes from the time in @p time1 until @p time2. * * @param time1 A string with the start time, in the given @p format. * @param time2 A string with the end time, in the given @p format. * @param format The format of @p time1 and @p time2. Default is "hh:mm". * @return The number of minutes from @p time1 until @p time2. If @p time2 is earlier than * @p time1 a negative value gets returned. **/ Q_INVOKABLE static int duration( const QString &time1, const QString &time2, const QString &format = "hh:mm" ); /** * @brief Adds @p minsToAdd minutes to the given @p time. * * @param time A string with the base time. * @param minsToAdd The number of minutes to add to @p time. * @param format The format of @p time. Default is "hh:mm". * @return A time string formatted in @p format with the calculated time. **/ Q_INVOKABLE static QString addMinsToTime( const QString &time, int minsToAdd, const QString &format = "hh:mm" ); /** * @brief Adds @p daysToAdd days to the date in @p dateTime. * * @param dateTime A string with the base time. * @param daysToAdd The number of minutes to add to @p time. * @param format The format of @p time. Default is "hh:mm". * @return A time string formatted in @p format with the calculated time. **/ Q_INVOKABLE static QDateTime addDaysToDate( const QDateTime &dateTime, int daysToAdd ); /** * @brief Adds @p daysToAdd days to @p date. * * @param date A string with the base date. * @param daysToAdd The number of days to add to @p date. * @param format The format of @p date. Default is "yyyy-MM-dd". * @return A date string formatted in @p format with the calculated date. **/ Q_INVOKABLE static QString addDaysToDate( const QString &date, int daysToAdd, const QString &format = "yyyy-MM-dd" ); /** * @brief Splits @p str at @p sep, but skips empty parts. * * @param string The string to split. * @param separator The separator. * @return A list of string parts. **/ Q_INVOKABLE static QStringList splitSkipEmptyParts( const QString &string, const QString &separator ); /** * @brief Finds the first occurrence of an HTML tag with @p tagName in @p str. * * @param str The string containing the HTML tag to be found. * @param tagName The name of the HTML tag to be found, ie. <tagName>. * @param options The same as in findHtmlTags(), "maxCount" will be set to 1. * * @b Example: * @code * // This matches the first <table> tag found in html * // which has a class attribute with a value that matches * // the regular expression pattern "test\\d+", * // eg. "test1", "test2", ... * var result = helper.findFirstHtmlTag( html, "table", * {attributes: {"class": "test\\d+"}} ); * @endcode * * @return A map with properties like in findHtmlTags(). Additionally these properties are * returned: * @li @b found: A boolean, @c true if the tag was found, @c false otherwise. * @see findHtmlTags **/ Q_INVOKABLE static QVariantMap findFirstHtmlTag( const QString &str, const QString &tagName, const QVariantMap &options = QVariantMap() ); /** * @brief This is an overloaded function which expects a value for "tagName" in @p options. * @overload QVariantMap findFirstHtmlTag(const QString&,const QString&,const QVariantMap&) **/ Q_INVOKABLE static inline QVariantMap findFirstHtmlTag( const QString &str, const QVariantMap &options = QVariantMap() ) { return findFirstHtmlTag( str, options["tagName"].toString(), options ); }; /** * @brief Find all occurrences of (top level) HTML tags with @p tagName in @p str. * * Using this function avoids having to deal with various problems when matching HTML elements: * @li Nested HTML elements with the same @p tagName. When simply searching for the first * closing tag after the found opening tag, a nested closing tag gets matched. If you are * sure that there are no nested tags or if you want to only match until the first nested * closing tag set the option "noNesting" in @p options to @c true. * @li Matching tags with specific attributes. This function extracts all attributes of a * matched tag. They can have values, which can be put in single/double/no quotation marks. * To only match tags with specific attributes, add them to the "attributes" option in * @p options. Regular expressions can be used to match the attribute name and value * independently. Attribute order does not matter. * @li Matching HTML tags correctly. For example a ">" inside an attributes value could cause * problems and have the tag cut off there. * * @note This function only returns found top level tags. These found tags may contain matching * child tags. You can use this function again on the contents string of a found top level * tag to find its child tags. * * @b Example: * @code * // This matches all <div> tags found in html which * // have a class attribute with the value "test" and only * // numbers as contents, eg. "
42
". * var result = helper.findHtmlTags( html, "div", * {attributes: {"class": "test"}, * contentsRegExp: "\\d+"} ); * @endcode * * @param str The string containing the HTML tags to be found. * @param tagName The name of the HTML tags to be found, ie. <tagName>. * @param options A map with these properties: * @li @b attributes: A map containing all required attributes and it's values. The keys of that * map are the names of required attributes and can be regular expressions. The values * are the values of the required attributes and are also handled as regular expressions. * @li @b contentsRegExp: A regular expression pattern which the contents of found HTML tags * must match. If it does not match, that tag does not get returned as found. * If no parenthesized subexpressions are present in this regular expression, the whole * matching string gets used as contents. If more than one parenthesized subexpressions * are found, only the first one gets used. The regular expression gets matched case * insensitive. By default all content of the HTML tag gets matched. * @li @b position: An integer, where to start the search for tags. If the position is inside * a top-level matching tag, its child tags will be matched and other following top-level * tags. This is 0 by default. * @li @b noContent: A boolean, @c false by default. If @c true, HTML tags without any * content are matched, eg. "br" or "img" tags. Otherwise tags need to be closed to get * matched. * @li @b noNesting: A boolean, @c false by default. If @c true, no checks will be made to * ensure that the first found closing tag belongs to the opening tag. In this case the * found contents always end after the first closing tag after the opening tag, no matter * if the closing tag belongs to a nested tag or not. By setting this to @c true you can * enhance performance. * @li @b maxCount: The maximum number of HTML tags to match or 0 to match any number of HTML tags. * @li @b debug: A boolean, @c false by default. If @c true, more debug output gets generated. * * @return A list of maps, each map represents one found tag and has these properties: * @li @b contents: A string, the contents of the found tag (if found is @c true). * @li @b position: An integer, the position of the first character of the found tag * (ie. '<') in @p str (if found is @c true). * @li @b endPosition: An integer, the position of the first character after the found end * tag in @p str (if found is @c true). * @li @b attributes: A map containing all found attributes of the tag and it's values (if * found is @c true). The attribute names are the keys of the map, while the attribute * values are the values of the map. **/ Q_INVOKABLE static QVariantList findHtmlTags( const QString &str, const QString &tagName, const QVariantMap &options = QVariantMap() ); /** * @brief This is an overloaded function which expects a value for "tagName" in @p options. * @overload QVariantMap findHtmlTags(const QString&,const QString&,const QVariantMap&) **/ Q_INVOKABLE static inline QVariantList findHtmlTags( const QString &str, const QVariantMap &options = QVariantMap() ) { return findHtmlTags( str, options["tagName"].toString(), options ); }; /** * @brief Finds all occurrences of HTML tags with @p tagName in @p str. * * This function uses findHtmlTags() to find the HTML tags and then extracts a name for each * found tag from @p str. * Instead of returning a list of all matched tags, a map is returned, with the found names as * keys and the tag objects (as returned in a list by findHtmlTags()) as values. * * @b Example: * @code * // In this example the findNamedHtmlTags() function gets * // used in a while loop to find columns (-tags) in rows * // (-tags) and assign names to the found columns. This * // makes it easy to parse HTML tables. * * // Initialize position value * var tableRow = { position: -1 }; * * // Use findFirstHtmlTag() with the current position value to * // find the next tag. The return value contains the * // updated position and the boolean property "found" * while ( (tableRow = helper.findFirstHtmlTag(html, "tr", * {position: tableRow.position + 1})).found ) * { * // Find columns, ie. tags in a table row. * // Each column gets a name assigned by findNamedHtmlTags(). * // Ambiguous names are resolved by adding/increasing a * // number after the name. The names get extracted from the * // -tags class attributes, by matching the given * // regular expression. * var columns = helper.findNamedHtmlTags( tableRow.contents, "td", * {ambiguousNameResolution: "addNumber", * namePosition: {type: "attribute", name: "class", * regexp: "([\\w]+)\\d+"}} ); * * // Check the "names" property of the return value, * // should contain the names of all columns that are needed * if ( !columns.names.contains("time") ) { * // Notify the data engine about an error, * // if a "time" column was expected * helper.error("Didn't find all needed columns! " + * "Found: " + columns.names, tableRow.contents); * continue; * } * * // Now read the contents of the columns * var time = columns["time"].contents; * * // Read more and add data to the result set using result.addData() * } * @endcode * * @param str The string containing the HTML tag to be found. * @param tagName The name of the HTML tag to be found, ie. <tagName>. * @param options The same as in findHtmlTags(), but @em additionally these options can be used: * @li @b namePosition: A map with more options, indicating the position of the name of tags: * @li @em type: Can be @em "contents" (ie. use tag contents as name, the default) or * @em "attribute" (ie. use a tag attribute value as name). If @em "attribute" is used * for @em "type", the name of the attribute can be set as @em "name" property. * Additionally a @em "regexp" property can be used to extract a string from the string * that would otherwise be used as name as is. * @li @em ambiguousNameResolution: Can be used to tell what should be done if the same name * was found multiple times. This can currently be one of: @em "addNumber" (adds a * number to the name, ie. "..1", "..2")., @em "replace" (a later match with an already * matched name overwrites the old match, the default). * * @return A map with the found names as keys and the tag objects as values. @em Additionally * these properties are returned: * @li @b names: A list of all found tag names. * @see findHtmlTags **/ Q_INVOKABLE static QVariantMap findNamedHtmlTags( const QString &str, const QString &tagName, const QVariantMap &options = QVariantMap() ); signals: /** * @brief An error was received from the script. * * @param message The error message. * @param failedParseText The text in the source document where parsing failed. * @param severity The severity of the error. **/ void messageReceived( const QString &message, const QScriptContextInfo &contextInfo, const QString &failedParseText = QString(), Helper::ErrorSeverity severity = Warning ); private: static QString getTagName( const QVariantMap &searchResult, const QString &type = "contents", const QString ®Exp = QString(), const QString attributeName = QString() ); void messageReceived( const QString &message, const QString &failedParseText, Helper::ErrorSeverity severity ); void emitRepeatedMessageWarning(); QMutex *m_mutex; QString m_serviceProviderId; QString m_lastErrorMessage; int m_errorMessageRepetition; }; /** \} */ // @ingroup scriptApi /** @ingroup scriptApi * @{ */ /** * @brief This class is used by scripts to store results in, eg. departures. * * An instance of this class gets published to scripts as @b result. * Scripts can use it to add items to the result set, ie. departures/arrivals/journeys/ * stop suggestions. Items can be added from scripts using addData(). * @code * // Add stop suggestion data to result set * result.addData({ StopName: "Name" }); * @endcode **/ class ResultObject : public QObject, protected QScriptable { Q_OBJECT Q_ENUMS( Feature Hint ) Q_FLAGS( Features Hints ) Q_PROPERTY( int count READ count ) public: /** * @brief Used to store enabled features. * * The meta object of ResultObject gets published to scripts under the name "enum" and contains * this enumeration. * * @see enableFeature() * @see isFeatureEnabled() **/ enum Feature { NoFeature = 0x00, /**< No feature is enabled. */ AutoPublish = 0x01, /**< Automatic publishing of the first few data items. Turn * this off if you want to call publish() manually. */ AutoDecodeHtmlEntities = 0x02, /**< Automatic decoding of HTML entities in strings and string * lists. If you are sure, that there are no HTML entities in the strings parsed * from the downloaded documents, you can turn this feature off. You can also * manually decode HTML entities using Helper::decodeHtmlEntities(). */ AutoRemoveCityFromStopNames = 0x04, /**< Automatic removing of city names from all stop names, ie. * stop names in eg. RouteStops or Target). Scripts can help the data engine with * this feature with the hints CityNamesAreLeft or CityNamesAreRight. * @code * result.giveHint( enum.CityNamesAreLeft ); * result.giveHint( enum.CityNamesAreRight ); * @endcode * @see Hint */ AllFeatures = AutoPublish | AutoDecodeHtmlEntities | AutoRemoveCityFromStopNames, /**< All available features are enabled. */ DefaultFeatures = AutoPublish | AutoDecodeHtmlEntities /**< The default set of features gets enabled. */ }; Q_DECLARE_FLAGS( Features, Feature ) /** * @brief Can be used by scripts to give hints to the data engine. * * The meta object of ResultObject gets published to scripts as @b enum and contains this * enumeration. * * @see giveHint() * @see isHintGiven() **/ enum Hint { NoHint = 0x00, /**< No hints given. */ DatesNeedAdjustment = 0x01, /**< Dates are set from today, not the requested date. They * need to be adjusted by X days, where X is the difference in days between today * and the requested date. */ NoDelaysForStop = 0x02, /**< Delays are not available for the current stop, although * delays are available for other stops. */ CityNamesAreLeft = 0x04, /**< City names are most likely on the left of stop names. */ CityNamesAreRight = 0x08 /**< City names are most likely on the right of stop names. */ }; Q_DECLARE_FLAGS( Hints, Hint ) /** * @brief Creates a new ResultObject instance. **/ ResultObject( QObject* parent = 0 ); virtual ~ResultObject(); /** * @brief Get the list of stored TimetableData objects. * * @return The list of stored TimetableData objects. **/ QList< TimetableData > data() const; Q_INVOKABLE inline QVariant data( int index, int information ) const { return data( index, static_cast(information) ); }; Q_INVOKABLE QVariant data( int index, Enums::TimetableInformation information ) const; /** * @brief Checks whether or not the list of TimetableData objects is empty. * * @return @c True, if the list of TimetableData objects isn't empty. @c False, otherwise. **/ Q_INVOKABLE bool hasData() const; /** * @brief Returns the number of timetable elements currently in the resultset. **/ Q_INVOKABLE int count() const; /** * @brief Whether or not @p feature is enabled. * * Script example: * @code * if ( result.isFeatureEnabled(enum.AutoPublish) ) { * // Do something when the AutoPublish feature is enabled * } * @endcode * * By default all features are enabled. * * @param feature The feature to check. Scripts can access the Feature enumeration * under the name "enum". * * @see Feature **/ Q_INVOKABLE bool isFeatureEnabled( Feature feature ) const; /** * @brief Set whether or not @p feature is @p enabled. * * Script example: * @code * // Disable the AutoPublish feature * result.enableFeature( enum.AutoPublish, false ); * @endcode * * By default all features are enabled, disable unneeded features for better performance. * * @param feature The feature to enable/disable. Scripts can access the Feature enumeration * under the name "enum". * @param enable @c True to enable @p feature, @c false to disable it. * * @see Feature **/ Q_INVOKABLE void enableFeature( Feature feature, bool enable = true ); /** * @brief Test if the given @p hint is set. * * Script example: * @code * if ( result.isHintGiven(enum.CityNamesAreLeft) ) { * // Do something when the CityNamesAreLeft hint is given * } * @endcode * * By default no hints are set. * * @param hint The hint to check. Scripts can access the Hint enumeration * under the name "enum". */ Q_INVOKABLE bool isHintGiven( Hint hint ) const; /** * @brief Set the given @p hint to @p enable. * * Script example: * @code * // Remove the CityNamesAreLeft hint * result.giveHint( enum.CityNamesAreLeft, false ); * @endcode * * By default no hints are set. * * @param hint The hint to give. * @param enable Whether the @p hint should be set or unset. */ Q_INVOKABLE void giveHint( Hint hint, bool enable = true ); /** * @brief Get the currently enabled features. * * Scripts can access the Features enumeration like @verbatimenum.AutoPublish@endverbatim. * By default this equals to DefaultFeatures. */ Features features() const; /** * @brief Get the currently set hints. * * Scripts can access the Hints enumeration like @verbatimenum.CityNamesAreLeft@endverbatim. * By default this equals to NoHints. */ Hints hints() const; static void dataList( const QList< TimetableData > &dataList, PublicTransportInfoList *infoList, ParseDocumentMode parseMode, Enums::VehicleType defaultVehicleType, const GlobalTimetableInfo *globalInfo, ResultObject::Features features, ResultObject::Hints hints ); Q_SIGNALS: /** * @brief Can be called by scripts to trigger the data engine to publish collected data. * * This does not need to be called by scripts, the data engine will publish all collected data, * when the script returns and all network requests are finished. After the first ten items * have been added, this signal is emitted automatically, if the AutoPublish feature is * enabled (the default). Use @verbatimresult.enableFeature(AutoPublish, false)@endverbatim to * disable this feature. * * If collecting data takes too long, calling this signal causes the data collected so far * to be published immediately. Good reasons to call this signal are eg. because additional * documents need to be downloaded or because a very big document gets parsed. Visualizations * connected to the data engine will then receive data not completely at once, but step by * step. * * It also means that the first data items are published to visualizations faster. A good idea * could be to only call publish() after the first few data items (similar to the AutoPublish * feature). That way visualizations get the first dataset very quickly, eg. the data that * fits into the current view. Remaining data will then be added after the script is finished. * * @note Do not call publish() too often, because it causes some overhead. Visualizations * will get notified about the updated data source and process it at whole, ie. not only * newly published items but also the already published items again. Publishing data in * groups of less than ten items will be too much in most situations. But if eg. another * document needs to be downloaded to make more data available, it is a good idea to call * publish() before starting the download (even with less than ten items). * Use count() to see how many items are collected so far. * * @see Feature * @see setFeatureEnabled **/ void publish(); /** * @brief Emitted when invalid data gets received through the addData() method. * * @param info The TimetableInformation which was invalid in @p map. * @param errorMessage An error message explaining why the data for @p info in @p map * is invalid. * @param context The script context from which addData() was called. * @param index The target index at which the data in @p map will be inserted into this result * object. * @param map The argument for addData(), which contained invalid data. **/ void invalidDataReceived( Enums::TimetableInformation info, const QString &errorMessage, const QScriptContextInfo &context, int index, const QVariantMap& map ); public Q_SLOTS: /** * @brief Clears the list of stored TimetableData objects. **/ void clear(); /** * @brief Add @p timetableItem to the result set. * * This can be data for departures, arrivals, journeys, stop suggestions or additional data. * See Enums::TimetableInformation for a list of the property names to use. * * @code * result.addData( {DepartureDateTime: new Date(), Target: 'Test'} ); * @endcode * * A predefined object can also be added like this: * @code * var departure = { DepartureDateTime: new Date() }; * * // Use "Target" as property name * departure.Target = 'Test'; * * // Alternative: Use enumerable value * departure[ PublicTransport.Target ] = 'Test'; * * result.addData( departure ); * @endcode * * @param timetableItem A script object with data for a timetable item. Contains data in a set * of properties, eg. the departure date and time for a departure gets stored as * DepartureDateTime. All available properties can be found in Enums::TimetableInformation. **/ void addData( const QVariantMap &timetableItem ); // TODO // /** @brief Whether or not @p info is contained in this TimetableData object. */ // bool contains( TimetableInformation info ) const { return m_timetableData.contains(info); }; private: QList< TimetableData > m_timetableData; // Protect data from concurrent access by the script in a separate thread and usage in C++ QMutex *m_mutex; Features m_features; Hints m_hints; }; /** \} */ // @ingroup scriptApi Q_DECLARE_OPERATORS_FOR_FLAGS( ResultObject::Features ) Q_DECLARE_OPERATORS_FOR_FLAGS( ResultObject::Hints ) class StoragePrivate; /** @ingroup scriptApi * @{ */ /** * @brief Used by scripts to store data between calls. * * An object of this type gets created for each script (not for each call to the script). * The data in the storage is therefore shared between calls to a script, but not between * scripts for different service providers. * * This class distinguishes between memory storage and persistent storage, ie. on disk. * Both storage types are protected by separate QReadWriteLock's, because scripts may run in * parallel. * * @code * // Write a single value and read it again * storage.write( "name1", 123 ); * var name1 = storage.read( "name1" ); // name1 == 123 * * // Write an object with multiple values at once and read it again * var object = { value1: 445, value2: "test", * otherValue: new Date() }; * storage.write( object ); * var readObject = storage.read(); * // The object read from storage now contains both the single * // value and the values of the object * // readObject == { name1: 123, value1: 445, value2: "test", * // otherValue: new Date() }; * * var value2 = storage.read( "value2" ); // value2 == "test" * var other = storage.read( "other", 555 ); // other == 555, the default value * storage.remove( "name1" ); // Remove a value from the storage * storage.clear(); * @endcode * * * After the service provider plugin has been reloaded (eg. after a restart), the values stored * like shown above are gone. To write data persistently to disk use readPersistent(), * writePersistent() and removePersistent(). Persistently stored data has a lifetime which can be * specified as argument to writePersistent() and defaults to one week. * The maximum lifetime is one month. * * @code * // Write a single value persistently and read it again * storage.writePersistent( "name1", 123 ); // Using the default lifetime * var name1 = storage.readPersistent( "name1" ); // name1 == 123 * * // Write an object with multiple values at once and read it again * var object = { value1: 445, value2: "test", otherValue: new Date() }; * storage.writePersistent( object ); * var readObject = storage.readPersistent(); * // The object read from storage now contains both the single * // value and the values of the object * // readObject == { name1: 123, value1: 445, value2: "test", * // otherValue: new Date() }; * * // Using custom lifetimes (in days) * // 1. Store value 66 for 30 days as "longNeeded" * storage.writePersistent( "longNeeded", 66, 30 ); * // 2. Lifetime can't be higher than 30 days * storage.writePersistent( "veryLongNeeded", 66, 300 ); * * // Check the remaining lifetime of a persistently stored value * var lifetimeLongNeeded = storage.lifetime( "longNeeded" ); // Now 30 * var other = storage.readPersistent( "other", 555 ); // other == 555, default * storage.removePersistent( "name1" ); // Remove a value from the storage * storage.clearPersistent(); * @endcode * * @warning Since the script can run multiple times simultanously in different threads which share * the same Storage object, the stored values are also shared. If you want to store a value for * the current job of the script only (eg. getting departures and remember a value after an * asynchronous request), you should store the value in a global script variable instead. * Otherwise one departure request job might use the value stored by another one, which is * probably not what you want. Scripts can not not access the Storage object of other scripts * (for other service providers). **/ class Storage : public QObject { Q_OBJECT public: /** * @brief Create a new Storage instance. * * @param serviceProviderId Used to find the right place in the service provider cache file * to read/write persistent data. * @param parent The parent QObject. **/ Storage( const QString &serviceProviderId, QObject *parent = 0 ); /** @brief Destructor. */ virtual ~Storage(); /** * @brief The maximum lifetime in days for data written to disk. * @see writePersistent() **/ static const uint MAX_LIFETIME = 30; /** * @brief The default lifetime in days for data written to disk. * @see writePersistent() **/ static const uint DEFAULT_LIFETIME = 7; /** * @brief The suffix to use for lifetime data entries. * * This suffix gets appended to the name with which data gets written persistently to disk * to get the name of the data entry storing the associated lifetime information. * @see writePersistent() **/ static const char* LIFETIME_ENTRYNAME_SUFFIX; /** * @brief The minimal interval in minutes to run checkLifetime(). * @see checkLifetime() **/ static const int MIN_LIFETIME_CHECK_INTERVAL = 15; /** * @brief Whether or not a data entry with @p name exists in memory. **/ Q_INVOKABLE bool hasData( const QString &name ) const; /** * @brief Whether or not a data entry with @p name exists in persistent memory. **/ Q_INVOKABLE bool hasPersistentData( const QString &name ) const; /** * @brief Reads all data stored in memory. **/ Q_INVOKABLE QVariantMap read(); /** * @brief Reads data stored in memory with @p name. **/ Q_INVOKABLE QVariant read( const QString &name, const QVariant& defaultData = QVariant() ); /** * @brief Reads the lifetime remaining for data written using writePersistent() with @p name. **/ Q_INVOKABLE int lifetime( const QString &name ); /** * @brief Reads data stored on disk with @p name. * * @param name The name of the value to read. * @param defaultData The value to return if no stored value was found under @p name. * If you use another default value than the default invalid QVariant, the type must match * the type of the stored value. Otherwise an invalid QVariant gets returned. * @see lifetime() **/ Q_INVOKABLE QVariant readPersistent( const QString &name, const QVariant& defaultData = QVariant() ); /** * @brief Checks the lifetime of all persistent data entries. * * If the lifetime of a persistent data entry has expired, it gets deleted. **/ void checkLifetime(); public Q_SLOTS: /** * @brief Stores @p data in memory with @p name. **/ void write( const QString &name, const QVariant &data ); /** * @brief Stores @p data in memory, eg. a script object. * * @param data The data to write to disk. This can be a script object. * @overload **/ void write( const QVariantMap &data ); /** * @brief Removes data stored in memory with @p name. **/ void remove( const QString &name ); /** * @brief Clears all data stored in memory. **/ void clear(); /** * @brief Stores @p data on disk with @p name. * * @param name A name to access the written data with. * @param data The data to write to disk. The type of the data can also be QVariantMap (ie. * script objects) or list types (gets encoded to QByteArray). Length of the data is limited * to 65535 bytes. * @param lifetime The lifetime in days of the data. Limited to 30 days and defaults to 7 days. * * @see lifetime **/ void writePersistent( const QString &name, const QVariant &data, uint lifetime = DEFAULT_LIFETIME ); /** * @brief Stores @p data on disk, eg. a script object. * * After @p lifetime days have passed, the written data will be deleted automatically. * To prevent automatic deletion the data has to be written again. * * @param data The data to write to disk. This can be a script object. * @param lifetime The lifetime in days of each entry in @p data. * Limited to 30 days and defaults to 7 days. * * @see lifetime * @overload **/ void writePersistent( const QVariantMap &data, uint lifetime = DEFAULT_LIFETIME ); /** * @brief Removes data stored on disk with @p name. * * @note Scripts do not need to remove data written persistently, ie. to disk, because each * data entry has a lifetime, which is currently limited to 30 days and defaults to 7 days. **/ void removePersistent( const QString &name ); /** * @brief Clears all data stored persistently, ie. on disk. * * @note Scripts do not need to remove data written persistently, ie. to disk, because each * data entry has a lifetime, which is currently limited to 30 days and defaults to 7 days. **/ void clearPersistent(); private: int lifetime( const QString &name, const KConfigGroup &group ); int lifetimeNoLock( const QString &name, const KConfigGroup &group ); void removePersistent( const QString &name, KConfigGroup &group ); QByteArray encodeData( const QVariant &data ) const; QVariant decodeData( const QByteArray &data ) const; StoragePrivate *d; }; /** \} */ // @ingroup scriptApi /** @ingroup scriptApi * @{ */ /** * @brief A data stream class to be used in scripts. * * This class gets made available to scripts under the name @c DataStream. * It can be setup to read from @c data that was received from a network request like here: * @code * // data is a QByteArray as received from a network request * // That data can now be decoded using helper.decode(), * // but if it contains binary data DataStream should be used with a QBuffer * var buffer = new QBuffer( data ); * buffer.open( QIODevice.ReadOnly ); * var stream = new DataStream( buffer ); * // The stream is now ready * * // Seek to position 10, if possible * if ( 10 < buffer.size() ) { * stream.seek( 10 ); * } * * // The current position is available in the 'pos' property * var pos = stream.pos; * * // Skip some bytes * stream.skip( 2 ); // Skip two bytes, equal to stream.seek(stream.pos + 2) * * // Read data * var number = stream.readUInt32(); // Read a four byte unsigned integer * var smallNumber = stream.readInt8(); // Read a one byte integer * var string = stream.readString(); // Read a string, ends at the first found \0 * * // Close the buffer again * buffer.close(); * @endcode * * This class wraps a QDataStream, because it's read/write operators cannot be used from QtScript * otherwise, ie. the operator>>(), operator<<() functions. This class offers some read functions * to make this possible, ie readInt8(), readUInt8() or readString(). To read byte data use * readBytes(). **/ class DataStreamPrototype: public QObject, protected QScriptable { Q_OBJECT Q_PROPERTY( quint64 pos READ pos WRITE seek ) Q_PROPERTY( bool atEnd READ atEnd ) public: DataStreamPrototype( QObject *parent = 0 ); DataStreamPrototype( const QByteArray &byteArray, QObject *parent = 0 ); DataStreamPrototype( QIODevice *device, QObject *parent = 0 ); DataStreamPrototype( DataStreamPrototype *other ); /** @brief Reads an 8 bit integer. */ Q_INVOKABLE qint8 readInt8(); /** @brief Reads an unsigned 8 bit integer. */ Q_INVOKABLE quint8 readUInt8(); /** @brief Reads a 16 bit integer. */ Q_INVOKABLE qint16 readInt16(); /** @brief Reads an unsigned 16 bit integer. */ Q_INVOKABLE quint16 readUInt16(); /** @brief Reads a 32 bit integer. */ Q_INVOKABLE qint32 readInt32(); /** @brief Reads an unsigned 32 bit integer. */ Q_INVOKABLE quint32 readUInt32(); /** @brief Reads a string until the first \0 character is read. */ Q_INVOKABLE QString readString(); /** @brief Reads bytes until the first \0 character is read. */ Q_INVOKABLE QByteArray readBytesUntilZero(); /** @brief Reads @p bytes bytes. */ Q_INVOKABLE QByteArray readBytes( uint bytes ); /** @brief Whether or not the stream is at it's end. */ Q_INVOKABLE bool atEnd() const { return m_dataStream->atEnd(); }; /** @brief Get the current position in the streams device. */ Q_INVOKABLE quint64 pos() const { return m_dataStream->device()->pos(); }; /** @brief Seek to @p pos in the streams device. */ Q_INVOKABLE bool seek( quint64 pos ) const { return m_dataStream->device()->seek(pos); }; /** @brief Peek data with the given @p maxLength. */ Q_INVOKABLE QByteArray peek( quint64 maxLength ) const { return m_dataStream->device()->peek(maxLength); }; /** @brief Skip @p bytes bytes. */ Q_INVOKABLE int skip( int bytes ) const { return m_dataStream->skipRawData(bytes); }; /** @brief Return a human-readable description of the last device error that occured. */ Q_INVOKABLE QString errorString() const { return m_dataStream->device()->errorString(); }; /** @brief Get the underlying QDataStream as a QSharedPointer. */ QSharedPointer< QDataStream > stream() const { return m_dataStream; }; private: QDataStream *thisDataStream() const; QSharedPointer< QDataStream > m_dataStream; }; /** \} */ // @ingroup scriptApi typedef DataStreamPrototype* DataStreamPrototypePtr; QScriptValue constructStream( QScriptContext *context, QScriptEngine *engine ); QScriptValue dataStreamToScript( QScriptEngine *engine, const DataStreamPrototypePtr &stream ); void dataStreamFromScript( const QScriptValue &object, DataStreamPrototypePtr &stream ); } // namespace ScriptApi Q_DECLARE_METATYPE(ScriptApi::Helper::ErrorSeverity) Q_DECLARE_METATYPE(ScriptApi::ResultObject::Hint) Q_DECLARE_METATYPE(ScriptApi::ResultObject::Hints) Q_DECLARE_METATYPE(ScriptApi::ResultObject::Feature) Q_DECLARE_METATYPE(ScriptApi::ResultObject::Features) Q_DECLARE_METATYPE(ScriptApi::NetworkRequest*) Q_DECLARE_METATYPE(ScriptApi::NetworkRequest::Ptr) Q_SCRIPT_DECLARE_QMETAOBJECT(ScriptApi::NetworkRequest, QObject*) Q_DECLARE_METATYPE(QIODevice*) Q_DECLARE_METATYPE(QDataStream*) Q_DECLARE_METATYPE(ScriptApi::DataStreamPrototype*) Q_SCRIPT_DECLARE_QMETAOBJECT(ScriptApi::DataStreamPrototype, QObject*) #endif // Multiple inclusion guard diff --git a/engine/serviceproviderdata.cpp b/engine/serviceproviderdata.cpp index 22de2a1..6614d0e 100644 --- a/engine/serviceproviderdata.cpp +++ b/engine/serviceproviderdata.cpp @@ -1,373 +1,373 @@ /* * Copyright 2012 Friedrich Pülz * * 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 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "serviceproviderdata.h" #include "serviceproviderglobal.h" // KDE includes -#include +#include +#include +#include #include -#include -#include // Qt includes #include class ChangelogEntryGreaterThan { public: inline bool operator()( const ChangelogEntry& l, const ChangelogEntry& r ) const { int comparison = ServiceProviderData::compareVersions( l.version, r.version ); if ( comparison == 0 ) { // Versions are equal, compare authors return l.author.compare( r.author, Qt::CaseInsensitive ) < 0; } else { // If comparison is 1, the left version is bigger than the right one, // if it is -1, the right one is bigger return comparison > 0; } }; }; ServiceProviderData::ServiceProviderData( const Enums::ServiceProviderType& type, const QString& id, QObject *parent ) : QObject(parent) { m_serviceProviderType = type; m_id = id; m_version = "1.0"; // Initial version of new provider plugins m_fileFormatVersion = "1.1"; // Current version of the file structure of the .pts-file m_useSeparateCityValue = false; m_onlyUseCitiesInList = false; m_defaultVehicleType = Enums::UnknownVehicleType; m_minFetchWait = 0; m_sampleLongitude = m_sampleLatitude = 0.0; } ServiceProviderData::ServiceProviderData( const Enums::ServiceProviderType &type, const QString &id, const QHash< QString, QString > &names, const QHash< QString, QString >& descriptions, const QString &version, const QString &fileVersion, bool useSeparateCityValue, bool onlyUseCitiesInList, const QString &url, const QString &shortUrl, int minFetchWait, const QString &author, const QString &email, Enums::VehicleType defaultVehicleType, const QList< ChangelogEntry >& changelog, const QStringList &cities, const QHash< QString, QString >& cityNameToValueReplacementHash, QObject *parent ) : QObject(parent) { m_serviceProviderType = type; m_id = id; m_name = names; m_description = descriptions; m_version = version; m_fileFormatVersion = fileVersion; m_useSeparateCityValue = useSeparateCityValue; m_onlyUseCitiesInList = onlyUseCitiesInList; m_url = url; m_shortUrl = shortUrl; m_minFetchWait = minFetchWait; m_author = author; m_email = email; m_defaultVehicleType = defaultVehicleType; m_changelog = changelog; m_cities = cities; m_hashCityNameToValue = cityNameToValueReplacementHash; m_sampleLongitude = m_sampleLatitude = 0.0; } ServiceProviderData::ServiceProviderData( const ServiceProviderData &data, QObject *parent ) : QObject(parent) { // Use assignment operator for initialization operator=( data ); } ServiceProviderData::~ServiceProviderData() { } ServiceProviderData &ServiceProviderData::operator=( const ServiceProviderData &data ) { m_serviceProviderType = data.m_serviceProviderType; m_id = data.m_id; m_name = data.m_name; m_description = data.m_description; m_version = data.m_version; m_fileFormatVersion = data.m_fileFormatVersion; m_useSeparateCityValue = data.m_useSeparateCityValue; m_onlyUseCitiesInList = data.m_onlyUseCitiesInList; m_url = data.m_url; m_shortUrl = data.m_shortUrl; m_minFetchWait = data.m_minFetchWait; m_author = data.m_author; m_shortAuthor = data.m_shortAuthor; m_email = data.m_email; m_defaultVehicleType = data.m_defaultVehicleType; m_changelog = data.m_changelog; m_country = data.m_country; m_cities = data.m_cities; m_credit = data.m_credit; m_hashCityNameToValue = data.m_hashCityNameToValue; m_fileName = data.m_fileName; m_charsetForUrlEncoding = data.m_charsetForUrlEncoding; m_fallbackCharset = data.m_fallbackCharset; m_sampleStopNames = data.m_sampleStopNames; m_sampleCity = data.m_sampleCity; m_sampleLongitude = data.m_sampleLongitude; m_sampleLatitude = data.m_sampleLatitude; m_notes = data.m_notes; // For ScriptedProvider m_scriptFileName = data.m_scriptFileName; m_scriptExtensions = data.m_scriptExtensions; // For GtfsProvider m_feedUrl = data.m_feedUrl; m_tripUpdatesUrl = data.m_tripUpdatesUrl; m_alertsUrl = data.m_alertsUrl; m_timeZone = data.m_timeZone; return *this; } bool ServiceProviderData::operator ==( const ServiceProviderData &data ) const { return m_serviceProviderType == data.m_serviceProviderType && m_id == data.m_id && m_name == data.m_name && m_description == data.m_description && m_version == data.m_version && m_fileFormatVersion == data.m_fileFormatVersion && m_useSeparateCityValue == data.m_useSeparateCityValue && m_onlyUseCitiesInList == data.m_onlyUseCitiesInList && m_url == data.m_url && m_shortUrl == data.m_shortUrl && m_minFetchWait == data.m_minFetchWait && m_author == data.m_author && m_shortAuthor == data.m_shortAuthor && m_email == data.m_email && m_defaultVehicleType == data.m_defaultVehicleType && m_changelog == data.m_changelog && m_country == data.m_country && m_cities == data.m_cities && m_credit == data.m_credit && m_hashCityNameToValue == data.m_hashCityNameToValue && m_fileName == data.m_fileName && m_charsetForUrlEncoding == data.m_charsetForUrlEncoding && m_fallbackCharset == data.m_fallbackCharset && m_sampleStopNames == data.m_sampleStopNames && m_sampleCity == data.m_sampleCity && m_sampleLongitude == data.m_sampleLongitude && m_sampleLatitude == data.m_sampleLatitude && m_notes == data.m_notes && // For ScriptedProvider m_scriptFileName == data.m_scriptFileName && m_scriptExtensions == data.m_scriptExtensions && // For GtfsProvider m_feedUrl == data.m_feedUrl && m_tripUpdatesUrl == data.m_tripUpdatesUrl && m_alertsUrl == data.m_alertsUrl && m_timeZone == data.m_timeZone; } void ServiceProviderData::finish() { // Generate a short URL if none is given if ( m_shortUrl.isEmpty() ) { m_shortUrl = shortUrlFromUrl( m_url ); } // Generate a short author name if none is given if ( m_shortAuthor.isEmpty() && !m_author.isEmpty() ) { m_shortAuthor = shortAuthorFromAuthor( m_author ); } // Use script author as author of the change entry if no one else was set for ( int i = 0; i < m_changelog.count(); ++i ) { if ( m_changelog[i].author.isEmpty() ) { m_changelog[i].author = m_shortAuthor; } } qStableSort( m_changelog.begin(), m_changelog.end(), ChangelogEntryGreaterThan() ); } QString ServiceProviderData::typeString() const { return ServiceProviderGlobal::typeToString( m_serviceProviderType ); } QString ServiceProviderData::typeName( ) const { return ServiceProviderGlobal::typeName( m_serviceProviderType ); } int ServiceProviderData::versionNumberFromString( const QString &version, int *startPos ) { bool ok; int versionNumber; int nextPointPos = version.indexOf( '.', *startPos ); if ( nextPointPos <= 0 ) { // No point found in version after startPos, get last number until the end of the string versionNumber = version.mid( *startPos ).toInt( &ok ); *startPos = -1; } else { // Found a point after startPos in version, extract the number from version versionNumber = version.mid( *startPos, nextPointPos - *startPos ).toInt( &ok ); *startPos = nextPointPos + 1; } if ( ok ) { return versionNumber; } else { kDebug() << "Version is invalid:" << version; return -1; } } int ServiceProviderData::compareVersions( const QString &version1, const QString &version2 ) { int pos1 = 0; int pos2 = 0; forever { int versionNumber1 = versionNumberFromString( version1, &pos1 ); int versionNumber2 = versionNumberFromString( version2, &pos2 ); if ( versionNumber1 < 0 || versionNumber2 < 0 ) { // Invalid version strings return 0; } if ( versionNumber1 < versionNumber2 ) { return -1; // Version 1 is smaller than version 2 } else if ( versionNumber1 > versionNumber2 ) { return 1; // Version 1 is bigger than version 2 } if ( pos1 == -1 && pos2 == -1 ) { return 0; // No more version numbers in both version } else if ( pos1 == -1 ) { return -1; // No more version numbers in version1, but in version2, which is therefore bigger } else if ( pos2 == -1 ) { return 1; // No more version numbers in version2, but in version1, which is therefore bigger } // pos1 and pos2 are both >= 0 here, // they are set behind the '.' after the just read version numbers, // this makes the next iteration read the next version number } return 0; } QString ServiceProviderData::shortUrlFromUrl( const QString &url ) { QString shortUrl = QUrl( url ).toString( QUrl::RemoveScheme | QUrl::RemovePort | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment | QUrl::StripTrailingSlash ); while ( shortUrl.startsWith('/') ) { shortUrl = shortUrl.mid( 1 ); } return shortUrl; } QString ServiceProviderData::shortAuthorFromAuthor( const QString &authorName ) { const QStringList names = authorName.toLower().split( ' ', QString::SkipEmptyParts ); if ( !names.isEmpty() ) { // Add first character of all prenames QString shortAuthor; for ( int i = 0; i < names.count() - 1; ++i ) { shortAuthor += names[i][0]; } // Add family name completely shortAuthor += names.last(); return shortAuthor.replace( QString::fromUtf8("ü"), QLatin1String("ue") ) .replace( QString::fromUtf8("ö"), QLatin1String("oe") ) .replace( QString::fromUtf8("ä"), QLatin1String("ae") ) .replace( QString::fromUtf8("Ü"), QLatin1String("Ue") ) .replace( QString::fromUtf8("Ö"), QLatin1String("Oe") ) .replace( QString::fromUtf8("Ä"), QLatin1String("Ae") ) .replace( QString::fromUtf8("ß"), QLatin1String("ss") ); } else { return QString(); } } QString ServiceProviderData::name() const { const QString lang = KGlobal::locale()->country(); return m_name.contains(lang) ? m_name[lang] : m_name["en"]; } QString ServiceProviderData::description() const { const QString lang = KGlobal::locale()->country(); return m_description.contains(lang) ? m_description[lang] : m_description["en"]; } QString ServiceProviderData::notes() const { return m_notes; } void ServiceProviderData::setUrl( const QString &url, const QString &shortUrl ) { m_url = url; m_shortUrl = shortUrl.isEmpty() ? shortUrlFromUrl(url) : shortUrl; } void ServiceProviderData::setAuthor( const QString& author, const QString &shortAuthor, const QString &email ) { m_author = author; m_shortAuthor = shortAuthor; m_email = email; } void ServiceProviderData::setFileName(const QString& fileName) { m_fileName = KStandardDirs::realFilePath( fileName ); } QString ServiceProviderData::mapCityNameToValue( const QString &city ) const { if ( m_hashCityNameToValue.contains( city.toLower() ) ) { return m_hashCityNameToValue[city.toLower()]; } else { return city; } } QString ServiceProviderData::changelogString() const { QString changelog; foreach ( const ChangelogEntry &entry, m_changelog ) { if ( !changelog.isEmpty() ) { changelog.append( '\n' ); } changelog.append( entry.version ); if ( !entry.author.isEmpty() ) { changelog.append( " (" + entry.author + ')' ); } changelog.append( ": " + entry.description ); } return changelog; } diff --git a/engine/timetableservice.cpp b/engine/timetableservice.cpp index c44623d..a4bcc42 100644 --- a/engine/timetableservice.cpp +++ b/engine/timetableservice.cpp @@ -1,219 +1,220 @@ /* * Copyright 2013 Friedrich Pülz * * 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 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Own includes #include "timetableservice.h" // KDE includes #include +#include // Qt incluse #include #include #include RequestAdditionalDataJob::RequestAdditionalDataJob( Plasma::DataEngine *engine, const QString &destination, const QString &operation, const QMap< QString, QVariant > ¶meters, QObject *parent ) : TimetableServiceJob(engine, destination, operation, parameters, parent), m_itemsDone(0), m_itemsFailed(0) { if ( parameters.contains("itemnumberbegin") && parameters.contains("itemnumberend") ) { m_updateItem = parameters["itemnumberbegin"].toInt(); m_updateItemEnd = parameters["itemnumberend"].toInt(); Q_ASSERT( m_updateItemEnd >= m_updateItem ); } else { m_updateItem = m_updateItemEnd = parameters["itemnumber"].toInt(); } } void RequestAdditionalDataJob::start() { // Find the slot of the engine to start the request const QMetaObject *meta = m_engine->metaObject(); const int slotIndex = meta->indexOfSlot( "requestAdditionalData(QString,int,int)" ); Q_ASSERT( slotIndex != -1 ); // Connect to the finished signal of the engine for finished additional data requests // NOTE This signal is emitted for each finished request, not for all requests, // if there are multiple requests connect( m_engine, SIGNAL(additionalDataRequestFinished(QString,int,bool,QString)), this, SLOT(additionalDataRequestFinished(QString,int,bool,QString)) ); // Invoke the slot to request additional data for each item meta->method( slotIndex ).invoke( m_engine, Qt::QueuedConnection, Q_ARG(QString, destination()), Q_ARG(int, m_updateItem), Q_ARG(int, m_updateItemEnd - m_updateItem + 1) ); } void RequestAdditionalDataJob::additionalDataRequestFinished( const QString &sourceName, int item, bool success, const QString &errorMessage ) { if ( sourceName != destination() || item < m_updateItem || item > m_updateItemEnd ) { // The finished() signal from the data engine was emitted for another service job return; } ++m_itemsDone; if ( !success ) { ++m_itemsFailed; if ( m_errorMessage.isEmpty() ) { m_errorMessage = errorMessage; } } const int totalItemsToUpdate = m_updateItemEnd - m_updateItem + 1; const int itemsFailed = m_itemsFailed; const bool isFinished = m_itemsDone == totalItemsToUpdate; const long unsigned int percent = qCeil((100 * m_itemsDone) / qreal(totalItemsToUpdate)); QString firstErrorMessage = m_errorMessage; Q_ASSERT( totalItemsToUpdate > 0 ); // Set the current percentage of requests that are done setPercent( percent ); if ( isFinished ) { // Last item is done set error message, if any if ( itemsFailed >= 1 ) { // Use a simple error message if multiple items were requested, otherwise use the error // message for the failed item. The error messages for all failed items are set in the // target data source in "additionalDataError" fields. if ( totalItemsToUpdate > 1 ) { firstErrorMessage = i18nc("@info/plain", "%1 of %2 items failed", itemsFailed, totalItemsToUpdate); } setError( KJob::UserDefinedError ); setErrorText( firstErrorMessage ); } // Set the result and emit finished(). This should only be done when the job is really // finished, it. there should be no more requests of multiple requests to be done for // this job. It will not crash in the service // NOTE The finished() signal may be connected to the deleteLater() slot of the service, // which is the parent of this job, ie. this may delete this job (later) setResult( success ); } } UpdateRequestJob::UpdateRequestJob( Plasma::DataEngine *engine, const QString &destination, const QString &operation, const QMap< QString, QVariant > ¶meters, QObject *parent ) : TimetableServiceJob( engine, destination, operation, parameters, parent ) { } void UpdateRequestJob::start() { // Find the slot of the engine to start the request const QMetaObject *meta = m_engine->metaObject(); const int slotIndex = meta->indexOfMethod( "requestUpdate(QString)" ); Q_ASSERT( slotIndex != -1 ); // Connect to the finished signal of the engine for finished update requests connect( m_engine, SIGNAL(updateRequestFinished(QString,bool,QString)), this, SLOT(updateRequestFinished(QString,bool,QString)) ); // Invoke the slot to request the update meta->method( slotIndex ).invoke( m_engine, Qt::QueuedConnection, Q_ARG(QString, destination()) ); } void UpdateRequestJob::updateRequestFinished( const QString &sourceName, bool success, const QString &errorMessage) { if ( sourceName != destination() ) { // The finished() signal from the data engine was emitted for another service job return; } setResult( success ); if ( !success ) { setError( TimetableService::UnknownError ); setErrorText( errorMessage ); } } RequestMoreItemsJob::RequestMoreItemsJob( Plasma::DataEngine *engine, const QString &destination, Enums::MoreItemsDirection direction, const QString &operation, const QMap< QString, QVariant > ¶meters, QObject *parent ) : TimetableServiceJob(engine, destination, operation, parameters, parent), m_direction(direction) { qRegisterMetaType< Enums::MoreItemsDirection >( "Enums::MoreItemsDirection" ); } void RequestMoreItemsJob::start() { // Find the slot of the engine to start the request const QMetaObject *meta = m_engine->metaObject(); const int slotIndex = meta->indexOfMethod( "requestMoreItems(QString,Enums::MoreItemsDirection)" ); Q_ASSERT( slotIndex != -1 ); // Connect to the finished signal of the engine for finished requests for more items connect( m_engine, SIGNAL(moreItemsRequestFinished(QString,Enums::MoreItemsDirection,bool,QString)), this, SLOT(moreItemsRequestFinished(QString,Enums::MoreItemsDirection,bool,QString)) ); // Invoke the slot to request more items meta->method( slotIndex ).invoke( m_engine, Qt::QueuedConnection, Q_ARG(QString, destination()), Q_ARG(Enums::MoreItemsDirection, m_direction) ); } void RequestMoreItemsJob::moreItemsRequestFinished( const QString &sourceName, Enums::MoreItemsDirection direction, bool success, const QString &errorMessage) { if ( sourceName != destination() || direction == m_direction ) { // The finished() signal from the data engine was emitted for another service job return; } if ( !success ) { setError( TimetableService::UnknownError ); setErrorText( errorMessage ); } setResult( success ); } TimetableService::TimetableService( Plasma::DataEngine *engine, const QString &name, QObject *parent ) : Service(parent), m_engine(engine) { // This associates the service with the "timetable.operations" file setName( "timetable" ); setDestination( name ); } Plasma::ServiceJob* TimetableService::createJob( const QString &operation, QMap< QString, QVariant > ¶meters ) { if ( operation == "requestAdditionalData" || operation == "requestAdditionalDataRange" ) { return new RequestAdditionalDataJob( m_engine, destination(), operation, parameters, this ); } else if ( operation == "requestUpdate" ) { return new UpdateRequestJob( m_engine, destination(), operation, parameters, this ); } else if ( operation == "requestEarlierItems" ) { return new RequestMoreItemsJob( m_engine, destination(), Enums::EarlierItems, operation, parameters, this ); } else if ( operation == "requestLaterItems" ) { return new RequestMoreItemsJob( m_engine, destination(), Enums::LaterItems, operation, parameters, this ); } else { qWarning() << "Operation" << operation << "not supported"; return 0; } }