diff --git a/engine/datasource.cpp b/engine/datasource.cpp index a354f1f..99075f1 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 // Qt includes #include #include DataSource::DataSource( const QString &dataSource ) : m_name(dataSource) { } DataSource::~DataSource() { } ProvidersDataSource::ProvidersDataSource( const QString &dataSource, - const QHash &providerData ) + const QMap &providerData ) : DataSource(dataSource), m_dirty(true), m_providerData(providerData) { } -SimpleDataSource::SimpleDataSource( const QString &dataSource, const QVariantHash &data ) +SimpleDataSource::SimpleDataSource( const QString &dataSource, const QVariantMap &data ) : DataSource(dataSource), m_data(data) { } -TimetableDataSource::TimetableDataSource( const QString &dataSource, const QVariantHash &data ) +TimetableDataSource::TimetableDataSource( const QString &dataSource, const QVariantMap &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 ) +ProvidersDataSource::ProviderData::ProviderData( const QVariantMap &data, const QString &_stateId, + const QVariantMap &_stateData ) : dataWithoutState(data), stateId(ProvidersDataSource::toStaticState(_stateId)), stateData(_stateData) { } -QVariantHash ProvidersDataSource::ProviderData::data() const +QVariantMap ProvidersDataSource::ProviderData::data() const { // Combine all provider data with state ID and state data - QVariantHash data = dataWithoutState; + QVariantMap data = dataWithoutState; data[ "state" ] = stateId; data[ "stateData" ] = stateData; return data; } -QVariantHash ProvidersDataSource::data() const +QVariantMap ProvidersDataSource::data() const { - // Combine all provider data QVariantHash's into one - QVariantHash data; - for ( QHash::ConstIterator it = m_providerData.constBegin(); + // Combine all provider data QVariantMap's into one + QVariantMap data; + for ( QMap::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 QVariantMap data = items[i].toMap(); 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 ); + itemHashes << hashForDeparture( item.toMap(), isDeparture ); } // Remove cached additional data for no longer present timetable items - QHash< uint, TimetableData >::Iterator it = m_additionalData.begin(); + QMap< 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(); + QMap::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 +QVariantMap 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 +QVariantMap 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 ) +void ProvidersDataSource::setProviderStateData( const QString &providerId, const QVariantMap &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/datasource.h b/engine/datasource.h index c52779b..b5713ec 100644 --- a/engine/datasource.h +++ b/engine/datasource.h @@ -1,376 +1,376 @@ /* * 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. */ #ifndef DATASOURCEDATA_HEADER #define DATASOURCEDATA_HEADER // Own includes #include "enums.h" #include "request.h" // Qt includes #include class QTimer; /** * @brief Abstract base class for all data sources. * * This class only provides a virtual data() function to get the QVariantHash with the data * of the data source, but it does not store it in a member variable. This gives subclasses the * possibility to store the data in other structures, like ProvidersDataSource does. * SimpleDataSource on the other hand adds a QVariantHash member variable to store the data of the * data source and is used for most data sources. * * @see ProvidersDataSource * @see SimpleDataSource **/ class DataSource { public: /** @brief Create a new data source object for a data source with the given @p sourceName. */ DataSource( const QString &sourceName = QString() ); virtual ~DataSource(); /** @brief The data source name. */ QString name() const { return m_name; }; /** @brief Get data stored for the data source. */ - virtual QVariantHash data() const = 0; + virtual QVariantMap data() const = 0; /** @brief Get data stored for the data source. */ QVariant value( const QString &key ) const { return data()[key]; }; protected: QString m_name; }; /** * @brief Simple data source class, containing data for a data source. * * This class overrides DataSource and adds a QVariantHash member variable to store the data * of the data source. It also adds some functions to give more control over the stored data. **/ class SimpleDataSource : public DataSource { public: /** @brief Create a new data source object for a data source @p sourceName with @p data. */ SimpleDataSource( const QString &sourceName = QString(), - const QVariantHash &data = QVariantHash() ); + const QVariantMap &data = QVariantMap() ); /** @brief Get data stored for the data source. */ - virtual QVariantHash data() const { return m_data; }; + virtual QVariantMap data() const { return m_data; }; /** @brief Clear the data stored for the data source. */ void clear() { m_data.clear(); }; /** @brief Set data stored for the data source. */ - virtual void setData( const QVariantHash &data ) { m_data = data; }; + virtual void setData( const QVariantMap &data ) { m_data = data; }; /** @brief Insert @p value as @p key into the data stored for the data source. */ virtual void setValue( const QString &key, const QVariant &value ) { m_data.insert( key, value ); }; protected: - QVariantHash m_data; + QVariantMap m_data; }; /** * @brief Contains data for service provider data sources. * * The data() function returns the complete data for the "ServiceProviders" data source. * To get data for a single service provider, ie. a "ServiceProvider [providerId]" data source, * use the providerData() function. **/ class ProvidersDataSource : public DataSource { public: /** * @brief Contains data for a single service provider. * * The data is separated into state data and non-state data. Non-state data does not change * as long as the provider plugin sources do not change, ie. the .pts file and eg. script files * for script provider plugins. This data contains values read from the .pts file and other * cached values that require the ServiceProvider object to be created, eg. the list of * supported features. * * State data contains information about the provider plugins current state. The state ID is * a short string identifying the providers state, it can be one of these: "ready", "error", * "gtfs_feed_import_pending" and "importing_gtfs_feed". State data always contains a field * "statusMessage" with a human readable description of the providers state. The may be * different status messages for the same state ID, providing some more information. Depending * on the state ID there may be more fields available in the state data. For example when the * state is "importing_gtfs_feed", there is a "progress" field with the progress percentage * of the import job. **/ struct ProviderData { /** * @brief Construct a new ProviderData object. * @param data Normal data for the provider. * @param stateId The ID of the providers current state. Since ProviderData objects are * only created when there is no operation running for the provider, dynamic states such * as "importing_gtfs_feed" are replaced with static ones using * ProvidersDataSource::toStaticState(). * @param stateData Data for the providers current state. A field 'statusMessage' should * always be contained. **/ - ProviderData( const QVariantHash &data = QVariantHash(), const QString &stateId = QString(), - const QVariantHash &stateData = QVariantHash() ); + ProviderData( const QVariantMap &data = QVariantMap(), const QString &stateId = QString(), + const QVariantMap &stateData = QVariantMap() ); /** @brief Combine provider data with state ID and state data. */ - QVariantHash data() const; + QVariantMap data() const; - QVariantHash dataWithoutState; + QVariantMap dataWithoutState; QString stateId; - QVariantHash stateData; + QVariantMap stateData; }; /** * @brief Create a new data source object for all service provider data sources. **/ ProvidersDataSource( const QString &sourceName = QString(), - const QHash &data = QHash() ); + const QMap &data = QMap() ); /** * @brief Replace dynamic states with static ones. * * Dynamic states are states that will change to another state after some time, * eg. "importing_gtfs_feed". When loading provider states from cache it makes no sense * to use dynamic states, because they are invalid then, most probably they stayed in the * cache after a crash. The operation described by the dynamic state cannot be continued * after a restart. Such dynamic states are replaced with static states here, that can be * used in initialization. * For example "importing_gtfs_feed" gets replaced by "gtfs_feed_import_pending". **/ static QString toStaticState( const QString &dynamicStateId ); /** @brief Add a provider with the given @p providerId and @p providerData. */ void addProvider( const QString &providerId, const ProviderData &providerData ); /** @brief Remove the provider with the given @p providerId. */ void removeProvider( const QString &providerId ); /** @brief Get all provider data, ie. the data for the "ServiceProviders" data source. */ - virtual QVariantHash data() const; + virtual QVariantMap data() const; /** @brief Get data for the provider with the given @p providerId. */ - QVariantHash providerData( const QString &providerId ) const; + QVariantMap providerData( const QString &providerId ) const; /** @brief Get the current state of the provider with the given @p providerId. */ QString providerState( const QString &providerId ) const; /** @brief Get state data of the provider with the given @p providerId. */ - QVariantHash providerStateData( const QString &providerId ) const; + QVariantMap providerStateData( const QString &providerId ) const; /** @brief Set the state of the provider with the given @p providerId to @p stateId. */ void setProviderState( const QString &providerId, const QString &stateId ); /** @brief Set the state data of the provider with the given @p providerId to @p stateData. */ - void setProviderStateData( const QString &providerId, const QVariantHash &stateData ); + void setProviderStateData( const QString &providerId, const QVariantMap &stateData ); /** @brief Set @p stateId and @p stateData for the provider with the given @p providerId. */ inline void setProviderState( const QString &providerId, const QString &stateId, - const QVariantHash &stateData ) + const QVariantMap &stateData ) { setProviderState( providerId, stateId ); setProviderStateData( providerId, stateData ); }; /** * @brief Get a list of providers that have changed since this function was last called. * This function also resets isDirty() to @p false. **/ QStringList takeChangedProviders() { const QStringList changedProviders = m_changedProviders; m_changedProviders.clear(); m_dirty = false; return changedProviders; }; /** @brief Check if there were changes to the providers. */ bool isDirty() const { return m_dirty; }; /** @brief Called when the provider plugin installation directory was changed. */ void providersHaveChanged() { m_dirty = true; }; /** @brief Mark providers that can no longer be found in an installation directory. */ QStringList markUninstalledProviders(); private: bool mayProviderBeNewlyChanged( const QString &providerId ) const { return !m_changedProviders.contains(providerId) && m_providerData.contains(providerId); }; bool m_dirty; QStringList m_changedProviders; - QHash< QString, ProviderData > m_providerData; + QMap< QString, ProviderData > m_providerData; }; /** @brief Contains data for a timetable data source, ie. for departures, arrivals or journeys. */ class TimetableDataSource : public SimpleDataSource { public: /** @brief Create a new timetable data source object for a data source @p sourceName. */ TimetableDataSource( const QString &dataSource = QString(), - const QVariantHash &data = QVariantHash() ); + const QVariantMap &data = QVariantMap() ); virtual ~TimetableDataSource(); /** @brief Remove obsolete cached data, eg. additional data for previous departures. */ void cleanup(); void addUsingDataSource( const QSharedPointer &request, const QString &sourceName, const QDateTime &dateTime, int count ); void removeUsingDataSource( const QString &sourceName ); int usageCount() const { return m_dataSources.count(); }; QStringList usingDataSources() const { return m_dataSources.keys(); }; /** @brief Get the date and time of the last timetable data update. */ QDateTime lastUpdate() const { return !m_data.contains("updated") ? QDateTime() : m_data["updated"].toDateTime(); }; /** @brief Get the ID of the provider used to get the timetable data. */ QString providerId() const { return m_data["serviceProvider"].toString(); }; /** @brief Whether or not this data source always contains timetable items for the same time. */ bool hasConstantTime() const { return m_name.contains(QLatin1String("date=")); }; /** @brief Whether or not this data source had an error in the data engine. */ bool hasError() const { return m_data["error"].toBool(); }; /** @brief Get flags for this data source, see UpdateFlags. */ UpdateFlags updateFlags() const; /** * @brief Get the key under which timetable items are stored in the data source. * @see timetableItems(), setTimetableItems() **/ QString timetableItemKey() const; /** * @brief Get the list of timetable items, which are stored in this data source. * This is equivalent to: * @code value( timetableItemKey() ); @endcode * @see timetableItemKey() **/ QVariantList timetableItems() const { return m_data[ timetableItemKey() ].toList(); }; /** * @brief Set the list of timetable items, which are stored in this data source. * This is equivalent to: * @code setValue( timetableItemKey(), items ); @endcode * @see timetableItemKey() **/ void setTimetableItems( const QVariantList &items ); /** * @brief Get all additional data of this data source. * Additional data gets stored by a hash value for the associated timetable item. **/ - QHash< uint, TimetableData > additionalData() const { return m_additionalData; }; + QMap< uint, TimetableData > additionalData() const { return m_additionalData; }; /** * @brief Set all additional data to @p additionalData. * This replaces all previously set additional timetable data. **/ - void setAdditionalData( const QHash< uint, TimetableData > &additionalData ) { + void setAdditionalData( const QMap< uint, TimetableData > &additionalData ) { // Cache all additional data for some time TODO m_additionalData.unite( additionalData ); }; /** @brief Get additional data for the item with @p departureHash. */ TimetableData additionalData( uint departureHash ) const { return m_additionalData[ departureHash ]; }; /** @brief Set additional data for the item with @p departureHash to @p additionalData. */ void setAdditionalData( uint departureHash, const TimetableData &additionalData ) { m_additionalData[ departureHash ] = additionalData; }; /** @brief Timer to update the data source periodically. */ QTimer *updateTimer() const { return m_updateTimer; }; void setUpdateTimer( QTimer *timer ); void stopUpdateTimer(); /** @brief Timer to delay updates to additional timetable data of timetable items. */ QTimer *updateAdditionalDataDelayTimer() const { return m_updateAdditionalDataDelayTimer; }; /** @brief Timer to cleanup cached additional data after some time. */ QTimer *cleanupTimer() const { return m_cleanupTimer; }; void setUpdateAdditionalDataDelayTimer( QTimer *timer ); void setCleanupTimer( QTimer *timer ); /** * @brief The time at which new downloads will have sufficient changes. * Sufficient means enough timetable items are in the past or there may be changed delays. **/ QDateTime nextDownloadTimeProposal() const { return m_nextDownloadTimeProposal; }; void setNextDownloadTimeProposal( const QDateTime &nextDownloadTime ) { m_nextDownloadTimeProposal = nextDownloadTime; }; bool enoughDataAvailable( const QDateTime &dateTime, int count ); QSharedPointer< AbstractRequest > request( const QString &sourceName ) const; - inline static uint hashForDeparture( const QVariantHash &departure, bool isDeparture = true ) { + inline static uint hashForDeparture( const QVariantMap &departure, bool isDeparture = true ) { return hashForDeparture( departure[Enums::toString(Enums::DepartureDateTime)].toDateTime(), static_cast(departure[Enums::toString(Enums::TypeOfVehicle)].toInt()), departure[Enums::toString(Enums::TransportLine)].toString(), departure[Enums::toString(Enums::Target)].toString(), isDeparture ); }; inline static uint hashForDeparture( const TimetableData &departure, bool isDeparture = true ) { return hashForDeparture( departure[Enums::DepartureDateTime].toDateTime(), static_cast(departure[Enums::TypeOfVehicle].toInt()), departure[Enums::TransportLine].toString(), departure[Enums::Target].toString(), isDeparture ); }; static uint hashForDeparture( const QDateTime &departure, Enums::VehicleType vehicleType, const QString &lineString, const QString &target, bool isDeparture = true ) { return qHash( QString("%1%2%3%4%5").arg(departure.toString("dMMyyhhmmss")) .arg(static_cast(vehicleType)) .arg(lineString) .arg(target.trimmed().toLower()) .arg(isDeparture ? "D" : "A") ); }; private: struct SourceData { SourceData( const QSharedPointer &request = QSharedPointer(), const QDateTime &dateTime = QDateTime(), int count = 1 ) : request(request), dateTime(dateTime), count(count) {} QSharedPointer< AbstractRequest > request; QDateTime dateTime; int count; }; - QHash< uint, TimetableData > m_additionalData; + QMap< uint, TimetableData > m_additionalData; QTimer *m_updateTimer; QTimer *m_cleanupTimer; QTimer *m_updateAdditionalDataDelayTimer; QDateTime m_nextDownloadTimeProposal; - QHash< QString, SourceData > m_dataSources; // Connected data sources ("ambiguous" ones) + QMap< QString, SourceData > m_dataSources; // Connected data sources ("ambiguous" ones) }; #endif // Multiple inclusion guard diff --git a/engine/gtfs/serviceprovidergtfs.cpp b/engine/gtfs/serviceprovidergtfs.cpp index 6cf8051..ad7c12a 100644 --- a/engine/gtfs/serviceprovidergtfs.cpp +++ b/engine/gtfs/serviceprovidergtfs.cpp @@ -1,909 +1,909 @@ /* * 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 "serviceprovidergtfs.h" // Own includes #include "serviceproviderdata.h" #include "serviceproviderglobal.h" #include "departureinfo.h" #include "gtfsservice.h" #include "gtfsrealtime.h" #include "request.h" // KDE includes #include #include #include #include #include #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include const qreal ServiceProviderGtfs::PROGRESS_PART_FOR_FEED_DOWNLOAD = 0.1; ServiceProviderGtfs::ServiceProviderGtfs( const ServiceProviderData *data, QObject *parent, const QSharedPointer &cache ) : ServiceProvider(data, parent, cache), m_state(Initializing), m_service(0) #ifdef BUILD_GTFS_REALTIME , m_tripUpdates(0), m_alerts(0) #endif { // Ensure that the GTFS feed was imported and the database is valid if ( updateGtfsDatabaseState(data->id(), data->feedUrl(), cache) == QLatin1String("ready") ) { m_state = Ready; // Load agency information from database and request GTFS-realtime data loadAgencyInformation(); #ifdef BUILD_GTFS_REALTIME updateRealtimeData(); #endif } else { m_state = Error; } // Update database, if a new version of the GTFS feed is available // and an initial import has finished successfully updateGtfsDatabase(); } ServiceProviderGtfs::~ServiceProviderGtfs() { // Free all agency objects qDeleteAll( m_agencyCache ); #ifdef BUILD_GTFS_REALTIME delete m_tripUpdates; delete m_alerts; #endif } QString ServiceProviderGtfs::updateGtfsDatabaseState( const QString &providerId, const QString &feedUrl, const QSharedPointer< KConfig > &_cache, - QVariantHash *stateData ) + QVariantMap *stateData ) { // Read 'feedImportFinished' value from provider cache QSharedPointer< KConfig > cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; KConfigGroup group = cache->group( providerId ); KConfigGroup gtfsGroup = group.group( "gtfs" ); QString errorMessage; bool importFinished = isGtfsFeedImportFinished( providerId, feedUrl, cache, &errorMessage ); // Update GTFS feed state fields that do not need the import to be finished already if ( stateData ) { // Data might not be available, // the "updateGtfsFeedInfo" operation of the GTFS service can be run to get the data qlonglong sizeInBytes = gtfsGroup.readEntry( "feedSizeInBytes", qlonglong(-1) ); QDateTime lastModified = QDateTime::fromString( gtfsGroup.readEntry("feedModifiedTime", QString()), Qt::ISODate ); stateData->insert( "gtfsFeedSize", sizeInBytes ); stateData->insert( "gtfsFeedModifiedTime", lastModified ); } // GTFS feed was successfully imported from the currently used feed URL if ( importFinished ) { // Import was marked as finished, test if the database file still exists and // is not empty (some space is needed for the tables also if they are empty) QFileInfo fi( GtfsDatabase::databasePath(providerId) ); if ( fi.exists() && fi.size() > 10000 ) { // Try to initialize the database if ( !GtfsDatabase::initDatabase(providerId, &errorMessage) ) { qWarning() << "Error initializing the database" << errorMessage; // Update 'feedImportFinished' field in the cache gtfsGroup.writeEntry( "feedImportFinished", false ); // Write to disk now if someone wants to read the value directly after this function gtfsGroup.sync(); if ( stateData ) { stateData->insert( "gtfsFeedImported", false ); stateData->insert( "statusMessage", errorMessage ); } return "gtfs_feed_import_pending"; } // Database exists, feed marked as imported and feed URL did not change // since the import, set state data and return state "ready" if ( stateData ) { stateData->insert( "gtfsFeedImported", true ); // Insert a status message stateData->insert( "statusMessage", i18nc("@info/plain", "GTFS feed succesfully imported") ); // Update GTFS database state fields const QString databasePath = GtfsDatabase::databasePath( providerId ); const QFileInfo databaseInfo( databasePath ); stateData->insert( "gtfsDatabasePath", databasePath ); stateData->insert( "gtfsDatabaseSize", databaseInfo.size() ); stateData->insert( "gtfsDatabaseModifiedTime", databaseInfo.lastModified().toString(Qt::ISODate) ); // Add an 'updatable' field to the state data const bool updatable = ServiceProviderGtfs::isUpdateAvailable( providerId, feedUrl, cache ); stateData->insert( "updatable", updatable ); } return "ready"; } else { qWarning() << "GTFS database file not found or empty database" << fi.filePath(); // The provider cache says the import has been finished, // but the database file does not exist any longer or is empty gtfsGroup.writeEntry( "feedImportFinished", false ); // Write to disk now if someone wants to read the value directly after this function gtfsGroup.sync(); // The GTFS feed has not been imported successfully yet // or the database file was deleted/corrupted if ( stateData ) { stateData->insert( "gtfsFeedImported", false ); stateData->insert( "statusMessage", i18nc("@info/plain", "GTFS feed not imported") ); } return "gtfs_feed_import_pending"; } } else { // GTFS feed was not imported or the feed URL has changed since the import if ( stateData ) { stateData->insert( "gtfsFeedImported", false ); stateData->insert( "statusMessage", errorMessage ); } return "gtfs_feed_import_pending"; } } bool ServiceProviderGtfs::isTestResultUnchanged( const QString &providerId, const QString &feedUrl, const QSharedPointer< KConfig > &cache ) { // The test result changes when the GTFS feed was updated return isUpdateAvailable( providerId, feedUrl, cache ); } bool ServiceProviderGtfs::isTestResultUnchanged( const QSharedPointer &cache ) const { return isTestResultUnchanged( data()->id(), data()->feedUrl(), cache ); } bool ServiceProviderGtfs::runTests( QString *errorMessage ) const { Q_UNUSED( errorMessage ); if ( m_state == Ready ) { // The GTFS feed was successfully imported return true; } const QUrl feedUrl( m_data->feedUrl() ); if ( feedUrl.isEmpty() || !feedUrl.isValid() ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Invalid GTFS feed URL: %1", m_data->feedUrl()); } return false; } // No errors found return true; } void ServiceProviderGtfs::updateGtfsDatabase() { if ( m_service ) { kDebug() << "Is already updating, please wait"; return; } Plasma::DataEngine *engine = qobject_cast< Plasma::DataEngine* >( parent() ); Q_ASSERT( engine ); m_service = engine->serviceForSource( "GTFS" ); QVariantMap op = m_service->operationDescription("updateGtfsDatabase"); op.insert( "serviceProviderId", m_data->id() ); m_service->startOperationCall( op ); } #ifdef BUILD_GTFS_REALTIME bool ServiceProviderGtfs::isRealtimeDataAvailable() const { return !m_data->realtimeTripUpdateUrl().isEmpty() || !m_data->realtimeAlertsUrl().isEmpty(); } void ServiceProviderGtfs::updateRealtimeData() { // m_state = DownloadingFeed; if ( !m_data->realtimeTripUpdateUrl().isEmpty() ) { KIO::StoredTransferJob *tripUpdatesJob = KIO::storedGet( m_data->realtimeTripUpdateUrl(), KIO::Reload, KIO::Overwrite | KIO::HideProgressInfo ); connect( tripUpdatesJob, SIGNAL(result(KJob*)), this, SLOT(realtimeTripUpdatesReceived(KJob*)) ); kDebug() << "Updating GTFS-realtime trip update data" << m_data->realtimeTripUpdateUrl(); } if ( !m_data->realtimeAlertsUrl().isEmpty() ) { KIO::StoredTransferJob *alertsJob = KIO::storedGet( m_data->realtimeAlertsUrl(), KIO::Reload, KIO::Overwrite | KIO::HideProgressInfo ); connect( alertsJob, SIGNAL(result(KJob*)), this, SLOT(realtimeAlertsReceived(KJob*)) ); kDebug() << "Updating GTFS-realtime alerts data" << m_data->realtimeAlertsUrl(); } if ( m_data->realtimeTripUpdateUrl().isEmpty() && m_data->realtimeAlertsUrl().isEmpty() ) { m_state = Ready; } } void ServiceProviderGtfs::realtimeTripUpdatesReceived( KJob *job ) { KIO::StoredTransferJob *transferJob = qobject_cast( job ); if ( job->error() != 0 ) { kDebug() << "Error downloading GTFS-realtime trip updates:" << job->errorString(); return; } delete m_tripUpdates; m_tripUpdates = GtfsRealtimeTripUpdate::fromProtocolBuffer( transferJob->data() ); if ( m_alerts || m_data->realtimeAlertsUrl().isEmpty() ) { m_state = Ready; } } void ServiceProviderGtfs::realtimeAlertsReceived( KJob *job ) { KIO::StoredTransferJob *transferJob = qobject_cast( job ); if ( job->error() != 0 ) { kDebug() << "Error downloading GTFS-realtime alerts:" << job->errorString(); return; } delete m_alerts; m_alerts = GtfsRealtimeAlert::fromProtocolBuffer( transferJob->data() ); if ( m_tripUpdates || m_data->realtimeTripUpdateUrl().isEmpty() ) { m_state = Ready; } } #endif // BUILD_GTFS_REALTIME void ServiceProviderGtfs::loadAgencyInformation() { if ( m_state != Ready ) { return; } QSqlQuery query( QSqlDatabase::database(m_data->id()) ); if ( !query.exec("SELECT * FROM agency") ) { kDebug() << "Could not load agency information from database:" << query.lastError(); return; } // Clear previously loaded agency data qDeleteAll( m_agencyCache ); m_agencyCache.clear(); // Read agency records from the database QSqlRecord record = query.record(); const int agencyIdColumn = record.indexOf( "agency_id" ); const int agencyNameColumn = record.indexOf( "agency_name" ); const int agencyUrlColumn = record.indexOf( "agency_url" ); const int agencyTimezoneColumn = record.indexOf( "agency_timezone" ); const int agencyLanguageColumn = record.indexOf( "agency_lang" ); const int agencyPhoneColumn = record.indexOf( "agency_phone" ); while ( query.next() ) { AgencyInformation *agency = new AgencyInformation; agency->name = query.value( agencyNameColumn ).toString(); agency->url = query.value( agencyUrlColumn ).toString(); agency->language = query.value( agencyLanguageColumn ).toString(); agency->phone = query.value( agencyPhoneColumn ).toString(); const QString timeZone = query.value(agencyTimezoneColumn).toString(); agency->timezone = new KTimeZone( timeZone.isEmpty() ? m_data->timeZone() : timeZone ); const uint id = query.value( agencyIdColumn ).toUInt(); m_agencyCache.insert( id, agency ); } } int ServiceProviderGtfs::AgencyInformation::timeZoneOffset() const { return timezone && timezone->isValid() ? timezone->currentOffset( Qt::LocalTime ) : 0; } qint64 ServiceProviderGtfs::databaseSize() const { QFileInfo fi( GtfsDatabase::databasePath(m_data->id()) ); return fi.size(); } ServiceProviderGtfs::AgencyInformation::~AgencyInformation() { delete timezone; } // NOTE When changing this function, also update ProjectPrivate::gtfsProviderFeatures() // in TimetableMate! QList ServiceProviderGtfs::features() const { QList features; features << Enums::ProvidesDepartures << Enums::ProvidesArrivals << Enums::ProvidesStopSuggestions << Enums::ProvidesRouteInformation << Enums::ProvidesStopID << Enums::ProvidesStopGeoPosition; // Enums::ProvidesStopsByGeoPosition TODO #ifdef BUILD_GTFS_REALTIME if ( !m_data->realtimeAlertsUrl().isEmpty() ) { features << Enums::ProvidesNews; } if ( !m_data->realtimeTripUpdateUrl().isEmpty() ) { features << Enums::ProvidesDelays; } #endif return features; } QDateTime ServiceProviderGtfs::timeFromSecondsSinceMidnight( const QDate &dateAtMidnight, int secondsSinceMidnight, QDate *date ) const { const int secondsInOneDay = 60 * 60 * 24; QDate resultDate = dateAtMidnight; while ( secondsSinceMidnight >= secondsInOneDay ) { secondsSinceMidnight -= secondsInOneDay; resultDate.addDays( 1 ); if ( date ) { date->addDays( 1 ); } } return QDateTime( resultDate, QTime(secondsSinceMidnight / (60 * 60), (secondsSinceMidnight / 60) % 60, secondsSinceMidnight % 60) ); } bool ServiceProviderGtfs::isGtfsFeedImportFinished( const QString &providerId, const QString &feedUrl, const QSharedPointer &_cache, QString *errorMessage ) { const QSharedPointer &cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; // Check if the GTFS feed import is marked as finished in the cache KConfigGroup group = cache->group( providerId ); KConfigGroup gtfsGroup = group.group( "gtfs" ); if ( gtfsGroup.readEntry("feedImportFinished", false) ) { // GTFS feed import is marked as finished, check if the feed URL has changed since if ( feedUrl.isEmpty() ) { // Feed URL not given, no change expected here (eg. directly after a finished import) return true; } const QString importedGtfsFeedUrl = gtfsGroup.readEntry( "feedUrl", QString() ); if ( importedGtfsFeedUrl == feedUrl ) { // Feed URL did not change, import is finished return true; } else { // Feed URL was modified, re-import needed, // update "feedImportFinished" field in the cache gtfsGroup.writeEntry( "feedImportFinished", false ); gtfsGroup.sync(); if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "GTFS feed was imported, but the feed URL " "has changed. Please re-import the feed from the new URL."); } return false; } } // GTFS feed import is marked as not finished or was not found in the cache if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "GTFS feed not imported. " "Please import it explicitly first."); } return false; } bool ServiceProviderGtfs::isUpdateAvailable( const QString &providerId, const QString &feedUrl, const QSharedPointer &_cache ) { const QSharedPointer cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; KConfigGroup group = cache->group( providerId ); KConfigGroup gtfsGroup = group.group( "gtfs" ); const bool importFinished = isGtfsFeedImportFinished( providerId, feedUrl, cache ); const QString databasePath = GtfsDatabase::databasePath( providerId ); const QFileInfo databaseInfo( databasePath ); const bool databaseReady = importFinished && databaseInfo.exists(); if ( databaseReady ) { // Check if an update is available const QString feedModifiedTimeString = gtfsGroup.readEntry( "feedModifiedTime", QString() ); const QDateTime gtfsFeedModifiedTime = QDateTime::fromString( feedModifiedTimeString, Qt::ISODate ); const QDateTime gtfsDatabaseModifiedTime = databaseInfo.lastModified(); return gtfsFeedModifiedTime.isValid() && gtfsFeedModifiedTime > gtfsDatabaseModifiedTime; } // GTFS feed not imported or database deleted return false; } uint ServiceProviderGtfs::stopIdFromName( const QString &stopName, bool *ok ) { // Try to get the ID for the given stop name. Only select stops, no stations (with one or // more sub stops) by requiring 'location_type=0', location_type 1 is for stations. // It's fast, because 'stop_name' is part of a compound index in the database. QString stopValue = stopName; stopValue.replace( '\'', "\'\'" ); QSqlQuery query( QSqlDatabase::database(m_data->id()) ); query.setForwardOnly( true ); // Don't cache records if ( !query.exec("SELECT stops.stop_id FROM stops WHERE stop_name='" + stopValue + "' " "AND (location_type IS NULL OR location_type=0)") ) { qWarning() << query.lastError(); kDebug() << query.executedQuery(); if ( ok ) { *ok = false; } return 0; } QSqlRecord stopRecord = query.record(); if ( query.next() ) { if ( ok ) { *ok = true; } return query.value( query.record().indexOf("stop_id") ).toUInt(); } else { bool _ok; const uint stopId = stopName.toUInt( &_ok ); if ( ok ) { *ok = _ok; } if ( !_ok ) { kDebug() << "No stop with the given name found (needs the exact name):" << stopName; return 0; } return stopId; } } void ServiceProviderGtfs::requestDepartures( const DepartureRequest &request ) { requestDeparturesOrArrivals( &request ); } void ServiceProviderGtfs::requestArrivals( const ArrivalRequest &request ) { requestDeparturesOrArrivals( &request ); } void ServiceProviderGtfs::requestDeparturesOrArrivals( const DepartureRequest *request ) { uint stopId; if ( !request->stopId().isEmpty() ) { // A stop ID is available, testing for it's ID in stop_times is not necessary bool ok; stopId = request->stopId().toUInt( &ok ); if ( !ok ) { qWarning() << "Invalid stop ID" << request->stopId() << "only numeric IDs allowed"; return; } } else { // Try to get the ID for the given stop name. bool ok; stopId = stopIdFromName( request->stop(), &ok ); if ( !ok ) { emit requestFailed( this, ErrorParsingFailed /*TODO*/, "No stop with the given name found (needs the exact name or an ID): " + request->stop(), QUrl(), request ); return; } } QSqlQuery query( QSqlDatabase::database(m_data->id()) ); query.setForwardOnly( true ); // Don't cache records // This creates a temporary table to calculate min/max fares for departures. // These values should be added into the db while importing, doing it here takes too long // const QString createJoinedFareTable = "CREATE TEMPORARY TABLE IF NOT EXISTS tmp_fares AS " // "SELECT * FROM fare_rules JOIN fare_attributes USING (fare_id);"; // if ( !query.prepare(createJoinedFareTable) || !query.exec() ) { // kDebug() << "Error while creating a temporary table fore min/max fare calculation:" // << query.lastError(); // kDebug() << query.executedQuery(); // return; // } // Query the needed departure info from the database. // It's fast, because all JOINs are done using INTEGER PRIMARY KEYs and // because 'stop_id' and 'departure_time' are part of a compound index in the database. // Sorting by 'arrival_time' may be a bit slower because is has no index in the database, // but if arrival_time values do not differ too much from the deaprture_time values, they // are also already sorted. // The tables 'calendar' and 'calendar_dates' are also fully implemented by the query below. // TODO: Create a new (temporary) table for each connected departure/arrival source and use // that (much smaller) table here for performance reasons const QString routeSeparator = "||"; const QTime time = request->dateTime().time(); const QString queryString = QString( "SELECT times.departure_time, times.arrival_time, times.stop_headsign, " "routes.route_type, routes.route_short_name, routes.route_long_name, " "trips.trip_headsign, routes.agency_id, stops.stop_id, trips.trip_id, " "routes.route_id, times.stop_sequence, " "( SELECT group_concat(route_stop.stop_name, '%5') AS route_stops " "FROM stop_times AS route_times INNER JOIN stops AS route_stop USING (stop_id) " "WHERE route_times.trip_id=times.trip_id AND route_times.stop_sequence %4= times.stop_sequence " "ORDER BY departure_time ) AS route_stops, " "( SELECT group_concat(route_times.departure_time, '%5') AS route_times " "FROM stop_times AS route_times " "WHERE route_times.trip_id=times.trip_id AND route_times.stop_sequence %4= times.stop_sequence " "ORDER BY departure_time ) AS route_times " // "( SELECT min(price) FROM tmp_fares WHERE origin_id=stops.zone_id AND price>0 ) AS min_price, " // "( SELECT max(price) FROM tmp_fares WHERE origin_id=stops.zone_id ) AS max_price, " // "( SELECT currency_type FROM tmp_fares WHERE origin_id=stops.zone_id LIMIT 1 ) AS currency_type " "FROM stops INNER JOIN stop_times AS times USING (stop_id) " "INNER JOIN trips USING (trip_id) " "INNER JOIN routes USING (route_id) " "LEFT JOIN calendar USING (service_id) " "LEFT JOIN calendar_dates ON (trips.service_id=calendar_dates.service_id " "AND strftime('%Y%m%d') LIKE calendar_dates.date) " "WHERE stop_id=%1 AND departure_time>%2 " "AND (calendar_dates.date IS NULL " // No matching record in calendar_dates table for today "OR NOT (calendar_dates.exception_type=2)) " // Journey is not removed today "AND (calendar.weekdays IS NULL " // No matching record in calendar table => always available "OR (strftime('%Y%m%d') BETWEEN calendar.start_date " // Current date is in the range... "AND calendar.end_date " // ...where the service is available... "AND substr(calendar.weekdays, strftime('%w') + 1, 1)='1') " // ...and it's available at the current weekday "OR (calendar_dates.date IS NOT NULL " // Or there is a matching record in calendar_dates for today... "AND calendar_dates.exception_type=1)) " // ...and this record adds availability of the service for today "ORDER BY departure_time " "LIMIT %3" ) .arg( stopId ) .arg( time.hour() * 60 * 60 + time.minute() * 60 + time.second() ) .arg( request->count() ) .arg( request->parseMode() == ParseForArrivals ? '<' : '>' ) // For arrivals route_stops/route_times need stops before the home stop .arg( routeSeparator ); if ( !query.prepare(queryString) || !query.exec() ) { kDebug() << "Error while querying for departures:" << query.lastError(); kDebug() << query.executedQuery(); return; } if ( query.size() == 0 ) { kDebug() << "Got an empty record"; return; } kDebug() << "Query executed"; kDebug() << query.executedQuery(); QSqlRecord record = query.record(); const int agencyIdColumn = record.indexOf( "agency_id" ); const int tripIdColumn = record.indexOf( "trip_id" ); const int routeIdColumn = record.indexOf( "route_id" ); const int stopIdColumn = record.indexOf( "stop_id" ); const int arrivalTimeColumn = record.indexOf( "arrival_time" ); const int departureTimeColumn = record.indexOf( "departure_time" ); const int routeShortNameColumn = record.indexOf( "route_short_name" ); const int routeLongNameColumn = record.indexOf( "route_long_name" ); const int routeTypeColumn = record.indexOf( "route_type" ); const int tripHeadsignColumn = record.indexOf( "trip_headsign" ); const int stopSequenceColumn = record.indexOf( "stop_sequence" ); const int stopHeadsignColumn = record.indexOf( "stop_headsign" ); const int routeStopsColumn = record.indexOf( "route_stops" ); const int routeTimesColumn = record.indexOf( "route_times" ); // const int fareMinPriceColumn = record.indexOf( "min_price" ); // const int fareMaxPriceColumn = record.indexOf( "max_price" ); // const int fareCurrencyColumn = record.indexOf( "currency_type" ); // Prepare agency information, if only one is given, it is used for all records AgencyInformation *agency = 0; if ( m_agencyCache.count() == 1 ) { agency = m_agencyCache.values().first(); } // Create a list of DepartureInfo objects from the query result DepartureInfoList departures; while ( query.next() ) { const QDate dateAtMidnight = request->dateTime().date(); // Load agency information from cache const QVariant agencyIdValue = query.value( agencyIdColumn ); if ( m_agencyCache.count() > 1 ) { Q_ASSERT( agencyIdValue.isValid() ); // GTFS says, that agency_id can only be null, if there is only one agency agency = m_agencyCache[ agencyIdValue.toUInt() ]; } // Time values are stored as seconds since midnight of the associated date int arrivalTimeValue = query.value(arrivalTimeColumn).toInt(); int departureTimeValue = query.value(departureTimeColumn).toInt(); QDateTime arrivalTime = timeFromSecondsSinceMidnight( dateAtMidnight, arrivalTimeValue ); QDateTime departureTime = timeFromSecondsSinceMidnight( dateAtMidnight, departureTimeValue ); // Apply timezone offset int offsetSeconds = agency ? agency->timeZoneOffset() : 0; if ( offsetSeconds != 0 ) { arrivalTime.addSecs( offsetSeconds ); departureTime.addSecs( offsetSeconds ); } TimetableData data; data[ Enums::DepartureDateTime ] = request->parseMode() == ParseForArrivals ? arrivalTime : departureTime; data[ Enums::TypeOfVehicle ] = vehicleTypeFromGtfsRouteType( query.value(routeTypeColumn).toInt() ); data[ Enums::Operator ] = agency ? agency->name : QString(); const QString transportLine = query.value(routeShortNameColumn).toString(); data[ Enums::TransportLine ] = !transportLine.isEmpty() ? transportLine : query.value(routeLongNameColumn).toString(); const QStringList routeStops = query.value(routeStopsColumn).toString().split( routeSeparator ); if ( routeStops.isEmpty() ) { // This happens, if the current departure is actually no departure, but an arrival at // the target station and vice versa for arrivals. continue; } data[ Enums::RouteStops ] = routeStops; data[ Enums::RouteExactStops ] = routeStops.count(); const QString tripHeadsign = query.value(tripHeadsignColumn).toString(); const QString stopHeadsign = query.value(stopHeadsignColumn).toString(); data[ Enums::Target ] = !tripHeadsign.isEmpty() ? tripHeadsign : (!stopHeadsign.isEmpty() ? stopHeadsign : (request->parseMode() == ParseForArrivals ? routeStops.first() : routeStops.last())); const QStringList routeTimeValues = query.value(routeTimesColumn).toString().split( routeSeparator ); QVariantList routeTimes; foreach ( const QString routeTimeValue, routeTimeValues ) { routeTimes << timeFromSecondsSinceMidnight( dateAtMidnight, routeTimeValue.toInt() ); } data[ Enums::RouteTimes ] = routeTimes; // const QString symbol = KCurrencyCode( query.value(fareCurrencyColumn).toString() ).defaultSymbol(); // data[ Pricing ] = KGlobal::locale()->formatMoney( // query.value(fareMinPriceColumn).toDouble(), symbol ) + " - " + // KGlobal::locale()->formatMoney( query.value(fareMaxPriceColumn).toDouble(), symbol ); #ifdef BUILD_GTFS_REALTIME if ( m_alerts ) { QStringList journeyNews; QString journeyNewsLink; foreach ( const GtfsRealtimeAlert &alert, *m_alerts ) { if ( alert.isActiveAt(QDateTime::currentDateTime()) ) { journeyNews << alert.description; journeyNewsLink = alert.url; } } if ( !journeyNews.isEmpty() ) { data[ Enums::JourneyNews ] = journeyNews.join( ", " ); data[ Enums::JourneyNewsLink ] = journeyNewsLink; } } if ( m_tripUpdates ) { uint tripId = query.value(tripIdColumn).toUInt(); uint routeId = query.value(routeIdColumn).toUInt(); uint stopId = query.value(stopIdColumn).toUInt(); uint stopSequence = query.value(stopSequenceColumn).toUInt(); foreach ( const GtfsRealtimeTripUpdate &tripUpdate, *m_tripUpdates ) { if ( (tripUpdate.tripId > 0 && tripId == tripUpdate.tripId) || (tripUpdate.routeId > 0 && routeId == tripUpdate.routeId) || (tripUpdate.tripId <= 0 && tripUpdate.routeId <= 0) ) { kDebug() << "tripId or routeId matched or not queried!"; foreach ( const GtfsRealtimeStopTimeUpdate &stopTimeUpdate, tripUpdate.stopTimeUpdates ) { if ( (stopTimeUpdate.stopId > 0 && stopId == stopTimeUpdate.stopId) || (stopTimeUpdate.stopSequence > 0 && stopSequence == stopTimeUpdate.stopSequence) || (stopTimeUpdate.stopId <= 0 && stopTimeUpdate.stopSequence <= 0) ) { kDebug() << "stopId matched or stopsequence matched or not queried!"; // Found a matching stop time update kDebug() << "Delays:" << stopTimeUpdate.arrivalDelay << stopTimeUpdate.departureDelay; } } } } } #endif // Create new departure information object and add it to the departure list. // Do not use any corrections in the DepartureInfo constructor, because all values // from the database are already in the correct format departures << DepartureInfoPtr( new DepartureInfo(data, PublicTransportInfo::NoCorrection) ); } // TODO Do not use a list of pointers here, maybe use data sharing for PublicTransportInfo/StopInfo? // The objects in departures are deleted in a connected slot in the data engine... const ArrivalRequest *arrivalRequest = dynamic_cast< const ArrivalRequest* >( request ); if ( arrivalRequest ) { emit arrivalsReceived( this, QUrl(), departures, GlobalTimetableInfo(), *arrivalRequest ); } else { emit departuresReceived( this, QUrl(), departures, GlobalTimetableInfo(), *request ); } } void ServiceProviderGtfs::requestStopSuggestions( const StopSuggestionRequest &request ) { QSqlQuery query( QSqlDatabase::database(m_data->id()) ); query.setForwardOnly( true ); QString stopValue = request.stop(); stopValue.replace( '\'', "\'\'" ); if ( !query.prepare(QString("SELECT * FROM stops WHERE stop_name LIKE '%%2%' LIMIT %1") .arg(STOP_SUGGESTION_LIMIT).arg(stopValue)) || !query.exec() ) { // Check of the error is a "disk I/O error", ie. the database file may have been deleted checkForDiskIoError( query.lastError(), &request ); kDebug() << query.lastError(); kDebug() << query.executedQuery(); return; } emit stopsReceived( this, QUrl(), stopsFromQuery(&query, &request), request ); } void ServiceProviderGtfs::requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ) { QSqlQuery query( QSqlDatabase::database(m_data->id()) ); query.setForwardOnly( true ); kDebug() << "Get stops near:" << request.distance() << "meters ==" << (request.distance() * 0.009 / 2); if ( !query.prepare(QString("SELECT * FROM stops " "WHERE stop_lon between (%2-%4) and (%2+%4) " "AND stop_lat between (%3-%4) and (%3+%4) LIMIT %1") .arg(STOP_SUGGESTION_LIMIT).arg(request.longitude()).arg(request.latitude()) .arg(request.distance() * 0.000009 / 2)) // Calculate degree from meters = 360/40,070,000 || !query.exec() ) { // Check of the error is a "disk I/O error", ie. the database file may have been deleted checkForDiskIoError( query.lastError(), &request ); kDebug() << query.lastError(); kDebug() << query.executedQuery(); return; } emit stopsReceived( this, QUrl(), stopsFromQuery(&query, &request), request ); } StopInfoList ServiceProviderGtfs::stopsFromQuery( QSqlQuery *query, const StopSuggestionRequest *request ) const { QSqlRecord record = query->record(); const int stopIdColumn = record.indexOf( "stop_id" ); const int stopNameColumn = record.indexOf( "stop_name" ); const int stopLongitudeColumn = record.indexOf( "stop_lon" ); const int stopLatitudeColumn = record.indexOf( "stop_lat" ); StopInfoList stops; while ( query->next() ) { const QString stopName = query->value(stopNameColumn).toString(); const QString id = query->value(stopIdColumn).toString(); const qreal longitude = query->value(stopLongitudeColumn).toReal(); const qreal latitude = query->value(stopLatitudeColumn).toReal(); int weight = -1; if ( !dynamic_cast(request) ) { // Compute a weight value for the found stop name. // The less different the found stop name is compared to the search string, the higher // it's weight gets. If the found name equals the search string, the weight becomes 100. // Use 84 as maximal starting weight value (if stopName doesn't equal the search string), // because maximally 15 bonus points are added which makes 99, less than total equality 100. weight = stopName == request->stop() ? 100 : 84 - qMin( 84, qAbs(stopName.length() - request->stop().length()) ); if ( weight < 100 && stopName.startsWith(request->stop()) ) { // 15 weight points bonus if the found stop name starts with the search string weight = qMin( 100, weight + 15 ); } if ( weight < 100 ) { // Test if the search string is the start of a new word in stopName // Start at 2, because startsWith is already tested above and at least a space must // follow to start a new word int pos = stopName.indexOf( request->stop(), 2, Qt::CaseInsensitive ); if ( pos != -1 && stopName[pos - 1].isSpace() ) { // 10 weight points bonus if a word in the found stop name // starts with the search string weight = qMin( 100, weight + 10 ); } } } stops << StopInfoPtr( new StopInfo(stopName, id, weight, longitude, latitude, request->city()) ); } if ( stops.isEmpty() ) { kDebug() << "No stops found"; } return stops; } bool ServiceProviderGtfs::checkForDiskIoError( const QSqlError &error, const AbstractRequest *request ) { Q_UNUSED( request ); // Check if the error is a "disk I/O" error or a "no such table" error, // ie. the database file may have been deleted/corrupted. // The error numbers (1, 10) are database dependend and work with SQLite if ( error.number() == 10 || error.number() == 1 ) { qWarning() << "Disk I/O error reported from database, reimport the GTFS feed" << error.text(); emit requestFailed( this, ErrorParsingFailed, i18nc("@info/plain", "The GTFS database is corrupted, please reimport " "the GTFS feed"), QUrl(), request ); m_state = Initializing; QString errorText; if ( !GtfsDatabase::initDatabase(m_data->id(), &errorText) ) { kDebug() << "Error initializing the database" << errorText; m_state = Error; return true; } QFileInfo fi( GtfsDatabase::databasePath(m_data->id()) ); if ( fi.exists() && fi.size() > 10000 ) { loadAgencyInformation(); #ifdef BUILD_GTFS_REALTIME updateRealtimeData(); #endif } return true; } else { return false; } } Enums::VehicleType ServiceProviderGtfs::vehicleTypeFromGtfsRouteType( int gtfsRouteType ) { switch ( gtfsRouteType ) { case 0: // Tram, Streetcar, Light rail. Any light rail or street level system within a metropolitan area. return Enums::Tram; case 1: // Subway, Metro. Any underground rail system within a metropolitan area. return Enums::Subway; case 2: // Rail. Used for intercity or long-distance travel. return Enums::IntercityTrain; case 3: // Bus. Used for short- and long-distance bus routes. return Enums::Bus; case 4: // Ferry. Used for short- and long-distance boat service. return Enums::Ferry; case 5: // Cable car. Used for street-level cable cars where the cable runs beneath the car. return Enums::TrolleyBus; case 6: // Gondola, Suspended cable car. Typically used for aerial cable cars where the car is suspended from the cable. return Enums::UnknownVehicleType; // TODO Add new type to VehicleType: eg. Gondola case 7: // Funicular. Any rail system designed for steep inclines. return Enums::UnknownVehicleType; // TODO Add new type to VehicleType: eg. Funicular default: return Enums::UnknownVehicleType; } } diff --git a/engine/gtfs/serviceprovidergtfs.h b/engine/gtfs/serviceprovidergtfs.h index 1d9f739..fbf6102 100644 --- a/engine/gtfs/serviceprovidergtfs.h +++ b/engine/gtfs/serviceprovidergtfs.h @@ -1,286 +1,286 @@ /* * 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 a class to access data from GTFS feeds. * @author Friedrich Pülz */ #ifndef SERVICEPROVIDERGTFS_HEADER #define SERVICEPROVIDERGTFS_HEADER #include "config.h" #include "serviceprovider.h" #include "gtfsimporter.h" #ifdef BUILD_GTFS_REALTIME #include "gtfsrealtime.h" #endif namespace Plasma { class Service; } class GtfsService; class QNetworkReply; class KTimeZone; /** * @brief This class uses a database similiar to the GTFS structure to access public transport data. * * To fill the GTFS database with data from a GeneralTransitFeedSpecification feed (zip file) * GtfsImporter is used by the GTFS service. This service has an operation "updateGtfsDatabase", * which gets called by this class. That operation only updates already imported GTFS feeds if * there is a new version. To import a new GTFS feed for the first time the operation * "importGtfsFeed" should be used. That operation does @em not get called by this class. * This is because importing GTFS feeds can require quite a lot disk space and importing can take * some time. The user should be asked to import a new GTFS feed. * * This class immediately emits the ...Received() signal in the associated request...() functions, * because making timetable data available is very fast using the GTFS database. Requesting data * from the database is very fast even for big databases (eg. 300MB). * * To add support for a new service provider using this accessor type you need to write an accessor * XML file for the service provider. * See @ref pageAccessorInfos. **/ class ServiceProviderGtfs : public ServiceProvider { Q_OBJECT public: /** @brief States of this class. */ enum State { Initializing = 0, /**< Only used in the constructor as long as the state is unknown. */ Ready, /**< The provider is ready to use, ie. the GTFS feed was successfully imported. */ Error /**< There was an error that prevents the provider from being used, eg. the GTFS * database does not exist or was deleted/corrupted. */ }; /** * @brief Holds information about a public transport agency. * * All agencies used in the GTFS feed are stored in the accessor for fast access. * For most GTFS feeds, there is only one agency. For others, there are only a few agencies. **/ struct AgencyInformation { AgencyInformation() : timezone(0) {}; virtual ~AgencyInformation(); /** @brief Gets the offset in seconds for the agency's timezone. */ int timeZoneOffset() const; QString name; QString phone; QString language; QUrl url; KTimeZone *timezone; }; /** @brief Typedef to store agency information of all agencies in the GTFS feed by ID. */ typedef QHash AgencyInformations; /** @brief The maximum number of stop suggestions to return. */ static const int STOP_SUGGESTION_LIMIT = 100; /** * @brief Update the GTFS database state for @p providerId in the cache and return the result. * * This function checks and updates the 'feedImportFinished' field in the 'gtfs' config group * in the cache. * * @param providerId The ID of the GTFS provider for which the database state should be updated. * @param feedUrl The GTFS feed URL of the provider. Used to check if an imported GTFS database * was created from the feed at this URL. If this is an empty/null string, no check is done. * @param cache A shared pointer to the provider cache. If an invalid pointer gets passed it * gets received using ServiceProviderGlobal::cache(). * @param stateData State data gets inserted here, at least a 'statusMessage'. * @return The state of the provider. If the GTFS feed was successfully imported and the * database is valid the returned state is "ready", otherwise "gtfs_feed_import_pending". **/ static QString updateGtfsDatabaseState( const QString &providerId, const QString &feedUrl, const QSharedPointer< KConfig > &cache = QSharedPointer(), - QVariantHash *stateData = 0 ); + QVariantMap *stateData = 0 ); /** * @brief Whether or not the GTFS feed import for the provider with @p data is finished. * * @param providerId The ID of the GTFS provider to check. * @param feedUrl The GTFS feed URL of the provider. Used to check if an imported GTFS database * was created from the feed at this URL. If this is an empty/null string, no check is done. * @param cache A shared pointer to the provider cache, see cache(). If this is an invalid * pointer it gets created using cache(). * @param errorMessage If not @c 0 and @c false gets returned, this gets set to an * error Message, explaining why the import is not finished. * @return @c True if the GTFS feed has been successfully imported and the feed URL has not * changed since the import, @c false otherwise. **/ static bool isGtfsFeedImportFinished( const QString &providerId, const QString &feedUrl, const QSharedPointer &cache = QSharedPointer(), QString *errorMessage = 0 ); /** @brief Checks if an updated GTFS feed is available for @p providerId. */ static bool isUpdateAvailable( const QString &providerId, const QString &feedUrl, const QSharedPointer &cache = QSharedPointer() ); /** * @brief Constructs a new ServiceProviderGtfs object. * * You should use createProvider() to get an accessor for a given service provider ID. * * @param info An object containing information about the service provider. * @param parent The parent QObject. **/ explicit ServiceProviderGtfs( const ServiceProviderData *data, QObject *parent = 0, const QSharedPointer &cache = QSharedPointer(0) ); /** @brief Destructor. */ virtual ~ServiceProviderGtfs(); /** * @brief Whether or not the cached test result for @p providerId is unchanged. * * This function tests if an updated GTFS feed is available. * @param providerId The ID of the GTFS provider to check. * @param feedUrl The GTFS feed URL of the provider. Used to check if an imported GTFS database * was created from the feed at this URL. If this is an empty/null string, no check is done. * @param cache A shared pointer to the cache. * @see isUpdateAvailable() * @see runTests() **/ static bool isTestResultUnchanged( const QString &providerId, const QString &feedUrl, const QSharedPointer &cache ); /** * @brief Whether or not the cached test result is unchanged. * * This function tests if an updated GTFS feed is available. * @param cache A shared pointer to the cache. * @see isUpdateAvailable() * @see runTests() **/ virtual bool isTestResultUnchanged( const QSharedPointer &cache ) const; /** @brief Returns the type of this provider, ie. GtfsProvider. */ virtual Enums::ServiceProviderType type() const { return Enums::GtfsProvider; }; /** @brief Gets a list of features that this provider supports. */ virtual QList features() const; #ifdef BUILD_GTFS_REALTIME /** @brief Returns true, if there is a GTFS-realtime source available. */ bool isRealtimeDataAvailable() const; #else bool isRealtimeDataAvailable() const { return false; }; // Dummy function #endif /** @brief Gets the size in bytes of the database containing the GTFS data. */ qint64 databaseSize() const; /** @brief Get the ID for the stop with the given @p stopName. */ uint stopIdFromName( const QString &stopName, bool *ok = 0 ); protected slots: #ifdef BUILD_GTFS_REALTIME /** * @brief GTFS-realtime TripUpdates data received. * * TripUpdates are realtime updates to departure/arrival times, ie. delays. **/ void realtimeTripUpdatesReceived( KJob *job ); /** * @brief GTFS-realtime Alerts data received. * * Alerts contain journey information for specific departures/arrivals. **/ void realtimeAlertsReceived( KJob *job ); #endif // BUILD_GTFS_REALTIME protected: void requestDeparturesOrArrivals( const DepartureRequest *request ); /** * @brief Requests a list of departures from the GTFS database. * @param request Information about the departure request. **/ virtual void requestDepartures( const DepartureRequest &request ); /** * @brief Requests a list of arrivals from the GTFS database. * @param request Information about the arrival request. **/ virtual void requestArrivals( const ArrivalRequest &request ); /** * @brief Requests a list of stop suggestions from the GTFS database. * @param request Information about the stop suggestion request. **/ virtual void requestStopSuggestions( const StopSuggestionRequest &request ); virtual void requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ); /** @brief Run script provider specific tests. */ virtual bool runTests( QString *errorMessage = 0 ) const; /** @brief Updates the GTFS database using the GTFS service. */ void updateGtfsDatabase(); #ifdef BUILD_GTFS_REALTIME /** @brief Updates the GTFS-realtime data, ie. delays and journey news. */ void updateRealtimeData(); #endif /** @brief Check @p error for IO errors, emit requestFailed() on failure. */ bool checkForDiskIoError( const QSqlError &error, const AbstractRequest *request ); /** @brief Get a list of stops from a successfully executed @p query. */ StopInfoList stopsFromQuery( QSqlQuery *query, const StopSuggestionRequest *request = 0 ) const; /** * @brief Whether or not realtime data is available in the @p data of a timetable data source. */ - virtual bool isRealtimeDataAvailable( const QVariantHash &data ) const { + virtual bool isRealtimeDataAvailable( const QVariantMap &data ) const { Q_UNUSED( data ); return isRealtimeDataAvailable(); }; private: /** A value between 0.0 and 1.0 indicating the amount of the total progress for downloading. */ static const qreal PROGRESS_PART_FOR_FEED_DOWNLOAD; /** * @brief Converts a GTFS route_type value to a matching VehicleType. * * @see http://code.google.com/intl/en-US/transit/spec/transit_feed_specification.html#routes_txt___Field_Definitions **/ static Enums::VehicleType vehicleTypeFromGtfsRouteType( int gtfsRouteType ); QDateTime timeFromSecondsSinceMidnight( const QDate &dateAtMidnight, int secondsSinceMidnight, QDate *date = 0 ) const; void loadAgencyInformation(); State m_state; // Current state AgencyInformations m_agencyCache; // Cache contents of the "agency" DB table, usally small, eg. only one agency Plasma::Service *m_service; #ifdef BUILD_GTFS_REALTIME GtfsRealtimeTripUpdates *m_tripUpdates; GtfsRealtimeAlerts *m_alerts; #endif }; #endif // Multiple inclusion guard diff --git a/engine/publictransportdataengine.cpp b/engine/publictransportdataengine.cpp index 12dca7e..618e705 100644 --- a/engine/publictransportdataengine.cpp +++ b/engine/publictransportdataengine.cpp @@ -1,2775 +1,2775 @@ /* * 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 ); QStringList providersList = ServiceProviderGlobal::installedProviders(); QString defaultProviderFile = providersList.first(); QString defaultProviderId = ServiceProviderGlobal::idFromFileName(defaultProviderFile); QVariantMap importFeedParams = service->operationDescription("importGtfsFeed"), updateFeedParams = service->operationDescription("updateGtfsFeedInfo"), updateDbParams = service->operationDescription("updateGtfsDatabase"), deleteDbParams = service->operationDescription("deleteGtfsDatabase"); importFeedParams.insert("serviceProviderId", defaultProviderId); updateFeedParams.insert("serviceProviderId", defaultProviderId); updateDbParams.insert("serviceProviderId", defaultProviderId); deleteDbParams.insert("serviceProviderId", defaultProviderId); Plasma::ServiceJob *importFeedJob = service->startOperationCall(importFeedParams), *updateFeedJob = service->startOperationCall(updateFeedParams), *updateDbJob = service->startOperationCall(updateDbParams), *deleteDbJob = service->startOperationCall(deleteDbParams); connect( importFeedJob, SIGNAL(finished(KJob*)), this, SLOT(gtfsServiceJobFinished(KJob*)) ); connect( updateFeedJob, SIGNAL(finished(KJob*)), this, SLOT(gtfsServiceJobFinished(KJob*)) ); connect( updateDbJob, SIGNAL(finished(KJob*)), this, SLOT(gtfsServiceJobFinished(KJob*)) ); connect( deleteDbJob, SIGNAL(finished(KJob*)), this, SLOT(gtfsServiceJobFinished(KJob*)) ); 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 ); + QVariantMap 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(); + for ( QVariantMap::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( KJob *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; + QVariantMap 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 ); + QVariantMap 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 ); + QVariantMap 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 = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + 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 ) +QVariantMap PublicTransportEngine::serviceProviderData( const ServiceProvider *provider ) { Q_ASSERT( provider ); return serviceProviderData( *(provider->data()), provider ); } -QVariantHash PublicTransportEngine::serviceProviderData( const ServiceProviderData &data, +QVariantMap PublicTransportEngine::serviceProviderData( const ServiceProviderData &data, const ServiceProvider *provider ) { - QVariantHash dataServiceProvider; + QVariantMap 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() +QVariantMap PublicTransportEngine::locations() { - QVariantHash ret; + QVariantMap 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 ); + QVariantMap locationMap; + locationMap.insert( "name", location ); if ( location == "international" ) { - locationHash.insert( "description", i18n("International providers. " + locationMap.insert( "description", i18n("International providers. " "There is one for getting flight departures/arrivals.") ); } else { - locationHash.insert( "description", i18n("Service providers for %1.", + locationMap.insert( "description", i18n("Service providers for %1.", KGlobal::locale()->countryCodeToName(location)) ); } - locationHash.insert( "defaultProvider", defaultProviderId ); - ret.insert( location, locationHash ); + locationMap.insert( "defaultProvider", defaultProviderId ); + ret.insert( location, locationMap ); } } } 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(); + QVariantMap locations = m_dataSources[ sourceTypeKeyword(LocationsSource) ]->data(); + QVariantMap locationCountry = locations[ data.defaultParameter.toLower() ].toMap(); 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; + QVariantMap providerData; QString errorMessage; ProvidersDataSource *providersSource = providersDataSource(); // Test if the provider is valid if ( testServiceProvider(providerId, &providerData, &errorMessage, cache) ) { - QVariantHash stateData; + QVariantMap 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; + QVariantMap 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, + QVariantMap *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(); + for ( QVariantMap::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, + QVariantMap *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() { QVariantMap erroneousProvidersMap; const QLatin1String name = sourceTypeKeyword( ErroneousServiceProvidersSource ); foreach(QString key, m_erroneousProviders.keys()) erroneousProvidersMap[key] = m_erroneousProviders[key]; setData( name, erroneousProvidersMap ); 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(); + QVariantMap item = items[ itemNumber ].toMap(); // 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; + QVariantMap 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; + QVariantMap 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; + QVariantMap 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(); + QVariantMap item = items[ request.itemNumber() ].toMap(); 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; + QVariantMap 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; + QVariantMap 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(); + QVariantMap item = items[ additionalDataRequest->itemNumber() ].toMap(); 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 item = + (direction == Enums::EarlierItems ? items.first() : items.last()).toMap(); const QVariantMap _requestData = item[Enums::toString(Enums::RequestData)].toMap(); // TODO Convert to hash from map.. - QVariantHash requestData; + QVariantMap 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 ); + const QVariantMap 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, int( 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_WITH_JSON( publictransport, PublicTransportEngine, "plasma-engine-publictransport.json") // this is needed since PublicTransportEngine is a QObject #include "build/publictransportdataengine.moc" diff --git a/engine/publictransportdataengine.h b/engine/publictransportdataengine.h index dcd5f0e..92a11e0 100644 --- a/engine/publictransportdataengine.h +++ b/engine/publictransportdataengine.h @@ -1,782 +1,782 @@ /* * 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. */ /** @file * @brief This file contains the public transport data engine. * @author Friedrich Pülz */ #ifndef PUBLICTRANSPORTDATAENGINE_HEADER #define PUBLICTRANSPORTDATAENGINE_HEADER // Own includes #include "config.h" #include "enums.h" #include "departureinfo.h" // Plasma includes #include class AbstractRequest; class AbstractTimetableItemRequest; class StopSuggestionRequest; class DepartureRequest; class ArrivalRequest; class JourneyRequest; class AdditionalDataRequest; class DataSource; class TimetableDataSource; class ProvidersDataSource; class ServiceProvider; class ServiceProviderData; class DepartureInfo; class JourneyInfo; class StopInfo; class KJob; class QTimer; class QFileSystemWatcher; /** * @brief This engine provides departure/arrival times and journeys for public transport. * * See @ref pageUsage. */ class PublicTransportEngine : public Plasma::DataEngine { Q_OBJECT public: /** @brief A shared pointer to a ServiceProvider. */ typedef QSharedPointer< ServiceProvider > ProviderPointer; /** * @brief The available types of sources of this data engine. * * Source names can be associated with these types by their first words using * souceTypeFromName(). * @see sourceTypeKeyword() * @see sourceTypeFromName() **/ enum SourceType { InvalidSourceName = 0, /**< Returned by @ref sourceTypeFromName, * if the source name is invalid. */ // Data sources providing information about the engine, available providers ServiceProviderSource = 1, /**< The source contains information about a specific * service provider, identified by it's ID or a country code. If a country code * is given, the default provider for that country gets used, if any. * See also @ref usage_serviceproviders_sec. */ ServiceProvidersSource = 2, /**< The source contains information about all available * service providers. See also @ref usage_serviceproviders_sec. */ ErroneousServiceProvidersSource = 3, /**< The source contains a list of erroneous * service providers. */ LocationsSource = 4, /**< The source contains information about locations * for which supported service providers exist. */ VehicleTypesSource = 5, /**< The source contains information about all available * vehicle types. They are stored by integer keys, matching the values of * the Enums::VehicleType (engine) and PublicTransport::VehicleType * (libpublictransporthelper) enumerations. The information stored in this * data source can also be retrieved from PublicTransport::VehicleType * using libpublictransporthelper. See also @ref usage_vehicletypes_sec. */ // Data sources providing timetable data DeparturesSource = 10, /**< The source contains timetable data for departures. * See also @ref usage_departures_sec. */ ArrivalsSource, /**< The source contains timetable data for arrivals. * See also @ref usage_departures_sec. */ StopsSource, /**< The source contains a list of stop suggestions. * See also @ref usage_stopList_sec. */ JourneysSource, /**< The source contains information about journeys. * See also @ref usage_journeys_sec. */ JourneysDepSource, /**< The source contains information about journeys, * that depart at the given date and time. See also @ref usage_journeys_sec. */ JourneysArrSource /**< The source contains information about journeys, * that arrive at the given date and time. See also @ref usage_journeys_sec. */ }; /** @brief Every data engine needs a constructor with these arguments. */ PublicTransportEngine( QObject *parent, const QVariantList &args ); /** @brief Destructor. */ ~PublicTransportEngine(); /** @brief Contains data read from a data source name. */ struct SourceRequestData { /** @brief Reads the given data source @p name and fills member variables. */ SourceRequestData( const QString &name ); /** @brief Deletes the request object if it was created. */ ~SourceRequestData(); /** @brief Whether or not the source name is valid. */ bool isValid() const; QString name; /**< The complete data source name. */ QString defaultParameter; /**< A parameter without name read from the source name. * This can be a provider ID or a location code. */ SourceType type; /**< The type of the data source. */ ParseDocumentMode parseMode; /**< Parse mode for requesting data sources. */ AbstractTimetableItemRequest *request; /**< A request object created for requesting * data sources. */ }; /** @brief Get the keyword used in source names associated with the given @p sourceType. */ static const QLatin1String sourceTypeKeyword( SourceType sourceType ); /** @brief Get ParseDocumentMode associated with the given @p sourceType. */ static ParseDocumentMode parseModeFromSourceType( SourceType sourceType ); /** * @brief Get the SourceType associated with the given @p sourceName. * This matches case sensitive, otherwise there can be multiple data sources with the * same data but only different case in their names. **/ static SourceType sourceTypeFromName( const QString &sourceName ); /** @brief Whether or not data sources of @p sourceType request data from a server. */ static bool isDataRequestingSourceType( SourceType sourceType ) { return static_cast< int >( sourceType ) >= 10; }; /** @brief Get the ID of the provider used for the source with the given @p sourceName. */ static QString providerIdFromSourceName( const QString &sourceName ); /** @brief Remove words from the beginning/end of stop names that occur in most names. */ static QStringList removeCityNameFromStops( const QStringList &stopNames ); /** @brief Reimplemented to add some always visible default sources. */ virtual QStringList sources() const; /** * @brief Gets the service for the data source with the given @p name. * * The returned service can be used to start operations on the timetable data source. * For example it has an operation to import GTFS feeds into a local database or to update * or delete that database. * @return A pointer to the created Plasma::Service or 0 if no service is available for @p name. * @see PublicTransportService **/ virtual Plasma::Service* serviceForSource( const QString &name ); /** * @brief Get the number of seconds until the next automatic update of @p sourceName. * * Timetable data sources update their data periodically, based on the needs of the data source * (is realtime data available?, until when are departures already available?, etc.). To update * a timetable data source before the automatic update, use requestUpdate(). * @param sourceName The name of the data source to query for the next automatic update time. * @param errorMessage If not 0, this gets set to a message describing the error if -1 * gets returned. * @returns The number of seconds until the next automatic update of @p sourceName or -1 if * no data source with @p sourceName is available or is not a timetable data source. * @see requestUpdate() **/ int secsUntilUpdate( const QString &sourceName, QString *errorMessage = 0 ); /** * @brief Get the minimum number of seconds until the next (manual) update of @p sourceName. * * Timetable data sources can be updated before the next automatic update using requestUpdate(). * But a minimum number of seconds needs to have passed before another update request will * be accepted. * @param sourceName The name of the data source to query for the minimal next update time. * @param errorMessage If not 0, this gets set to a message describing the error if -1 * gets returned. * @returns The minumal number of seconds until the next update request of @p sourceName * will be accepted or -1 if no data source with @p sourceName is available or is not a * timetable data source. * @see requestUpdate() **/ int minSecsUntilUpdate( const QString &sourceName, QString *errorMessage = 0 ); /** * @brief Requests an update for @p sourceName. * * Can be used to update a timetable data source before the next calculated automatic update * time. This allows for more updates in the same time, but not unlimited. The waiting time * between two updates is lower when using requestUpdate(), compared to automatic updates. * @note This is marked with Q_INVOKABLE so that it can be invoked using QMetaObject from the * timetable service for that source. * @param sourceName The name of the data source to request an update for. * @param errorMessage If not 0, this gets set to a message describing the error if @c false * gets returned. * @returns @c True, if the update gets processed, @c false otherwise, eg. if not enough time * has passed since the last update. * @see minSecsUntilUpdate() * @see secsUntilUpdate() **/ Q_INVOKABLE bool requestUpdate( const QString &sourceName ); /** @brief Request more items in @p direction for @p sourceName */ Q_INVOKABLE bool requestMoreItems( const QString &sourceName, Enums::MoreItemsDirection direction = Enums::LaterItems ); /** * @brief Get a hash with information about the current GTFS feed import state. **/ - QString updateProviderState( const QString &providerId, QVariantHash *stateData, + QString updateProviderState( const QString &providerId, QVariantMap *stateData, const QString &providerType, const QString &feedUrl = QString(), bool readFromCache = true ); #ifdef BUILD_PROVIDER_TYPE_GTFS /** * @brief Try to start a GTFS service @p job that accesses the GTFS database. * @return @c True, if the @p job can be started, @c false otherwise. If there already is a * running GTFS service @p job that accesses the GTFS database no other such jobs can be * started and this function returns @c false. **/ Q_INVOKABLE bool tryToStartGtfsFeedImportJob( Plasma::ServiceJob *job ); #endif // BUILD_PROVIDER_TYPE_GTFS /** * @brief The default time offset from now for the first departure/arrival/journey in results. * * This is used if no time was specified in the source name with the parameter @c time, * @c datetime or @c timeoffset. **/ static const int DEFAULT_TIME_OFFSET; /** * @brief The (minimal) time in milliseconds for unused providers to stay cached. * * When a provider is not used any longer, it is not destroyed immediately, but gets cached. * After this timeout still unused cached providers get destroyed. If an unused provider * gets used again before this timeout it will not get destroyed. * * This timeout can be some seconds long, only the ServiceProvider instance gets cached. * Can prevent too many recreations of providers. **/ static const int PROVIDER_CLEANUP_TIMEOUT; signals: /** * @brief Emitted when a request for additional data has been finished. * * This signal gets used by the timetable service to get notified when a job has finsihed * and whether it was successful or not. * @param sourceName The name of the data source for which an additional data request finished. * @param item The index of the timetable item in @p sourceName for which the additional data * request finished or -1 if the request failed for all items. * @param success Whether or not the additional data job was successful. * @param errorMessage Contains an error message if @p success is @c false. * * @note The "additionalDataError" field of the target data source is not necessarily updated * when this signal gets emitted. This only happens when the request could actually be * started, ie. if the target data source exists, is a timetable data source and the provider * is valid and supports the Enums::ProvidesAdditionalData feature. * * @see usage_service_sec **/ void additionalDataRequestFinished( const QString &sourceName, int item, bool success = true, const QString &errorMessage = QString() ); /** @brief Emitted when a manual update request (requestUpdate()) has finished. */ void updateRequestFinished( const QString &sourceName, bool success = true, const QString &errorMessage = QString() ); /** @brief Emitted when a request for more items (requestMoreItems()) has finished. */ void moreItemsRequestFinished( const QString &sourceName, Enums::MoreItemsDirection direction, bool success = true, const QString &errorMessage = QString() ); public slots: /** * @brief Request additional timetable data in @p sourceName. * * @param sourceName The name of the timetable data source for which additional data should * be requested. * @param updateItem The index of the (first) timetable item to request additional data for. * @param count The number of timetable items beginning at @p updateItem to request additional * data for. The default is 1. **/ void requestAdditionalData( const QString &sourceName, int updateItem, int count = 1 ); protected slots: /** * @brief Free resources for the data source with the given @p name where possible. * * Destroys the DataSource object associated with the data source, if the source is not also * connected under other ambiguous names (for timetable data sources). If a ServiceProvider * was used for the data source but not for any other source, it gets destroyed, too. **/ void slotSourceRemoved( const QString &name ); /** * @brief The network state has changed, eg. was connected or disconnected. * * This slot is connected with the @c networkstatus module of @c kded through DBus. * @p state The new network state. See Solid::Networking::Status. **/ void networkStateChanged( uint state ); /** @brief The update timeout for a timetable data source was reached. */ void updateTimeout(); /** @brief The cleanup timeout for a timetable data source was reached. */ void cleanupTimeout(); /** @brief Do some cleanup, eg. deleting unused cached providers after a while. */ void cleanup(); #ifdef BUILD_PROVIDER_TYPE_GTFS /** @brief A @p job of the GTFS service has finished. */ void gtfsServiceJobFinished( KJob *job ); /** @brief Received a message from a @p job of the GTFS service. */ void gtfsImportJobInfoMessage( KJob *job, const QString &plain, const QString &rich ); /** @brief A @p job of the GTFS service notifies it's progress in @p percent. */ void gtfsImportJobPercent( KJob *job, ulong percent ); #endif // BUILD_PROVIDER_TYPE_GTFS /** * @brief Departures were received. * * @param provider The provider that was used to get the departures. * @param requestUrl The url used to request the information. * @param departures The departures that were received. * @param globalInfo Global information that affects all departures. * @param request Information about the request for the here received @p departures. * * @see ServiceProvider::useSeparateCityValue() **/ void departuresReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &departures, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool deleteDepartureInfos = true ); /** * @brief Arrivals were received. * * @param provider The provider that was used to get the arrivals. * @param requestUrl The url used to request the information. * @param arrivals The arrivals that were received. * @param globalInfo Global information that affects all arrivals. * @param request Information about the request for the here received @p arrivals. * * @see ServiceProvider::useSeparateCityValue() **/ void arrivalsReceived( ServiceProvider *provider, const QUrl &requestUrl, const ArrivalInfoList &arrivals, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool deleteDepartureInfos = true ); /** * @brief Journeys were received. * * @param provider The provider that was used to get the journeys. * @param requestUrl The url used to request the information. * @param journeys The journeys that were received. * @param globalInfo Global information that affects all journeys. * @param request Information about the request for the here received @p journeys. * * @see ServiceProvider::useSeparateCityValue() **/ void journeysReceived( ServiceProvider *provider, const QUrl &requestUrl, const JourneyInfoList &journeys, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool deleteJourneyInfos = true ); /** * @brief Stop suggestions were received. * * @param provider The service provider that was used to get the stops. * @param requestUrl The url used to request the information. * @param stops The stops that were received. * @param request Information about the request for the here received @p stops. * * @see ServiceProvider::useSeparateCityValue() **/ void stopsReceived( ServiceProvider *provider, const QUrl &requestUrl, const StopInfoList &stops, const StopSuggestionRequest &request, bool deleteStopInfos = true ); /** * @brief Additional data was received. * * @param provider The service provider that was used to get additional data. * @param requestUrl The url used to request the information. * @param data The additional data that was receceived. * @param request Information about the request for the here received @p data. * * @see ServiceProvider::useSeparateCityValue() **/ void additionalDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const TimetableData &data, const AdditionalDataRequest &request ); /** * @brief A request has failed. * * @param provider The provider that was used to get data from the service provider. * @param errorCode The error code or NoError if there was no error. * @param errorString If @p errorCode isn't NoError this contains a description of the error. * @param requestUrl The url used to request the information. * @param request Information about the request that failed with @p errorType. * * @see ServiceProvider::useSeparateCityValue() **/ void requestFailed( ServiceProvider *provider, ErrorCode errorCode, const QString &errorMessage, const QUrl &requestUrl, const AbstractRequest *request ); /** * @brief Update data sources which have new additional data available. * * This slot gets used to combine too many data source updates in short time * into a single update. **/ void updateDataSourcesWithNewAdditionData(); /** * @brief A global or local directory with service provider XML files was changed. * * This slot uses reloadChangedProviders() to actually update the loaded service providers. * Because a change in multiple files in one or more directories causes a call to this slot for * each file, this would cause all providers to be reloaded for each changed file. Especially * when installing a new version, while running an old one, this can take some time. * Therefore this slot uses a short timeout. If a new call to this slot happens within that * timeout, the timeout gets simply restarted. Otherwise after the timeout * reloadChangedProviders() gets called. * * @param path The changed directory. * @see reloadChangedProviders() */ void serviceProviderDirChanged( const QString &path ); /** * @brief Deletes all currently created providers and associated data and recreates them. * * This slot gets called by serviceProviderDirChanged() if a global or local directory * containing service provider XML files has changed. It does not call this function on every * directory change, but only if there are no further changes in a short timespan. * The provider cache (ServiceProviderGlobal::cache()) gets updated for all changed provider * plugins. * @note Deleted data sources only get filled again when new requests are made, * they are not updated. * * @see serviceProviderDirChanged() **/ void reloadChangedProviders(); protected: /** * @brief This function gets called when a new data source gets requested. * * @param name The name of the requested data source. * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool sourceRequestEvent( const QString &name ); /** * @brief This function gets called when a data source gets requested or needs an update. * * It checks the source type of the data source with the given @p name using sourceTypeFromName * and then calls the corresponding update...() function for the source type. * All departure/arrival/journey/stop suggestion data sources are updated using * updateTimetableSource(). Other data sources are updated using updateServiceProviderSource(), * updateServiceProviderForCountrySource(), updateErroneousServiceProviderSource(), * updateLocationSource(). * * @param name The name of the data source to be updated. * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool updateSourceEvent( const QString &name ); /** * @brief Implementation of sourceRequestEvent() and updateSourceEvent() (@p update @c true). * * Requesting and updating a source is done in (almost) the same way. * @param data A SourceRequestData for the data source to be requested/updated. * @param update @c True, if the data source should be updated. @c False, otherwise. **/ bool requestOrUpdateSourceEvent( const SourceRequestData &data, bool update = false ); /** * @brief Updates the ServiceProviders data source. * * The data source contains information about all available service providers. * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool updateServiceProviderSource(); /** * @brief Updates the data source for the service provider mentioned in @p name. * * If a service provider ID is given in @p name, information about that service provider is * returned. If a location is given (ie. "international" or a two letter country code) * information about the default service provider for that location is returned. * * @param name The name of the data source. It starts with the ServiceProvider keyword and is * followed by a service provider ID or a location, ie "ServiceProvider de" or * "ServiceProvider de_db". * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool updateServiceProviderForCountrySource( const SourceRequestData &sourceData ); /** * @brief Updates the ErrornousServiceProviders data source. * * The data source contains information about all erroneous service providers. * * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool updateErroneousServiceProviderSource(); /** * @brief Updates the "Locations" data source. * * Locations with available service providers are determined by checking the list of valid * accessors for the countries they support. * * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool updateLocationSource(); /** * @brief Updates the timetable data source with the given @p data. * * Timetable data may be one of these here: Departure, arrivals, journeys (to or from the home * stop) or stop suggestions. * First it checks if the data source is up to date using isSourceUpToDate. If it's not, new * data gets requested using the associated accessor. The accessor gets retrieved using * getSpecificAccessor, which creates a new accessor if there is no cached value. * * Data may arrive asynchronously depending on the used accessor. * * @return @c True, if the data source could be updated successfully. @c False, otherwise. **/ bool updateTimetableDataSource( const SourceRequestData &data ); /** @brief Fill the VehicleTypes data source. */ void initVehicleTypesSource(); /** * @brief Wheather or not the data source with the given @p name is up to date. * * @param nonAmbiguousName The (non ambiguous) name of the source to be checked. * @return @c True, if the data source is up to date. @c False, otherwise. * @see disambiguateSourceName() **/ bool isSourceUpToDate( const QString &nonAmbiguousName, UpdateFlags updateFlags = DefaultUpdateFlags ); /** * @brief Get the minimum number of seconds until the next update of @p sourceName. * * Whether or not the time until the next automatic or manual update should be calculated * is defined by @p updateFlags. * This function gets used by secsUntilUpdate() and minSecsUntilUpdate(). **/ int getSecsUntilUpdate( const QString &sourceName, QString *errorMessage = 0, UpdateFlags updateFlags = DefaultUpdateFlags ); /** * @brief Test @p providerId for errors. * * @param providerId The ID of the provider to test. * @param providerData A pointer to a QVariantHash into which provider data gets inserted, * ie. the data that gets used for "ServiceProvider [providerId]" data sources. * @param errorMessage A pointer to a QString which gets set to an error message * if @c false gets returned. * @param cache A shared pointer to the provider cache. If an invalid pointer is given it * gets created using ServiceProviderGlobal::cache(). * @return @c True, if the service provider is valid, @c false otherwise. **/ - bool testServiceProvider( const QString &providerId, QVariantHash *providerData, + bool testServiceProvider( const QString &providerId, QVariantMap *providerData, QString *errorMessage, const QSharedPointer &cache = QSharedPointer() ); /** * @brief Request timetable data after checking the provider and the request. * * The request fails, if the requested provider is invalid, if an unsupported feature * is needed to run the request or if a @c city parameter is missing but needed by the * provider. * @param data A SourceRequestData object for the data source name that triggered the request. * @return @c True, if the request could be started successfully, @c false otherwise. * @see ServiceProvider::request() **/ bool request( const SourceRequestData &data ); private: /** @brief Get the date and time for the next update of @p dataSource. */ QDateTime sourceUpdateTime( TimetableDataSource *dataSource, UpdateFlags updateFlags = DefaultUpdateFlags ); /** @brief Handler for departuresReceived() (@p isDepartureData @c true) and arrivalsReceived() */ void timetableDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &items, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool deleteDepartureInfos = true, bool isDepartureData = true ); /** * @brief Gets information about @p provider for a service provider data source. * * If @p provider is 0, the provider cache file ServiceProvider::cacheFileName() gets checked. * If it contains all needed information which is not available in @p data no ServiceProvider * object needs to be created to fill the QHash with the service provider information. If there * are no cached values for the provider it gets created to get and cache those values. * * @param provider The accessor to get information about. May be 0, see above. * @return A QHash with values keyed with strings. **/ - QVariantHash serviceProviderData( const ServiceProviderData &data, + QVariantMap serviceProviderData( const ServiceProviderData &data, const ServiceProvider *provider = 0 ); /** @brief Overload, use if @p provider is @em not 0. */ - QVariantHash serviceProviderData( const ServiceProvider *provider ); + QVariantMap serviceProviderData( const ServiceProvider *provider ); /** * @brief Gets a hash with information about available locations. * * Locations are here countries plus some special locations like "international". **/ - QVariantHash locations(); + QVariantMap locations(); /** * @brief Get a pointer to the provider with the given @p providerId. * * If the provider to get is in use it does not need to be created. * @param providerId The ID of the provider to get. * @param newlyCreated If not 0 this gets set to @p true if the provider was newly created * and @p false if it was already created. * @return A pointer to the provider with the given @p providerId or an invalid pointer * if the provider could not be found or it's state is not "ready". **/ ProviderPointer providerFromId( const QString &providerId, bool *newlyCreated = 0 ); /** * @brief Delete the provider with the given @p providerId and clears all cached data. * * The provider to delete is considered invalid and will not be cached. * Data sources using the provider will be deleted if @p keepProviderDataSources is @c false, * which is @em not the default. **/ void deleteProvider( const QString &providerId, bool keepProviderDataSources = true ); /** @brief Publish the data of @p dataSource under it's data source name. */ void publishData( DataSource *dataSource, const QString &newlyRequestedProviderId = QString() ); /** * @brief Get a pointer to the ProvidersDataSource object of service provider data source(s). * The ProvidersDataSource objects provides data for the "ServiceProviders" and * "ServiceProvider [providerId]" data source(s). **/ ProvidersDataSource *providersDataSource() const; /** @brief Checks if the provider with the given @p providerId is used by a connected source. */ bool isProviderUsed( const QString &providerId ); /** * @brief Get an error code for the given @p stateId. * * The returned error code gets published in data sources if the provider state is not "ready" * and the provider cannot be used. **/ static ErrorCode errorCodeFromState( const QString &stateId ); /** * @brief Whether or not the state data with @p stateDataKey should be written to the cache. * * Dynamic state data gets invalid when the engine gets destroyed. Therefore they should not * be written to the provider cache. For example a 'progress' field is useless in the cache, * since the operation needs to be restarted if it was aborted, eg. because of a crash. **/ static bool isStateDataCached( const QString &stateDataKey ); /** * @brief Disambiguates the given @p sourceName. * * Date and time parameters get replaced by a @c datetime parameter, with the time rounded to * 15 minutes to use one data source object for all sources that are less than 15 minutes apart * from each other. * Parameters get reordered to a standard order, so that data source names with only a * different parameter order point to the same DataSource object. Parameter values get * converted to lower case (eg. stop names). * * Without doing this eg. departure data sources with only little different time values * will @em not share their departures and download them twice. **/ static QString disambiguateSourceName( const QString &sourceName ); /** @brief If @p providerId is empty return the default provider for the users country. */ static QString fixProviderId( const QString &providerId ); /** * @brief Get the service provider with the given @p serviceProviderId, if any. * * @param serviceProviderId The ID of the service provider to get. * The ID starts with a country code, followed by an underscore and it's name. * If it's empty, the default service provider for the users country will * be used, if there is any. **/ static ServiceProvider *createProvider( const QString &serviceProviderId = QString(), QObject *parent = 0, const QSharedPointer &cache = QSharedPointer(0) ); /** * @brief Create a provider for the given @p data. * @param data The data object to create a provider for. * @param parent The parent for the new provider **/ static ServiceProvider *createProviderForData( const ServiceProviderData *data, QObject *parent = 0, const QSharedPointer &cache = QSharedPointer(0) ); /** @brief Get the timetable data source that uses the given @p timer. */ TimetableDataSource *dataSourceFromTimer( QTimer *timer ) const; /** * @brief Update all provider data including the provider state. * * Uses updateProviderState() to update the state of the provider. **/ bool updateProviderData( const QString &providerId, const QSharedPointer &cache = QSharedPointer(0) ); bool enoughDataAvailable( DataSource *dataSource, const SourceRequestData &sourceData ) const; void startCleanupLater(); void startDataSourceCleanupLater( TimetableDataSource *dataSource ); // Implementation for the requestAdditionalData() slot, // call testDataSourceForAdditionalDataRequests() before and publishData() afterwards. // Both functions only need to be called once for multiple calls to this function. bool requestAdditionalData( const QString &sourceName, int updateItem, TimetableDataSource *dataSource ); // Should be called once before calling requestAdditionalData(), // if 0 gets returned all additional data requests on sourceName will fail TimetableDataSource *testDataSourceForAdditionalDataRequests( const QString &sourceName ); QHash< QString, ProviderPointer > m_providers; // Currently used providers by ID QHash< QString, ProviderPointer > m_cachedProviders; // Unused but still cached providers by ID - QVariantHash m_erroneousProviders; // Error messages for erroneous providers by ID + QVariantMap m_erroneousProviders; // Error messages for erroneous providers by ID QHash< QString, DataSource* > m_dataSources; // Data objects for data sources, stored by // unambiguous data source name QFileSystemWatcher *m_fileSystemWatcher; // Watches provider installation directories QTimer *m_providerUpdateDelayTimer; // Delays updates after changes in installation directories QTimer *m_cleanupTimer; // Cleanup cached providers after PROVIDER_CLEANUP_TIMEOUT QStringList m_runningSources; // Sources which are currently being processed }; #endif // Multiple inclusion guard diff --git a/engine/request.cpp b/engine/request.cpp index 93ce610..7e22207 100644 --- a/engine/request.cpp +++ b/engine/request.cpp @@ -1,217 +1,217 @@ /* * 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 "request.h" #ifdef BUILD_PROVIDER_TYPE_SCRIPT // Own includes #include "script/serviceproviderscript.h" // Qt includes #include #endif QString AbstractRequest::parseModeName() const { return parseModeName( m_parseMode ); } QString AbstractRequest::parseModeName( ParseDocumentMode parseMode ) { switch ( parseMode ) { case ParseForDepartures: return "departures"; case ParseForArrivals: return "arrivals"; case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: return "journeys"; case ParseForStopSuggestions: return "stopSuggestions"; case ParseInvalid: return "invalid"; default: return "unknown"; } } QString DepartureRequest::argumentsString() const { return QString("{stop: \"%1\", stopIsId: \"%2\", city: \"%3\", count: %4, " "dateTime: %5, dataType: %6}") .arg(m_stopId.isEmpty() ? m_stop : m_stopId) .arg(!m_stopId.isEmpty()).arg(m_city).arg(m_count) .arg(m_dateTime.toString(Qt::SystemLocaleShortDate), parseModeName()); } QString ArrivalRequest::argumentsString() const { return DepartureRequest::argumentsString(); } QString StopSuggestionRequest::argumentsString() const { return QString("{stop: \"%1\", city: \"%2\", count: %3}").arg(m_stop, m_city).arg(m_count); } QString StopsByGeoPositionRequest::argumentsString() const { return QString("{longitude: %1, longitude: %2, distance: %3, count: %4}") .arg(m_longitude).arg(m_latitude).arg(m_distance).arg(m_count); } QString AdditionalDataRequest::argumentsString() const { return QString("{dataType: %1, transportLine: \"%2\", " "target: \"%3\", dateTime: %4, routeDataUrl: %5}") .arg(parseModeName(), m_transportLine, m_target, m_dateTime.toString(Qt::SystemLocaleShortDate), m_routeDataUrl); } QString JourneyRequest::argumentsString() const { return QString("{city: \"%1\", count: %2, " "originStop: \"%3\", originStopIsId: \"%4\", " "targetStop: \"%5\", targetStopIsId: \"%6\", dateTime: %7}") .arg(m_city).arg(m_count) .arg(m_stopId.isEmpty() ? m_stop : m_stopId) .arg(!m_stopId.isEmpty()) .arg(m_targetStopId.isEmpty() ? m_targetStop : m_targetStopId) .arg(!m_targetStopId.isEmpty()) .arg(m_dateTime.toString(Qt::SystemLocaleShortDate)); } QString MoreItemsRequest::argumentsString() const { Q_ASSERT( m_request ); return Enums::toString(m_direction) + ": " + m_request->argumentsString(); } #ifdef BUILD_PROVIDER_TYPE_SCRIPT QString StopSuggestionRequest::functionName() const { return ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS; } QString DepartureRequest::functionName() const { // Same name for ArrivalRequest return ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE; } QString JourneyRequest::functionName() const { return ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS; } QString AdditionalDataRequest::functionName() const { return ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA; } QScriptValue StopSuggestionRequest::toScriptValue( QScriptEngine *engine ) const { QScriptValue value = engine->newObject(); value.setProperty( QLatin1String("stop"), m_stop ); value.setProperty( QLatin1String("city"), m_city ); value.setProperty( QLatin1String("count"), m_count ); return value; } QScriptValue StopsByGeoPositionRequest::toScriptValue( QScriptEngine *engine ) const { QScriptValue value = engine->newObject(); value.setProperty( QLatin1String("longitude"), m_longitude ); value.setProperty( QLatin1String("latitude"), m_latitude ); value.setProperty( QLatin1String("distance"), m_distance ); value.setProperty( QLatin1String("count"), m_count ); return value; } QScriptValue DepartureRequest::toScriptValue( QScriptEngine *engine ) const { QScriptValue value = engine->newObject(); value.setProperty( QLatin1String("stop"), m_stopId.isEmpty() ? m_stop : m_stopId ); value.setProperty( QLatin1String("stopIsId"), !m_stopId.isEmpty() ); value.setProperty( QLatin1String("city"), m_city ); value.setProperty( QLatin1String("count"), m_count ); value.setProperty( QLatin1String("dateTime"), engine->newDate(m_dateTime) ); value.setProperty( QLatin1String("dataType"), parseModeName() ); value.setProperty( QLatin1String("moreItemsDirection"), Enums::RequestedItems ); return value; } QScriptValue ArrivalRequest::toScriptValue( QScriptEngine *engine ) const { QScriptValue value = DepartureRequest::toScriptValue( engine ); value.setProperty( QLatin1String("dataType"), parseModeName() ); return value; } QScriptValue JourneyRequest::toScriptValue( QScriptEngine *engine ) const { QScriptValue value = engine->newObject(); value.setProperty( QLatin1String("stop"), m_stopId.isEmpty() ? m_stop : m_stopId ); value.setProperty( QLatin1String("stopIsId"), !m_stopId.isEmpty() ); value.setProperty( QLatin1String("city"), m_city ); value.setProperty( QLatin1String("count"), m_count ); value.setProperty( QLatin1String("originStop"), m_stopId.isEmpty() ? m_stop : m_stopId ); // Already in argument as "stop" value.setProperty( QLatin1String("originStopIsId"), !m_stopId.isEmpty() ); // Already in argument as "stopId" value.setProperty( QLatin1String("targetStop"), m_targetStopId.isEmpty() ? m_targetStop : m_targetStopId ); value.setProperty( QLatin1String("targetStopIsId"), !m_targetStopId.isEmpty() ); value.setProperty( QLatin1String("dateTime"), engine->newDate(m_dateTime) ); value.setProperty( QLatin1String("moreItemsDirection"), Enums::RequestedItems ); return value; } QScriptValue AdditionalDataRequest::toScriptValue( QScriptEngine *engine ) const { QScriptValue value = engine->newObject(); value.setProperty( QLatin1String("stop"), m_stopId.isEmpty() ? m_stop : m_stopId ); value.setProperty( QLatin1String("stopIsId"), !m_stop.isEmpty() ); value.setProperty( QLatin1String("city"), m_city ); value.setProperty( QLatin1String("dataType"), m_sourceName.startsWith(QLatin1String("Arrivals"), Qt::CaseInsensitive) ? parseModeName(ParseForArrivals) : parseModeName(ParseForDepartures)); value.setProperty( QLatin1String("transportLine"), m_transportLine ); value.setProperty( QLatin1String("target"), m_target ); value.setProperty( QLatin1String("dateTime"), engine->newDate(m_dateTime) ); value.setProperty( QLatin1String("routeDataUrl"), m_routeDataUrl ); return value; } QScriptValue MoreItemsRequest::toScriptValue( QScriptEngine *engine ) const { Q_ASSERT( m_request ); QScriptValue value = m_request->toScriptValue( engine ); QScriptValue data = engine->newObject(); - for ( QVariantHash::ConstIterator it = m_requestData.constBegin(); + for ( QVariantMap::ConstIterator it = m_requestData.constBegin(); it != m_requestData.constEnd(); ++it ) { data.setProperty( it.key(), engine->toScriptValue(it.value()) ); } value.setProperty( QLatin1String("requestData"), data ); //engine->toScriptValue(requestData) ); value.setProperty( QLatin1String("moreItemsDirection"), m_direction ); return value; } #endif // BUILD_PROVIDER_TYPE_SCRIPT diff --git a/engine/request.h b/engine/request.h index 7694d3d..b828317 100644 --- a/engine/request.h +++ b/engine/request.h @@ -1,402 +1,402 @@ /* * 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 classes describing (timetable data) requests. * @author Friedrich Pülz */ #ifndef REQUEST_HEADER #define REQUEST_HEADER // Own includes #include "config.h" #include "enums.h" // Qt includes #include /** * @brief Stores information about a request to the publictransport data engine. * * All values other than @p sourceName can be derived (parsed) from the source name. **/ class AbstractRequest { public: AbstractRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseInvalid ) : m_sourceName(sourceName), m_parseMode(parseMode) { }; AbstractRequest( const AbstractRequest &request ) : m_sourceName(request.m_sourceName), m_parseMode(request.m_parseMode) { }; virtual ~AbstractRequest() {}; virtual AbstractRequest *clone() const = 0; virtual QString argumentsString() const = 0; /** * @brief The requesting source name. * Other values in AbstractRequest are derived from this string. **/ QString sourceName() const { return m_sourceName; }; /** @brief Describes what should be retrieved with the request, eg. departures or a a stop ID. */ ParseDocumentMode parseMode() const { return m_parseMode; }; void setParseMode( ParseDocumentMode parseMode ) { m_parseMode = parseMode; }; QString parseModeName() const; static QString parseModeName( ParseDocumentMode parseMode ); #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const = 0; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const = 0; #endif protected: QString m_sourceName; ParseDocumentMode m_parseMode; }; class AbstractTimetableItemRequest : public AbstractRequest { public: AbstractTimetableItemRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseInvalid ) : AbstractRequest(sourceName, parseMode), m_count(20) {}; AbstractTimetableItemRequest( const QString &sourceName, const QString &stop, const QString &stopId, const QDateTime &dateTime, int count, const QString &city = QString(), ParseDocumentMode parseMode = ParseInvalid ) : AbstractRequest(sourceName, parseMode), m_dateTime(dateTime), m_stop(stop), m_stopId(stopId), m_count(count), m_city(city) {}; AbstractTimetableItemRequest( const AbstractTimetableItemRequest &other ) : AbstractRequest(other), m_dateTime(other.m_dateTime), m_stop(other.m_stop), m_stopId(other.m_stopId), m_count(other.m_count), m_city(other.m_city) {}; virtual ~AbstractTimetableItemRequest() {}; virtual AbstractRequest *clone() const = 0; virtual QString argumentsString() const = 0; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const = 0; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const = 0; #endif /** @brief The date and time to get results for. */ QDateTime dateTime() const { return m_dateTime; }; /** @brief The stop name of the request. */ QString stop() const { return m_stop; }; /** @brief The stop ID of the request. */ QString stopId() const { return m_stopId; }; /** * @brief The number of timetable items to request, eg. departures or stop suggestions. * @note This is just a hint for the provider **/ int count() const { return m_count; }; /** * @brief The city to get stop suggestions for. * This is only needed if @ref ServiceProvider::useSeparateCityValue returns @c true. **/ QString city() const { return m_city; }; void setDateTime( const QDateTime &dateTime ) { m_dateTime = dateTime; }; void setStop( const QString &stop ) { m_stop = stop; }; void setStopId( const QString &stopId ) { m_stopId = stopId; }; void setCity( const QString &city ) { m_city = city; }; void setCount( int count ) { m_count = count; }; protected: /** @brief The date and time to get results for. */ QDateTime m_dateTime; QString m_stop; QString m_stopId; int m_count; QString m_city; }; class StopSuggestionRequest : public AbstractTimetableItemRequest { public: StopSuggestionRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseForStopSuggestions ) : AbstractTimetableItemRequest(sourceName, parseMode) {}; StopSuggestionRequest( const QString &sourceName, const QString &stop, int count, const QString &city = QString(), ParseDocumentMode parseMode = ParseForStopSuggestions ) : AbstractTimetableItemRequest(sourceName, stop, QString(), QDateTime(), count, city, parseMode) {}; StopSuggestionRequest( const StopSuggestionRequest &other ) : AbstractTimetableItemRequest(other) {}; virtual AbstractRequest *clone() const { return new StopSuggestionRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const; #endif }; class StopsByGeoPositionRequest : public StopSuggestionRequest { public: StopsByGeoPositionRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseForStopSuggestions ) : StopSuggestionRequest(sourceName, parseMode), m_longitude(0.0), m_latitude(0.0), m_distance(5000) {}; StopsByGeoPositionRequest( const QString &sourceName, qreal longitude, qreal latitude, int count = 200, int distance = 5000, ParseDocumentMode parseMode = ParseForStopSuggestions ) : StopSuggestionRequest(sourceName, QString(), count, QString(), parseMode), m_longitude(longitude), m_latitude(latitude), m_distance(distance) {}; StopsByGeoPositionRequest( const StopsByGeoPositionRequest &request ) : StopSuggestionRequest(request), m_longitude(request.m_longitude), m_latitude(request.m_latitude), m_distance(request.m_distance) {}; virtual AbstractRequest *clone() const { return new StopsByGeoPositionRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; #endif /** @brief The longitude of the stop. */ qreal longitude() const { return m_longitude; }; /** @brief The latitude of the stop. */ qreal latitude() const { return m_latitude; }; /** @brief Maximal distance in meters from the position at longitude/latitude. */ int distance() const { return m_distance; }; void setLongitude( qreal longitude ) { m_longitude = longitude; }; void setLatitude( qreal latitude ) { m_latitude = latitude; }; void setDistance( int distance ) { m_distance = distance; }; protected: qreal m_longitude; qreal m_latitude; int m_distance; }; class DepartureRequest : public AbstractTimetableItemRequest { public: DepartureRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseForDepartures ) : AbstractTimetableItemRequest(sourceName, parseMode) {}; DepartureRequest( const QString &sourceName, const QString &stop, const QString &stopId, const QDateTime &dateTime, int count, const QString &city = QString(), ParseDocumentMode parseMode = ParseForDepartures ) : AbstractTimetableItemRequest(sourceName, stop, stopId, dateTime, count, city, parseMode) {}; DepartureRequest( const DepartureRequest &info ) : AbstractTimetableItemRequest(info) {}; virtual AbstractRequest *clone() const { return new DepartureRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const; #endif }; class ArrivalRequest : public DepartureRequest { public: ArrivalRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseForArrivals ) : DepartureRequest(sourceName, parseMode) {}; ArrivalRequest( const QString &sourceName, const QString &stop, const QString &stopId, const QDateTime &dateTime, int count, const QString &city = QString(), ParseDocumentMode parseMode = ParseForArrivals ) : DepartureRequest(sourceName, stop, stopId, dateTime, count, city, parseMode) {}; virtual AbstractRequest *clone() const { return new ArrivalRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; #endif }; /** * @brief Stores information about a request for journeys to the publictransport data engine. * * All values other than @p sourceName are derived (parsed) from it or represent the current state * of the request, eg. @p roundTrips stores the number of requests sent to a server to get * enough data (each round trip adds some data). **/ class JourneyRequest : public AbstractTimetableItemRequest { public: JourneyRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseForJourneysByDepartureTime ) : AbstractTimetableItemRequest(sourceName, parseMode), m_roundTrips(0) { }; JourneyRequest( const QString &sourceName, const QString &startStop, const QString &startStopId, const QString &targetStop, const QString &targetStopId, const QDateTime &dateTime, int count, const QString &urlToUse, const QString &city = QString(), ParseDocumentMode parseMode = ParseForJourneysByDepartureTime ) : AbstractTimetableItemRequest(sourceName, startStop, startStopId, dateTime, count, city, parseMode), m_targetStop(targetStop), m_targetStopId(targetStopId), m_urlToUse(urlToUse), m_roundTrips(0) { }; JourneyRequest( const JourneyRequest &other ) : AbstractTimetableItemRequest(other), m_targetStop(other.m_targetStop), m_targetStopId(other.m_targetStopId), m_urlToUse(other.m_urlToUse), m_roundTrips(other.m_roundTrips) { }; virtual JourneyRequest *clone() const { return new JourneyRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const; #endif /** @brief The target stop name of the request. */ QString targetStop() const { return m_targetStop; }; /** @brief The ID of the target stop of the request. */ QString targetStopId() const { return m_targetStopId; }; /** @brief Specifies the URL to use to download the journey source document. */ QString urlToUse() const { return m_urlToUse; }; /** @brief Current number of round trips used to fulfil the journey request. */ int roundTrips() const { return m_roundTrips; }; void setTargetStop( const QString &targetStop ) { m_targetStop = targetStop; }; void setTargetStopId( const QString &targetStopId ) { m_targetStopId = targetStopId; }; protected: QString m_targetStop; QString m_targetStopId; QString m_urlToUse; int m_roundTrips; }; class AdditionalDataRequest : public AbstractTimetableItemRequest { public: AdditionalDataRequest( const QString &sourceName = QString(), ParseDocumentMode parseMode = ParseForAdditionalData ) : AbstractTimetableItemRequest(sourceName, parseMode) {}; AdditionalDataRequest( const QString &sourceName, int itemNumber, const QString &stop, const QString &stopId, const QDateTime &dateTime, const QString &transportLine, const QString &target, const QString &city = QString(), const QString &routeDataUrl = QString(), ParseDocumentMode parseMode = ParseForAdditionalData ) : AbstractTimetableItemRequest(sourceName, stop, stopId, dateTime, 1, city, parseMode), m_itemNumber(itemNumber), m_transportLine(transportLine), m_target(target), m_routeDataUrl(routeDataUrl) {}; AdditionalDataRequest( const AdditionalDataRequest &other ) : AbstractTimetableItemRequest(other), m_itemNumber(other.m_itemNumber), m_transportLine(other.m_transportLine), m_target(other.m_target), m_routeDataUrl(other.m_routeDataUrl) {}; virtual AbstractRequest *clone() const { return new AdditionalDataRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const; #endif int itemNumber() const { return m_itemNumber; }; QString transportLine() const { return m_transportLine; }; QString target() const { return m_target; }; QString routeDataUrl() const { return m_routeDataUrl; }; protected: int m_itemNumber; QString m_transportLine; QString m_target; QString m_routeDataUrl; }; class MoreItemsRequest : public AbstractRequest { public: MoreItemsRequest( const QString &sourceName = QString(), const QSharedPointer &request = QSharedPointer(), - const QVariantHash &requestData = QVariantHash(), + const QVariantMap &requestData = QVariantMap(), Enums::MoreItemsDirection direction = Enums::LaterItems ) : AbstractRequest(sourceName, request ? request->parseMode() : ParseInvalid), m_request(request), m_requestData(requestData), m_direction(direction) { Q_ASSERT( request ); }; MoreItemsRequest( const MoreItemsRequest &other ) : AbstractRequest(other), m_request(other.m_request), m_requestData(other.m_requestData), m_direction(other.m_direction) {}; virtual AbstractRequest *clone() const { return new MoreItemsRequest(*this); }; virtual QString argumentsString() const; #ifdef BUILD_PROVIDER_TYPE_SCRIPT virtual QScriptValue toScriptValue( QScriptEngine *engine ) const; /** @brief Get the name of the script function that is associated with this request. */ virtual QString functionName() const { return QString(); }; // TODO #endif QSharedPointer< AbstractRequest > request() const { return m_request; }; - QVariantHash requestData() const { return m_requestData; }; + QVariantMap requestData() const { return m_requestData; }; Enums::MoreItemsDirection direction() const { return m_direction; }; protected: QSharedPointer< AbstractRequest > m_request; - QVariantHash m_requestData; + QVariantMap m_requestData; Enums::MoreItemsDirection m_direction; }; Q_DECLARE_METATYPE(DepartureRequest) Q_DECLARE_METATYPE(ArrivalRequest) Q_DECLARE_METATYPE(JourneyRequest) Q_DECLARE_METATYPE(StopSuggestionRequest) Q_DECLARE_METATYPE(StopsByGeoPositionRequest) Q_DECLARE_METATYPE(AdditionalDataRequest) #endif // Multiple inclusion guard diff --git a/engine/serviceprovider.cpp b/engine/serviceprovider.cpp index 805a120..763669d 100644 --- a/engine/serviceprovider.cpp +++ b/engine/serviceprovider.cpp @@ -1,292 +1,292 @@ /* * 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 "serviceprovider.h" // Own includes #include "serviceproviderdata.h" #include "serviceprovidertestdata.h" #include "serviceproviderglobal.h" #include "serviceproviderdatareader.h" #include "departureinfo.h" #include "request.h" // KDE includes #include #include #include #include #include #include // Qt includes #include #include #include #include #include ServiceProvider::ServiceProvider( const ServiceProviderData *data, QObject *parent, const QSharedPointer< KConfig > &cache ) : QObject(parent), m_data(data ? data : new ServiceProviderData(Enums::InvalidProvider, QString(), this)) { Q_UNUSED( cache ); const_cast(m_data)->setParent( this ); m_idAlreadyRequested = false; qRegisterMetaType< StopInfoList >( "StopInfoList" ); qRegisterMetaType< ArrivalInfoList >( "ArrivalInfoList" ); qRegisterMetaType< DepartureInfoList >( "DepartureInfoList" ); qRegisterMetaType< JourneyInfoList >( "JourneyInfoList" ); } ServiceProvider::~ServiceProvider() { } ServiceProviderTestData ServiceProvider::runSubTypeTest( const ServiceProviderTestData &oldTestData, const QSharedPointer< KConfig > cache ) const { if ( oldTestData.isSubTypeTestPending() || !isTestResultUnchanged(cache) ) { // Run subclass tests QString errorMessage; const bool testResult = runTests( &errorMessage ); // Write test result ServiceProviderTestData newTestData = oldTestData; newTestData.setSubTypeTestStatus( testResult ? ServiceProviderTestData::Passed : ServiceProviderTestData::Failed, errorMessage ); newTestData.write( id(), cache ); return newTestData; } else { return oldTestData; } } ServiceProvider *ServiceProvider::createProvider( Enums::ServiceProviderType type, QObject *parent ) { return new ServiceProvider( new ServiceProviderData(type), parent ); } bool ServiceProvider::isSourceFileModified( const QSharedPointer &cache ) const { return ServiceProviderGlobal::isSourceFileModified( m_data->id(), cache ); } void ServiceProvider::request( AbstractRequest *request ) { StopsByGeoPositionRequest *stopsByGeoPositionRequest = dynamic_cast< StopsByGeoPositionRequest* >( request ); if ( stopsByGeoPositionRequest ) { requestStopsByGeoPosition( *stopsByGeoPositionRequest ); return; } StopSuggestionRequest *stopSuggestionRequest = dynamic_cast< StopSuggestionRequest* >( request ); if ( stopSuggestionRequest ) { requestStopSuggestions( *stopSuggestionRequest ); return; } // ArrivalRequest is a derivate of DepartureRequest, therefore this test needs to be done before ArrivalRequest *arrivalRequest = dynamic_cast< ArrivalRequest* >( request ); if ( arrivalRequest ) { requestArrivals( *arrivalRequest ); return; } DepartureRequest *departureRequest = dynamic_cast< DepartureRequest* >( request ); if ( departureRequest ) { requestDepartures( *departureRequest ); return; } JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( journeyRequest ) { requestJourneys( *journeyRequest ); return; } } void ServiceProvider::requestDepartures( const DepartureRequest &request ) { Q_UNUSED( request ); kDebug() << "Not implemented"; return; } void ServiceProvider::requestArrivals( const ArrivalRequest &request ) { Q_UNUSED( request ); kDebug() << "Not implemented"; return; } void ServiceProvider::requestStopSuggestions( const StopSuggestionRequest &request ) { Q_UNUSED( request ); kDebug() << "Not implemented"; return; } void ServiceProvider::requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ) { Q_UNUSED( request ); kDebug() << "Not implemented"; return; } void ServiceProvider::requestJourneys( const JourneyRequest &request ) { Q_UNUSED( request ); kDebug() << "Not implemented"; return; } void ServiceProvider::requestAdditionalData( const AdditionalDataRequest &request ) { Q_UNUSED( request ); kDebug() << "Not implemented"; return; } void ServiceProvider::requestMoreItems( const MoreItemsRequest &moreItemsRequest ) { Q_UNUSED( moreItemsRequest ); kDebug() << "Not implemented"; return; } QString ServiceProvider::gethex( ushort decimal ) { QString hexchars = "0123456789ABCDEFabcdef"; return QChar( '%' ) + hexchars[decimal >> 4] + hexchars[decimal & 0xF]; } QString ServiceProvider::toPercentEncoding( const QString &str, const QByteArray &charset ) { QString unreserved = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~"; QString encoded; QByteArray ba = QTextCodec::codecForName( charset )->fromUnicode( str ); for ( int i = 0; i < ba.length(); ++i ) { char ch = ba[i]; if ( unreserved.indexOf(ch) != -1 ) { encoded += ch; } else if ( ch < 0 ) { encoded += gethex( 256 + ch ); } else { encoded += gethex( ch ); } } return encoded; } QByteArray ServiceProvider::charsetForUrlEncoding() const { return m_data->charsetForUrlEncoding(); } QString ServiceProvider::id() const { return m_data->id(); } Enums::ServiceProviderType ServiceProvider::type() const { return m_data->type(); } int ServiceProvider::minFetchWait( UpdateFlags updateFlags ) const { Q_UNUSED( updateFlags ) return qMax( 60, m_data->minFetchWait() ); } QDateTime ServiceProvider::nextUpdateTime( UpdateFlags updateFlags, const QDateTime &lastUpdate, const QDateTime &latestForSufficientChanges, - const QVariantHash &data ) const + const QVariantMap &data ) const { if ( !lastUpdate.isValid() ) { return QDateTime::currentDateTime(); } if ( updateFlags.testFlag(UpdateWasRequestedManually) ) { return lastUpdate.addSecs( minFetchWait(updateFlags) ); } // If the requested time is constant, wait until next midnight const QDateTime dateTime = QDateTime::currentDateTime(); QDateTime _latestForSufficientChanges = updateFlags.testFlag(SourceHasConstantTime) ? QDateTime(dateTime.date().addDays(1)) : latestForSufficientChanges; if ( isRealtimeDataAvailable(data) ) { // Wait maximally 30 minutes until an update if realtime data is available, // for more updates the timetable service must be used to request an update manually return _latestForSufficientChanges.isValid() ? qBound( lastUpdate.addSecs(minFetchWait(updateFlags)), _latestForSufficientChanges, lastUpdate.addSecs(30 * 60) ) : lastUpdate.addSecs(minFetchWait(updateFlags)); } else { // No realtime data, no need to update existing timetable items, // only update to have enough valid items for the data source. // With constant time update only at midnight for dynamic date. // With dynamic time (eg. the current time) update to have enough items available // while old ones get removed as time passes by. return _latestForSufficientChanges.isValid() ? qMax( lastUpdate.addSecs(minFetchWait()), _latestForSufficientChanges ) : lastUpdate.addSecs(minFetchWait()); } } -bool ServiceProvider::isRealtimeDataAvailable( const QVariantHash &data ) const +bool ServiceProvider::isRealtimeDataAvailable( const QVariantMap &data ) const { return features().contains(Enums::ProvidesDelays) && data.contains("delayInfoAvailable") ? data["delayInfoAvailable"].toBool() : false; } QString ServiceProvider::country() const { return m_data->country(); } QStringList ServiceProvider::cities() const { return m_data->cities(); } QString ServiceProvider::credit() const { return m_data->credit(); } bool ServiceProvider::useSeparateCityValue() const { return m_data->useSeparateCityValue(); } bool ServiceProvider::onlyUseCitiesInList() const { return m_data->onlyUseCitiesInList(); } diff --git a/engine/serviceprovider.h b/engine/serviceprovider.h index 4e67f0b..355d018 100644 --- a/engine/serviceprovider.h +++ b/engine/serviceprovider.h @@ -1,411 +1,411 @@ /* * 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 the base class for all service provider plugins * used by the public transport data engine. * @author Friedrich Pülz */ #ifndef SERVICEPROVIDER_HEADER #define SERVICEPROVIDER_HEADER // Own includes #include "enums.h" #include "departureinfo.h" // KDE includes #include // Qt includes #include #include class QScriptEngine; class KConfig; class KConfigGroup; class KJob; class AbstractRequest; class DepartureRequest; class ArrivalRequest; class StopSuggestionRequest; class StopsByGeoPositionRequest; class AdditionalDataRequest; class JourneyRequest; class MoreItemsRequest; class StopInfo; class DepartureInfo; class JourneyInfo; class ServiceProviderTestData; class ServiceProviderData; class ChangelogEntry; /** * @brief Get timetable information for public transport from different service providers. * * This class can be instantiated to create an invalid service provider. To create a valid * provider instantiate one of the derivates of this class. * The easiest way to implement support for a new service provider is to add an XML file describing * the service provider and a script to parse timetable documents. * If that's not enough a new class can be derived from ServiceProvider or it's derivates. * These functions should then be overwritten: * @li requestDepartures() * @li requestArrivals() * @li requestStopSuggestions() * @li requestJourneys() * If one of these functions isn't overridden, the associated timetable data can not be accessed * from the service provider. **/ class ServiceProvider : public QObject { Q_OBJECT Q_PROPERTY( QString id READ id ) Q_PROPERTY( QList features READ features ) Q_PROPERTY( QString country READ country ) Q_PROPERTY( QStringList cities READ cities ) Q_PROPERTY( QString credit READ credit ) Q_PROPERTY( int minFetchWait READ minFetchWait ) Q_PROPERTY( bool useSeparateCityValue READ useSeparateCityValue ) Q_PROPERTY( bool onlyUseCitiesInList READ onlyUseCitiesInList ) public: /** * @brief Constructs a new ServiceProvider object. * * You should use getSpecificProvider() to get an service provider that can download and * parse documents from the given service provider ID. * * @param data The data eg. read from a service provider plugin XML file, to construct a * ServiceProvider for. If this is 0 an invalid ServiceProvider gets created. * ServiceProvider takes ownership for @p data. * @param parent The parent QObject. **/ explicit ServiceProvider( const ServiceProviderData *data = 0, QObject *parent = 0, const QSharedPointer &cache = QSharedPointer(0) ); /** @brief Destructor. */ virtual ~ServiceProvider(); /** @brief Create a new provider of the given @p type. */ static ServiceProvider *createProvider( Enums::ServiceProviderType type, QObject *parent = 0 ); /** @brief Create an invalid provider. */ static inline ServiceProvider *createInvalidProvider( QObject *parent = 0 ) { return createProvider(Enums::InvalidProvider, parent); }; /** * @brief Whether or not the cached test result is unchanged. * * Derived classes should override this function to indicate when test results might have * changed, eg. because an additionally needed file has been modified. If true gets returned * this prevents that runTests() gets called again. If false gets returned runTests() will * be called again the next time the ServiceProvider object gets created. * * @param cache A shared pointer to the cache. * @see runTests() **/ virtual bool isTestResultUnchanged( const QSharedPointer &cache ) const { Q_UNUSED( cache ); return true; }; ServiceProviderTestData runSubTypeTest( const ServiceProviderTestData &oldTestData, const QSharedPointer< KConfig > cache ) const; /** @brief Get the ID of this service provider. */ QString id() const; /** @brief Get the type of this provider. */ Enums::ServiceProviderType type() const; /** @brief Whether or not the source XML file was modified since the cache was last updated. */ bool isSourceFileModified( const QSharedPointer &cache ) const; /** * @brief Get the minimum seconds to wait between two data-fetches from the service provider. * * The default implementation simply takes the value from ServiceProviderData::minFetchWait(), * but minimally 60 seconds to not produce too many updates. * @param updateFlags Flags to take into consideration when calculating the result, eg. whether * or not the result gets used for a manual data source update. **/ virtual int minFetchWait( UpdateFlags updateFlags = DefaultUpdateFlags ) const; /** * @brief Get the date and time when new data should be fetched from the service provider. * * @param updateFlags Flags to take into consideration when calculating the result, eg. whether * or not the result gets used for a manual data source update. * @param lastUpdate The date and time of the last update of the data. Must be valid, there * cannot be a 'next' update without a previous one. * @param latestForSufficientChanges The latest date and time at which an update should be made * to not run out of timetable items over time (passed items get removed). This is not used * if @p updateFlags has the SourceHasConstantTime flag set. Nevertheless it must be valid. * @see minFetchWait() **/ virtual QDateTime nextUpdateTime( UpdateFlags updateFlags, const QDateTime &lastUpdate, const QDateTime &latestForSufficientChanges, - const QVariantHash &data = QVariantHash() ) const; + const QVariantMap &data = QVariantMap() ) const; /** * @brief Get a list of features that this provider supports. * The default implementation returns an empty list. **/ virtual QList features() const { return QList(); }; /** @brief The country for which the provider returns results. */ QString country() const; /** @brief A list of cities for which the service providers returns results. */ QStringList cities() const; /** @brief Get a credit string to be shown with the timetable data. */ QString credit() const; /** @brief Get a pointer to the ServiceProviderData object for this provider. */ const ServiceProviderData *data() const { return m_data; }; /** @brief Get the number of currently running requests. */ virtual int runningRequests() const { return 0; }; /** @brief Abort all currently running requests. */ virtual void abortAllRequests() {}; /** * @brief Checks the type of @p request and calls the associated request function. * * Calls requestDepartures() if @p request is of type DepartureRequest, requestArrivals() * if @p request is of type ArrivalRequest and so on. The request object being pointed to * by @p request can be deleted after calling this function. **/ void request( AbstractRequest *request ); /** * @brief Request departures as described in @p request. * * When the departures are completely received departuresReceived() gets emitted. * The default implementation does nothing. * @param request Information about the departure request. **/ virtual void requestDepartures( const DepartureRequest &request ); /** * @brief Request arrivals as described in @p request. * * When the arrivals are completely received arrivalsReceived() gets emitted. * The default implementation does nothing. * @param request Information about the arrival request. **/ virtual void requestArrivals( const ArrivalRequest &request ); /** * @brief Request journeys as described in @p request. * * When the journeys are completely received journeysReceived() gets emitted. * The default implementation does nothing. * @param request Information about the journey request. **/ virtual void requestJourneys( const JourneyRequest &request ); /** * @brief Request stop suggestions as described in @p request. * * When the stop suggestions are completely received stopsReceived() gets emitted. * The default implementation does nothing. * @param request Information about the stop suggestion request. **/ virtual void requestStopSuggestions( const StopSuggestionRequest &request ); /** * @brief Request stops by geo position as described in @p request. * * When the stops are completely received stopsReceived() gets emitted. * The default implementation does nothing. * @param request Information about the stops by geo position request. **/ virtual void requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ); /** * @brief Request additional data for a valid timetable item in the engine. * * When the additional data is completely received additionalDataReceived() gets emitted. * The default implementation does nothing. * @param request Information about the additional data request. **/ virtual void requestAdditionalData( const AdditionalDataRequest &request ); /** @brief Request more items for a data source. */ virtual void requestMoreItems( const MoreItemsRequest &moreItemsRequest ); /** @brief Whether or not the city should be put into the "raw" url. */ virtual bool useSeparateCityValue() const; /** * @brief Whether or not cities may be chosen freely. * * @return @c True if only cities in the list returned by cities() are valid. * @return @c False (default) if cities may be chosen freely, but may be invalid. **/ virtual bool onlyUseCitiesInList() const; /** * @brief Encodes the url in @p str using the charset in @p charset. Then it is percent encoded. * * @see charsetForUrlEncoding() **/ static QString toPercentEncoding( const QString &str, const QByteArray &charset ); protected: /** * @brief Get the charset used to encode urls before percent-encoding them. Normally * this charset is UTF-8. But that doesn't work for sites that require parameters * in the url (..¶m=x) to be encoded in that specific charset. * * @see ServiceProvider::toPercentEncoding() **/ virtual QByteArray charsetForUrlEncoding() const; /** * @brief Run sub-type tests in derived classes. * * Derived classes should override this function to do additional tests, eg. test for * additionally needed files and their correctness. After this function has been called it's * result gets stored in the cache. The cached value gets used until isTestResultUnchanged() * returns false, the sub-type test gets marked as pending or the provider source XML file * gets modified. * The default implementation simply returns true. * * @param errorMessage If not 0, an error message gets stored in the QString being pointed to. * @return @c True, if all tests completed successfully, @c false otherwise. * @see isTestResultUnchanged() * @see ServiceProviderTestData::SubTypeTestPassed * @see ServiceProviderTestData::SubTypeTestFailed **/ virtual bool runTests( QString *errorMessage = 0 ) const { Q_UNUSED( errorMessage ); return true; }; QString m_curCity; /**< @brief Stores the currently used city. */ const ServiceProviderData *const m_data; /**< @brief Stores service provider data. * This ServiceProviderData object contains all static data needed by ServiceProvider. * ServiceProvider uses this object to request/receive the correct data and execute * the correct script for a specific service provider. * To get a non-const copy of this object, use ServiceProviderData::clone(). */ signals: /** * @brief Emitted when a new departure list has been received. * * @param provider The provider that was used to get the departures. * @param requestUrl The url used to request the information. * @param departures A list of departures that were received. * @param request Information about the request for the just received departure list. * @see ServiceProvider::useSeperateCityValue() **/ void departuresReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &departures, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request ); /** * @brief Emitted when a new arrival list has been received. * * @param provider The provider that was used to get the arrivals. * @param requestUrl The url used to request the information. * @param arrivals A list of arrivals that were received. * @param request Information about the request for the just received arrival list. * @see ServiceProvider::useSeperateCityValue() **/ void arrivalsReceived( ServiceProvider *provider, const QUrl &requestUrl, const ArrivalInfoList &arrivals, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request ); /** * @brief Emitted when a new journey list has been received. * * @param provider The provider that was used to get the journeys. * @param requestUrl The url used to request the information. * @param journeys A list of journeys that were received. * @param request Information about the request for the just received journey list. * @see ServiceProvider::useSeperateCityValue() **/ void journeysReceived( ServiceProvider *provider, const QUrl &requestUrl, const JourneyInfoList &journeys, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request ); /** * @brief Emitted when a list of stops has been received. * * @param provider The provider that was used to get the stops. * @param requestUrl The url used to request the information. * @param stops A pointer to a list of @ref StopInfo objects. * @param request Information about the request for the just received stop list. * @see ServiceProvider::useSeperateCityValue() **/ void stopsReceived( ServiceProvider *provider, const QUrl &requestUrl, const StopInfoList &stops, const StopSuggestionRequest &request ); /** * @brief Emitted when additional data has been received. * * @param provider The provider that was used to get additional data. * @param requestUrl The url used to request the information. * @param data A TimetableData object containing the additional data. * @param request Information about the request for the just received additional data. * @see ServiceProvider::useSeperateCityValue() **/ void additionalDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const TimetableData &data, const AdditionalDataRequest &request ); /** * @brief Emitted when an error occurred while parsing. * * @param provider The provider that was used to download and parse information * from the service provider. * @param errorCode The error code or NoError if there was no error. * @param errorString If @p errorCode isn't NoError this contains a * description of the error. * @param requestUrl The url used to request the information. * @param request Information about the request that resulted in the error. * @see ServiceProvider::useSeperateCityValue() **/ void requestFailed( ServiceProvider *provider, ErrorCode errorCode, const QString &errorString, const QUrl &requestUrl, const AbstractRequest *request ); void forceUpdate(); protected: /** * @brief Whether or not realtime data is available in the @p data of a timetable data source. * * The default implementation checks for the provider feature @c Enums::ProvidesDelays and * checks @p data for a "delayInfoAvailable" key and returns it's boolean value if it exists, * otherwise it returns @c false. */ - virtual bool isRealtimeDataAvailable( const QVariantHash &data ) const; + virtual bool isRealtimeDataAvailable( const QVariantMap &data ) const; private: static QString gethex( ushort decimal ); bool m_idAlreadyRequested; }; #endif // Multiple inclusion guard