diff --git a/engine/gtfs/gtfsdatabase.cpp b/engine/gtfs/gtfsdatabase.cpp index 8a0684a..da5cf48 100644 --- a/engine/gtfs/gtfsdatabase.cpp +++ b/engine/gtfs/gtfsdatabase.cpp @@ -1,373 +1,374 @@ /* * 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. */ #include "gtfsdatabase.h" #include #include -#include + #include #include #include #include #include #include #include +#include QString GtfsDatabase::databasePath( const QString &providerName ) { - const QString dir = KGlobal::dirs()->saveLocation("data", "plasma_engine_publictransport/gtfs/"); + const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "plasma_engine_publictransport/gtfs/"); return dir + providerName + ".sqlite"; } bool GtfsDatabase::initDatabase( const QString &providerName, QString *errorText ) { QSqlDatabase db = QSqlDatabase::database( providerName ); if ( !db.isValid() ) { db = QSqlDatabase::addDatabase( "QSQLITE", providerName ); if ( !db.isValid() ) { kDebug() << "Error adding a QSQLITE database" << db.lastError(); *errorText = "Error adding a QSQLITE database " + db.lastError().text(); return false; } db.setDatabaseName( databasePath(providerName) ); if ( !db.open() ) { kDebug() << "Error opening the database connection" << db.lastError(); *errorText = "Error opening the database connection " + db.lastError().text(); return false; } } return true; } bool GtfsDatabase::createDatabaseTables( QString *errorText, QSqlDatabase database ) { QSqlQuery query( database ); kDebug() << "Create tables"; // Create table for "agency.txt" TODO agency_id only referenced from routes => merge tables? query.prepare( "CREATE TABLE IF NOT EXISTS agency (" "agency_id INTEGER UNIQUE PRIMARY KEY, " // (optional for gtfs with a single agency) "agency_name VARCHAR(256) NOT NULL, " // (required) The name of the agency "agency_url VARCHAR(512) NOT NULL, " // (required) URL of the transit agency "agency_timezone VARCHAR(256), " // (required, if NULL, the default timezone from the accesor XML is used, from -tag) Timezone name, see http://en.wikipedia.org/wiki/List_of_tz_zones "agency_lang VARCHAR(2), " // (optional) A two-letter ISO 639-1 code for the primary language used by this transit agency "agency_phone VARCHAR(64)" // (optional) A single voice telephone number for the agency (can contain punctuation marks) // "agency_fare_url VARCHAR(512)" // (optional) URL of a website about fares of the transit agency (found in TriMet's GTFS data) ")" ); if( !query.exec() ) { kDebug() << "Error creating 'agency' table:" << query.lastError(); *errorText = "Error creating 'agency' table: " + query.lastError().text(); return false; } // Create table for "routes.txt" // Values for the "route_type" field: // 0 - Tram, Streetcar, Light rail. Any light rail or street level system within a metropolitan area. // 1 - Subway, Metro. Any underground rail system within a metropolitan area. // 2 - Rail. Used for intercity or long-distance travel. // 3 - Bus. Used for short- and long-distance bus routes. // 4 - Ferry. Used for short- and long-distance boat service. // 5 - Cable car. Used for street-level cable cars where the cable runs beneath the car. // 6 - Gondola, Suspended cable car. Typically used for aerial cable cars where the car is suspended from the cable. // 7 - Funicular. Any rail system designed for steep inclines. query.prepare( "CREATE TABLE IF NOT EXISTS routes (" "route_id INTEGER UNIQUE PRIMARY KEY NOT NULL, " // (required) "agency_id INTEGER, " // (optional) Defines an agency for the route "route_short_name VARCHAR(128), " // (required) The short name of a route (can be an empty (NULL in DB) string, then route_long_name is used) "route_long_name VARCHAR(256), " // (required) The long name of a route (can be an empty (NULL in DB) string, then route_short_name is used) "route_desc VARCHAR(256), " // (optional) Additional information "route_type INTEGER NOT NULL, " // (required) The type of transportation used on a route (see above) "route_url VARCHAR(512), " // (optional) URL of a web page about a particular route "route_color VARCHAR(6), " // (optional) The (background) color for a route as a six-character hexadecimal number, eg. 00FFFF, default is white "route_text_color VARCHAR(6), " // (optional) The text color for a route as a six-character hexadecimal number, eg. 00FFFF, default is black "FOREIGN KEY(agency_id) REFERENCES agency(agency_id)" ")" ); if( !query.exec() ) { kDebug() << "Error creating 'routes' table:" << query.lastError(); *errorText = "Error creating 'routes' table: " + query.lastError().text(); return false; } // Create table for "stops.txt" query.prepare( "CREATE TABLE IF NOT EXISTS stops (" "stop_id INTEGER UNIQUE PRIMARY KEY NOT NULL, " // (required) "stop_code VARCHAR(30), " // (optional) Makes stops uniquely identifyable by passengers "stop_name VARCHAR(256) NOT NULL, " // (required) The name of the stop "stop_desc VARCHAR(256), " // (optional) Additional information "stop_lat REAL NOT NULL, " // (required) The WGS 84 latitude of the stop "stop_lon REAL NOT NULL, " // (required) The WGS 84 longitude of the stop from -180 to 180 "zone_id INTEGER, " // (optional) The fare zone ID for a stop => fare_rules.txt "stop_url VARCHAR(512), " // (optional) URL of a web page about a particular stop "location_type TINYINT, " // (optional) "1": Station (with one or more stops), "0" or NULL: Stop "direction VARCHAR(30), " // (optional) "position VARCHAR(30), " // TODO "parent_station INTEGER, " // (optional) stop_id of a parent station (with location_type == 1) "min_fare_id INTEGER, " // TODO "max_fare_id INTEGER" ");" ); if( !query.exec() ) { kDebug() << "Error creating 'stops' table:" << query.lastError() << query.lastQuery(); *errorText = "Error creating 'stops' table: " + query.lastError().text(); return false; } query.prepare( "CREATE INDEX IF NOT EXISTS stops_stop_name_id ON stops(stop_id, stop_name);" ); if( !query.exec() ) { kDebug() << "Error creating index for 'stop_name' in 'stops' table:" << query.lastError() << query.lastQuery(); *errorText = "Error creating index for 'stop_name' in 'stops' table: " + query.lastError().text(); return false; } // Not used // // Create table for "shapes.txt" // query.prepare( "CREATE TABLE IF NOT EXISTS shapes (" // "shape_id INTEGER UNIQUE PRIMARY KEY NOT NULL, " // (required) // "shape_pt_lat REAL NOT NULL, " // (required) The WGS 84 latitude of a point in a shape // "shape_pt_lon REAL NOT NULL, " // (required) The WGS 84 longitude of a point in a shape from -180 to 180 // "shape_pt_sequence INTEGER NOT NULL, " // (required) Associates the latitude and longitude of a shape point with its sequence order along the shape // "shape_dist_traveled REAL" // (optional) If used, positions a shape point as a distance traveled along a shape from the first shape point // ")" ); // if( !query.exec() ) { // kDebug() << "Error creating 'shapes' table:" << query.lastError(); // return false; // } // Create table for "trips.txt" query.prepare( "CREATE TABLE IF NOT EXISTS trips (" "trip_id INTEGER UNIQUE PRIMARY KEY NOT NULL, " // (required) TODO trip_id only referenced from (small) frequencies and (big) stop_times => merge? "route_id INTEGER NOT NULL, " // (required) Uniquely identifies a route (routes.txt) "service_id INTEGER NOT NULL, " // (required) Uniquely identifies a set of dates when service is available for one or more routes (in "calendar" or "calendar_dates") "trip_headsign VARCHAR(256), " // (optional) The text that appears on a sign that identifies the trip's destination to passengers "trip_short_name VARCHAR(256), " // (optional) The text that appears in schedules and sign boards to identify the trip to passengers "direction_id TINYINT, " // (optional) A binary value to distinguish between bi-directional trips with the same route_id "block_id INTEGER, " // (optional) Identifies the block to which the trip belongs. A block consists of two or more sequential trips made using the same vehicle, where a passenger can transfer from one trip to the next just by staying in the vehicle. "shape_id INTEGER, " // (optional) Uniquely identifies a shape (shapes.txt) "FOREIGN KEY(route_id) REFERENCES routes(route_id), " "FOREIGN KEY(shape_id) REFERENCES shapes(shape_id)" ")" ); if( !query.exec() ) { kDebug() << "Error creating 'trips' table:" << query.lastError(); *errorText = "Error creating 'trips' table: " + query.lastError().text(); return false; } // Create table for "stop_times.txt" query.prepare( "CREATE TABLE IF NOT EXISTS stop_times (" "trip_id INTEGER NOT NULL, " // (required) Uniquely identifies a trip (trips.txt) "arrival_time INTEGER NOT NULL, " // (required) Specifies the arrival time at a specific stop for a specific trip on a route, HH:MM:SS or H:MM:SS, can be > 23:59:59 for times on the next day, eg. for trips that span with multiple dates "departure_time INTEGER NOT NULL, " // (required) Specifies the departure time from a specific stop for a specific trip on a route, HH:MM:SS or H:MM:SS, can be > 23:59:59 for times on the next day, eg. for trips that span with multiple dates "stop_id INTEGER NOT NULL, " // (required) Uniquely identifies a stop (with location_type == 0, if used) "stop_sequence INTEGER NOT NULL, " // (required) Identifies the order of the stops for a particular trip "stop_headsign VARCHAR(256), " // (optional) The text that appears on a sign that identifies the trip's destination to passengers. Used to override the default trip_headsign when the headsign changes between stops. "pickup_type TINYINT, " // (optional) Indicates whether passengers are picked up at a stop as part of the normal schedule or whether a pickup at the stop is not available, 0: Regularly scheduled pickup, 1: No pickup available, 2: Must phone agency to arrange pickup, 3: Must coordinate with driver to arrange pickup, default is 0 "drop_off_type TINYINT, " // (optional) Indicates whether passengers are dropped off at a stop as part of the normal schedule or whether a drop off at the stop is not available, 0: Regularly scheduled drop off, 1: No drop off available, 2: Must phone agency to arrange drop off, 3: Must coordinate with driver to arrange drop off, default is 0 "shape_dist_traveled TINYINT, " // (optional) If used, positions a stop as a distance from the first shape point (same unit as in shapes.txt) "FOREIGN KEY(trip_id) REFERENCES trips(trip_id), " "FOREIGN KEY(stop_id) REFERENCES stops(stop_id), " "PRIMARY KEY(stop_id, departure_time, trip_id)" // makes inserts slow.. ");" ); if( !query.exec() ) { kDebug() << "Error creating 'stop_times' table:" << query.lastError(); *errorText = "Error creating 'stop_times' table: " + query.lastError().text(); return false; } // Create an index to quickly access trip information sorted by stop_sequence, // eg. for route stop lists for departures query.prepare( "CREATE INDEX IF NOT EXISTS stop_times_trip ON stop_times(trip_id, stop_sequence, stop_id);" ); if( !query.exec() ) { kDebug() << "Error creating index for 'trip_id' in 'stop_times' table:" << query.lastError(); *errorText = "Error creating index for 'trip_id' in 'stop_times' table: " + query.lastError().text(); return false; } // Create table for "calendar.txt" (exceptions in "calendar_dates.txt") query.prepare( "CREATE TABLE IF NOT EXISTS calendar (" "service_id INTEGER UNIQUE PRIMARY KEY NOT NULL, " // (required) Uiquely identifies a set of dates when service is available for one or more routes "weekdays VARCHAR(7) NOT NULL, " // (required) Combines GTFS fields monday-sunday into a string of '1' (available at that weekday) and '0' (not available) "start_date VARCHAR(8) NOT NULL, " // (required) Contains the start date for the service, in yyyyMMdd format "end_date VARCHAR(8) NOT NULL" // (required) Contains the end date for the service, in yyyyMMdd format ")" ); if( !query.exec() ) { kDebug() << "Error creating 'calendar' table:" << query.lastError(); *errorText = "Error creating 'calendar' table: " + query.lastError().text(); return false; } // Create table for "calendar_dates.txt" query.prepare( "CREATE TABLE IF NOT EXISTS calendar_dates (" "service_id INTEGER NOT NULL, " // (required) Uiquely identifies a set of dates when a service exception is available for one or more routes, Each (service_id, date) pair can only appear once in "calendar_dates", if the a service_id value appears in both "calendar" and "calendar_dates", the information in "calendar_dates" modifies the service information specified in "calendar", referenced by "trips" "date VARCHAR(8) NOT NULL, " // (required) Specifies a particular date when service availability is different than the norm, in yyyyMMdd format "exception_type TINYINT NOT NULL, " // (required) Indicates whether service is available on the date specified in the date field (1: The service has been added for the date, 2: The service has been removed) "PRIMARY KEY(service_id, date)" ")" ); if( !query.exec() ) { kDebug() << "Error creating 'calendar_dates' table:" << query.lastError(); *errorText = "Error creating 'calendar_dates' table: " + query.lastError().text(); return false; } // Create table for "fare_attributes.txt" query.prepare( "CREATE TABLE IF NOT EXISTS fare_attributes (" "fare_id INTEGER UNIQUE PRIMARY KEY NOT NULL, " // (required) Uniquely identifies a fare class "price DECIMAL(5,2) NOT NULL, " // (required) The fare price, in the unit specified by currency_type "currency_type VARCHAR(3) NOT NULL, " // (required) Defines the currency used to pay the fare, ISO 4217 alphabetical currency code, see http://www.iso.org/iso/en/prods-services/popstds/currencycodeslist.html "payment_method TINYINT NOT NULL, " // (required) Indicates when the fare must be paid (0: paid on board, 1: must be paid before boarding) "transfers TINYINT, " // (required in fare_attributes.txt, but may be empty => use NULL here) Specifies the number of transfers permitted on this fare (0: no transfers permitted on this fare, 1: passenger may transfer once, 2: passenger may transfer twice, (empty): Unlimited transfers are permitted) "transfer_duration INTEGER" // (optional) Specifies the length of time in seconds before a transfer expires ")" ); if( !query.exec() ) { kDebug() << "Error creating 'fare_attributes' table:" << query.lastError(); *errorText = "Error creating 'fare_attributes' table: " + query.lastError().text(); return false; } // Create table for "fare_rules.txt" query.prepare( "CREATE TABLE IF NOT EXISTS fare_rules (" "fare_id INTEGER NOT NULL, " // (required) Uniquely identifies a fare class "route_id INTEGER, " // (optional) Associates the fare ID with a route "origin_id INTEGER, " // (optional) Associates the fare ID with an origin zone ID "destination_id INTEGER, " // (optional) Associates the fare ID with a destination zone ID "contains_id INTEGER, " // (optional) Associates the fare ID with a zone ID (intermediate zone) "FOREIGN KEY(fare_id) REFERENCES fare_attributes(fare_id), " "FOREIGN KEY(route_id) REFERENCES routes(route_id), " "FOREIGN KEY(origin_id) REFERENCES stops(zone_id), " "FOREIGN KEY(destination_id) REFERENCES stops(zone_id), " "FOREIGN KEY(contains_id) REFERENCES stops(zone_id)" ")" ); if( !query.exec() ) { kDebug() << "Error creating 'fare_rules' table:" << query.lastError(); *errorText = "Error creating 'fare_rules' table: " + query.lastError().text(); return false; } // Create table for "frequencies.txt" query.prepare( "CREATE TABLE IF NOT EXISTS frequencies (" "trip_id INTEGER PRIMARY KEY NOT NULL, " // (required) Identifies a trip on which the specified frequency of service applies "start_time INTEGER NOT NULL, " // (required) Specifies the time at which service begins with the specified frequency, HH:MM:SS or H:MM:SS, can be > 23:59:59 for times on the next day, eg. for trips that span with multiple dates "end_time INTEGER NOT NULL, " // (required) Indicates the time at which service changes to a different frequency (or ceases) at the first stop in the trip, HH:MM:SS or H:MM:SS, can be > 23:59:59 for times on the next day, eg. for trips that span with multiple dates "headway_secs INTEGER NOT NULL, " // (required) Indicates the time between departures from the same stop (headway) for this trip type, during the time interval specified by start_time and end_time, in seconds "FOREIGN KEY(trip_id) REFERENCES trips(trip_id)" ")" ); if( !query.exec() ) { kDebug() << "Error creating 'frequencies' table:" << query.lastError(); *errorText = "Error creating 'frequencies' table: " + query.lastError().text(); return false; } // Create table for "transfers.txt" query.prepare( "CREATE TABLE IF NOT EXISTS transfers (" "from_stop_id INTEGER NOT NULL, " // (required) Identifies a stop or station where a connection between routes begins "to_stop_id INTEGER NOT NULL, " // (required) Identifies a stop or station where a connection between routes ends "transfer_type INTEGER NOT NULL, " // (required) Specifies the type of connection for the specified (from_stop_id, to_stop_id) pair (0 or empty: This is a recommended transfer point between two routes, 1: This is a timed transfer point between two routes. The departing vehicle is expected to wait for the arriving one, with sufficient time for a passenger to transfer between routes, 2: This transfer requires a minimum amount of time between arrival and departure to ensure a connection. The time required to transfer is specified by min_transfer_time, 3: Transfers are not possible between routes at this location) "min_transfer_time INTEGER, " // (optional) When a connection between routes requires an amount of time between arrival and departure (transfer_type=2), the min_transfer_time field defines the amount of time that must be available in an itinerary to permit a transfer between routes at these stops. The min_transfer_time must be sufficient to permit a typical rider to move between the two stops, including buffer time to allow for schedule variance on each route, in seconds "FOREIGN KEY(from_stop_id) REFERENCES stops(stop_id), " "FOREIGN KEY(to_stop_id) REFERENCES stops(stop_id)" ")" ); if( !query.exec() ) { kDebug() << "Error creating 'transfers' table:" << query.lastError(); *errorText = "Error creating 'transfers' table: " + query.lastError().text(); return false; } return true; } QVariant GtfsDatabase::convertFieldValue( const QByteArray &fieldValue, FieldType type ) { if ( fieldValue.isEmpty() ) { return QVariant(); } switch ( type ) { case Integer: return fieldValue.toInt(); case Double: return fieldValue.toDouble(); case Date: case Url: return fieldValue; case HashId: return qHash( fieldValue ); // Use the hash to convert string IDs case SecondsSinceMidnight: { // May contain hour values >= 24 (for times the next day), which is no valid QTime // Convert valid time format 'h:mm:ss' to 'hh:mm:ss' const QByteArray timeString = fieldValue.length() == 7 ? '0' + fieldValue : fieldValue; return timeString.left(2).toInt() * 60 * 60 + timeString.mid(3, 2).toInt() * 60 + timeString.right(2).toInt(); } case Color: { const QString trimmed = fieldValue.trimmed(); return trimmed.isEmpty() ? Qt::transparent : QColor('#' + trimmed); } case String: default: // TODO Make camel case if everything is upper case? return QString::fromUtf8( fieldValue ); } } GtfsDatabase::FieldType GtfsDatabase::typeOfField( const QString &fieldName ) { if ( fieldName == QLatin1String("min_transfer_time") || fieldName == QLatin1String("transfer_type") || fieldName == QLatin1String("headway_secs") || fieldName == QLatin1String("transfer_duration") || fieldName == QLatin1String("transfers") || fieldName == QLatin1String("payment_method") || fieldName == QLatin1String("exception_type") || fieldName == QLatin1String("shape_dist_traveled") || fieldName == QLatin1String("drop_off_type") || fieldName == QLatin1String("pickup_type") || fieldName == QLatin1String("stop_sequence") || fieldName == QLatin1String("shape_pt_sequence") || fieldName == QLatin1String("parent_station") || fieldName == QLatin1String("location_type") || fieldName == QLatin1String("route_type") ) { return Integer; } else if ( fieldName.endsWith("_id") ) { return HashId; } else if ( fieldName == QLatin1String("start_time") || fieldName == QLatin1String("end_time") || fieldName == QLatin1String("arrival_time") || fieldName == QLatin1String("departure_time") ) { return SecondsSinceMidnight; // A time stored as INTEGER } else if ( fieldName == QLatin1String("date") || fieldName == QLatin1String("startDate") || fieldName == QLatin1String("endDate") ) { return Date; } else if ( fieldName.endsWith(QLatin1String("_lat")) || fieldName.endsWith(QLatin1String("_lon")) || fieldName == QLatin1String("price") ) { return Double; } else if ( fieldName.endsWith(QLatin1String("_url")) ) { return Url; } else if ( fieldName.endsWith(QLatin1String("_color")) ) { return Color; } else { return String; } } diff --git a/engine/gtfs/serviceprovidergtfs.cpp b/engine/gtfs/serviceprovidergtfs.cpp index 0cd8804..4b93656 100644 --- a/engine/gtfs/serviceprovidergtfs.cpp +++ b/engine/gtfs/serviceprovidergtfs.cpp @@ -1,908 +1,908 @@ /* * 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 #include // Qt includes #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 ) { // 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" ); KConfigGroup op = m_service->operationDescription("updateGtfsDatabase"); op.writeEntry( "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/publictransportdataengine.cpp b/engine/publictransportdataengine.cpp index 4a8062b..610575c 100644 --- a/engine/publictransportdataengine.cpp +++ b/engine/publictransportdataengine.cpp @@ -1,2750 +1,2751 @@ /* * 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 // Qt includes #include #include #include #include #include +#include const int PublicTransportEngine::DEFAULT_TIME_OFFSET = 0; const int PublicTransportEngine::PROVIDER_CLEANUP_TIMEOUT = 10000; // 10 seconds Plasma::Service* PublicTransportEngine::serviceForSource( const QString &name ) { #ifdef BUILD_PROVIDER_TYPE_GTFS // Return the GTFS service for "GTFS" or "gtfs" names if ( name.toLower() == QLatin1String("gtfs") ) { GtfsService *service = new GtfsService( name, this ); service->setDestination( name ); connect( service, SIGNAL(finished(Plasma::ServiceJob*)), this, SLOT(gtfsServiceJobFinished(Plasma::ServiceJob*)) ); return service; } #endif // If the name of a data requesting source is given, return the timetable service const SourceType type = sourceTypeFromName( name ); if ( isDataRequestingSourceType(type) ) { const QString nonAmbiguousName = disambiguateSourceName( name ); if ( m_dataSources.contains(nonAmbiguousName) ) { // Data source exists TimetableService *service = new TimetableService( this, name, this ); service->setDestination( name ); return service; } } // No service for the given name found return 0; } void PublicTransportEngine::publishData( DataSource *dataSource, const QString &newlyRequestedProviderId ) { Q_ASSERT( dataSource ); TimetableDataSource *timetableDataSource = dynamic_cast< TimetableDataSource* >( dataSource ); if ( timetableDataSource ) { foreach ( const QString &usingDataSource, timetableDataSource->usingDataSources() ) { setData( usingDataSource, timetableDataSource->data() ); } return; } setData( dataSource->name(), dataSource->data() ); ProvidersDataSource *providersSource = dynamic_cast< ProvidersDataSource* >( dataSource ); if ( providersSource ) { // Update "ServiceProvider " sources, which are also contained in the // DataSource object for the "ServiceProviders" data source. // Check which data sources of changed providers of that type are connected QStringList sources = Plasma::DataEngine::sources(); const QStringList changedProviders = providersSource->takeChangedProviders(); foreach ( const QString changedProviderId, changedProviders ) { const QString providerSource = sourceTypeKeyword(ServiceProviderSource) + ' ' + changedProviderId; if ( sources.contains(providerSource) ) { // Found a data source for the current changed provider, clear it and set new data removeAllData( providerSource ); setData( providerSource, providersSource->providerData(changedProviderId) ); } } if ( !newlyRequestedProviderId.isEmpty() ) { const QString providerSource = sourceTypeKeyword(ServiceProviderSource) + ' ' + newlyRequestedProviderId; removeAllData( providerSource ); setData( providerSource, providersSource->providerData(newlyRequestedProviderId) ); } } } ProvidersDataSource *PublicTransportEngine::providersDataSource() const { const QString name = sourceTypeKeyword( ServiceProvidersSource ); ProvidersDataSource *dataSource = dynamic_cast< ProvidersDataSource* >( m_dataSources[name] ); Q_ASSERT_X( dataSource, "PublicTransportEngine::providersDataSource()", "ProvidersDataSource is not available in m_dataSources!" ); return dataSource; } #ifdef BUILD_PROVIDER_TYPE_GTFS bool PublicTransportEngine::tryToStartGtfsFeedImportJob( Plasma::ServiceJob *job ) { Q_ASSERT( job ); updateServiceProviderSource(); ProvidersDataSource *dataSource = providersDataSource(); Q_ASSERT( dataSource ); const QString providerId = job->property( "serviceProviderId" ).toString(); if ( dataSource->providerState(providerId) == QLatin1String("importing_gtfs_feed") ) { // GTFS feed already gets imported, cannot start another import job return false; } // Update provider state in service provider data source(s) QVariantHash stateData = dataSource->providerStateData( providerId ); const QString databasePath = GtfsDatabase::databasePath( providerId ); stateData[ "gtfsDatabasePath" ] = databasePath; stateData[ "gtfsDatabaseSize" ] = 0; stateData[ "progress" ] = 0; QString state; if ( job->operationName() == QLatin1String("importGtfsFeed") ) { state = "importing_gtfs_feed"; } else if ( job->operationName() == QLatin1String("deleteGtfsDatabase") ) { state = "gtfs_feed_import_pending"; } else { // The operations "updateGtfsDatabase" and "updateGtfsFeedInfo" can run in the background state = "ready"; } dataSource->setProviderState( providerId, state, stateData ); // Store the state in the cache QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); KConfigGroup group = cache->group( providerId ); group.writeEntry( "state", state ); KConfigGroup stateGroup = group.group( "stateData" ); for ( QVariantHash::ConstIterator it = stateData.constBegin(); it != stateData.constEnd(); ++it ) { if ( isStateDataCached(it.key()) ) { stateGroup.writeEntry( it.key(), it.value() ); } } publishData( dataSource ); // Connect to messages of the job to update the provider state connect( job, SIGNAL(infoMessage(KJob*,QString,QString)), this, SLOT(gtfsImportJobInfoMessage(KJob*,QString,QString)) ); connect( job, SIGNAL(percent(KJob*,ulong)), this, SLOT(gtfsImportJobPercent(KJob*,ulong)) ); // The import job can be started return true; } void PublicTransportEngine::gtfsServiceJobFinished( Plasma::ServiceJob *job ) { // Disconnect messages of the job disconnect( job, SIGNAL(infoMessage(KJob*,QString,QString)), this, SLOT(gtfsImportJobInfoMessage(KJob*,QString,QString)) ); disconnect( job, SIGNAL(percent(KJob*,ulong)), this, SLOT(gtfsImportJobPercent(KJob*,ulong)) ); // Check that the job was not canceled because another database job was already running const bool canAccessGtfsDatabase = job->property("canAccessGtfsDatabase").toBool(); const bool isAccessingGtfsDatabase = job->property("isAccessingGtfsDatabase").toBool(); const QString providerId = job->property( "serviceProviderId" ).toString(); if ( (!canAccessGtfsDatabase && isAccessingGtfsDatabase) || providerId.isEmpty() ) { // Invalid job or cancelled, because another import job is already running // for the provider return; } // Reset state in "ServiceProviders", "ServiceProvider " data sources // Do not read and give the current feed URL for the provider to updateProviderState(), // because the feed URL should not have changed since the beginning of the feed import QVariantHash stateData; const QString state = updateProviderState( providerId, &stateData, "GTFS", QString(), false ); ProvidersDataSource *dataSource = providersDataSource(); dataSource->setProviderState( providerId, state, stateData ); publishData( dataSource ); } void PublicTransportEngine::gtfsImportJobInfoMessage( KJob *job, const QString &plain, const QString &rich ) { // Update "ServiceProviders", "ServiceProvider " data sources const QString providerId = job->property( "serviceProviderId" ).toString(); ProvidersDataSource *dataSource = providersDataSource(); QVariantHash stateData = dataSource->providerStateData( providerId ); stateData[ "statusMessage" ] = plain; if ( rich.isEmpty() ) { stateData.remove( "statusMessageRich" ); } else { stateData[ "statusMessageRich" ] = rich; } dataSource->setProviderStateData( providerId, stateData ); publishData( dataSource ); } void PublicTransportEngine::gtfsImportJobPercent( KJob *job, ulong percent ) { // Update "ServiceProviders", "ServiceProvider " data sources const QString providerId = job->property( "serviceProviderId" ).toString(); ProvidersDataSource *dataSource = providersDataSource(); QVariantHash stateData = dataSource->providerStateData( providerId ); stateData[ "progress" ] = int( percent ); dataSource->setProviderStateData( providerId, stateData ); publishData( dataSource ); } #endif // BUILD_PROVIDER_TYPE_GTFS PublicTransportEngine::PublicTransportEngine( QObject* parent, const QVariantList& args ) : Plasma::DataEngine( parent, args ), m_fileSystemWatcher(0), m_providerUpdateDelayTimer(0), m_cleanupTimer(0) { // We ignore any arguments - data engines do not have much use for them Q_UNUSED( args ) // This prevents applets from setting an unnecessarily high update interval // and using too much CPU. // 60 seconds should be enough, departure / arrival times have minute precision (except for GTFS). setMinimumPollingInterval( 60000 ); // Cleanup the cache from obsolete data for providers that were uninstalled // while the engine was not running ServiceProviderGlobal::cleanupCache(); // Get notified when data sources are no longer used connect( this, SIGNAL(sourceRemoved(QString)), this, SLOT(slotSourceRemoved(QString)) ); // Get notified when the network state changes to update data sources, // which update timers were missed because of missing network connection QDBusConnection::sessionBus().connect( "org.kde.kded", "/modules/networkstatus", "org.kde.Solid.Networking.Client", "statusChanged", this, SLOT(networkStateChanged(uint)) ); // Create "ServiceProviders" and "ServiceProvider [providerId]" data source object const QString name = sourceTypeKeyword( ServiceProvidersSource ); m_dataSources.insert( name, new ProvidersDataSource(name) ); updateServiceProviderSource(); // Ensure the local provider installation directory exists in the users HOME and will not // get removed, by creating a small file in it so that the directory is never empty. // If an installation directory gets removed, the file system watcher would need to watch the // parent directory instead to get notified when it gets created again. const QString installationSubDirectory = ServiceProviderGlobal::installationSubDirectory(); - const QString saveDir = KGlobal::dirs()->saveLocation( "data", installationSubDirectory ); + 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 ); + const QStringList directories = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, installationSubDirectory ); m_fileSystemWatcher = new QFileSystemWatcher( directories ); connect( m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(serviceProviderDirChanged(QString)) ); } PublicTransportEngine::~PublicTransportEngine() { if ( !m_runningSources.isEmpty() || !m_providers.isEmpty() ) { qDebug() << m_runningSources.count() << "data sources are still being updated," << m_providers.count() << "providers used, abort and delete all providers"; QStringList providerIds = m_providers.keys(); foreach ( const QString &providerId, providerIds ) { deleteProvider( providerId, false ); } } delete m_fileSystemWatcher; delete m_providerUpdateDelayTimer; qDeleteAll( m_dataSources ); m_dataSources.clear(); // Providers cached in m_cachedProviders get deleted automatically (QSharedPointer) // and need no special handling, since they are not used currently } QStringList PublicTransportEngine::sources() const { QStringList sources = Plasma::DataEngine::sources(); sources << sourceTypeKeyword(LocationsSource) << sourceTypeKeyword(ServiceProvidersSource) << sourceTypeKeyword(ErroneousServiceProvidersSource) << sourceTypeKeyword(VehicleTypesSource); sources.removeDuplicates(); return sources; } void PublicTransportEngine::networkStateChanged( uint state ) { if ( state != 4 ) { // 4 => Connected, see Solid::Networking::Status return; } // Network is connected again, check for missed update timers in connected data sources for ( QHash::ConstIterator it = m_dataSources.constBegin(); it != m_dataSources.constEnd(); ++it ) { // Check if the current data source is a timetable data source, without running update TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( *it ); if ( !dataSource || m_runningSources.contains(it.key()) ) { continue; } // Check if the next automatic update time was missed (stored in the data source) const QDateTime nextAutomaticUpdate = dataSource->value("nextAutomaticUpdate").toDateTime(); if ( nextAutomaticUpdate <= QDateTime::currentDateTime() ) { // Found a timetable data source that should have been updated already and // is not currently being updated (not in m_runningSources). // This happens if there was no network connection while an automatic update // was triggered. If the system is suspended the QTimer's for automatic updates // are not triggered at all. // Do now manually request updates for all connected sources, ie. do what should // have been done in updateTimeout(). foreach ( const QString &sourceName, dataSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } } } } bool PublicTransportEngine::isProviderUsed( const QString &providerId ) { // Check if a request is currently running for the provider foreach ( const QString &runningSource, m_runningSources ) { if ( runningSource.contains(providerId) ) { return true; } } // Check if a data source is connected that uses the provider for ( QHash< QString, DataSource* >::ConstIterator it = m_dataSources.constBegin(); it != m_dataSources.constEnd(); ++it ) { Q_ASSERT( *it ); TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( *it ); if ( dataSource && dataSource->providerId() == providerId ) { return true; } } // The provider is not used any longer by the engine return false; } void PublicTransportEngine::slotSourceRemoved( const QString &sourceName ) { const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( m_dataSources.contains(nonAmbiguousName) ) { // If this is a timetable data source, which might be associated with multiple // ambiguous source names, check if this data source is still connected under other names TimetableDataSource *timetableDataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( timetableDataSource ) { timetableDataSource->removeUsingDataSource( sourceName ); if ( timetableDataSource->usageCount() > 0 ) { // The TimetableDataSource object is still used by other connected data sources return; } } if ( sourceName == sourceTypeKeyword(ServiceProvidersSource) ) { // Do not remove ServiceProviders data source return; } // If a provider was used by the source, // remove the provider if it is not used by another source const DataSource *dataSource = m_dataSources.take( nonAmbiguousName ); Q_ASSERT( dataSource ); if ( dataSource->data().contains("serviceProvider") ) { const QString providerId = dataSource->value("serviceProvider").toString(); if ( !providerId.isEmpty() && !isProviderUsed(providerId) ) { // Move provider from the list of used providers to the list of cached providers m_cachedProviders.insert( providerId, m_providers.take(providerId) ); } } // Start the cleanup timer, will delete cached providers after a timeout startCleanupLater(); // The data source is no longer used, delete it delete dataSource; } else if ( nonAmbiguousName.startsWith(sourceTypeKeyword(ServiceProviderSource)) ) { // A "ServiceProvider xx_xx" data source was removed, data for these sources // is stored in the "ServiceProviders" data source object in m_dataSources. // Remove not installed providers also from the "ServiceProviders" data source. const QString providerId = nonAmbiguousName.mid( QString(sourceTypeKeyword(ServiceProviderSource)).length() + 1 ); if ( !providerId.isEmpty() && !isProviderUsed(providerId) ) { // Test if the provider is installed const QStringList providerPaths = ServiceProviderGlobal::installedProviders(); bool isInstalled = false; foreach ( const QString &providerPath, providerPaths ) { const QString &installedProviderId = ServiceProviderGlobal::idFromFileName( providerPath ); if ( providerId == installedProviderId ) { isInstalled = true; break; } } if ( !isInstalled ) { qDebug() << "Remove provider" << providerId; // Provider is not installed, remove it from the "ServiceProviders" data source providersDataSource()->removeProvider( providerId ); removeData( sourceTypeKeyword(ServiceProvidersSource), providerId ); } } } } void PublicTransportEngine::startCleanupLater() { // Create the timer if it is not currently running if ( !m_cleanupTimer ) { m_cleanupTimer = new QTimer( this ); m_cleanupTimer->setInterval( PROVIDER_CLEANUP_TIMEOUT ); connect( m_cleanupTimer, SIGNAL(timeout()), this, SLOT(cleanup()) ); } // (Re)start the timer m_cleanupTimer->start(); } void PublicTransportEngine::cleanup() { // Delete the timer delete m_cleanupTimer; m_cleanupTimer = 0; // Remove all shared pointers of unused cached providers, // ie. delete the provider objects m_cachedProviders.clear(); } QVariantHash PublicTransportEngine::serviceProviderData( const ServiceProvider *provider ) { Q_ASSERT( provider ); return serviceProviderData( *(provider->data()), provider ); } QVariantHash PublicTransportEngine::serviceProviderData( const ServiceProviderData &data, const ServiceProvider *provider ) { QVariantHash dataServiceProvider; dataServiceProvider.insert( "id", data.id() ); dataServiceProvider.insert( "fileName", data.fileName() ); dataServiceProvider.insert( "type", ServiceProviderGlobal::typeName(data.type()) ); #ifdef BUILD_PROVIDER_TYPE_GTFS if ( data.type() == Enums::GtfsProvider ) { dataServiceProvider.insert( "feedUrl", data.feedUrl() ); } #endif #ifdef BUILD_PROVIDER_TYPE_SCRIPT if ( data.type() == Enums::ScriptedProvider ) { dataServiceProvider.insert( "scriptFileName", data.scriptFileName() ); } #endif dataServiceProvider.insert( "name", data.name() ); dataServiceProvider.insert( "url", data.url() ); dataServiceProvider.insert( "shortUrl", data.shortUrl() ); dataServiceProvider.insert( "country", data.country() ); dataServiceProvider.insert( "cities", data.cities() ); dataServiceProvider.insert( "credit", data.credit() ); dataServiceProvider.insert( "useSeparateCityValue", data.useSeparateCityValue() ); dataServiceProvider.insert( "onlyUseCitiesInList", data.onlyUseCitiesInList() ); dataServiceProvider.insert( "author", data.author() ); dataServiceProvider.insert( "shortAuthor", data.shortAuthor() ); dataServiceProvider.insert( "email", data.email() ); dataServiceProvider.insert( "description", data.description() ); dataServiceProvider.insert( "version", data.version() ); QStringList changelog; foreach ( const ChangelogEntry &entry, data.changelog() ) { changelog << QString( "%2 (%1): %3" ).arg( entry.version ).arg( entry.author ).arg( entry.description ); } dataServiceProvider.insert( "changelog", changelog ); // To get the list of features, ServiceProviderData is not enough // A given ServiceProvider or cached data gets used if available. Otherwise the ServiceProvider // gets created just to get the list of features const QSharedPointer cache = ServiceProviderGlobal::cache(); KConfigGroup providerGroup = cache->group( data.id() ); if ( provider ) { // Write features to the return value const QList< Enums::ProviderFeature > features = provider->features(); const QStringList featureStrings = ServiceProviderGlobal::featureStrings( features ); dataServiceProvider.insert( "features", featureStrings ); dataServiceProvider.insert( "featureNames", ServiceProviderGlobal::featureNames(features) ); // Make sure, features have been written to the cache providerGroup.writeEntry( "features", featureStrings ); } else { // Check stored feature strings and re-read features if an invalid string was found bool ok; QStringList featureStrings = providerGroup.readEntry("features", QStringList()); const bool featureListIsEmpty = featureStrings.removeOne("(none)"); QList< Enums::ProviderFeature > features = ServiceProviderGlobal::featuresFromFeatureStrings( featureStrings, &ok ); if ( (featureListIsEmpty || !featureStrings.isEmpty()) && ok ) { // Feature list could be read from cache dataServiceProvider.insert( "features", featureStrings ); dataServiceProvider.insert( "featureNames", ServiceProviderGlobal::featureNames(features) ); } else { qDebug() << "No cached feature data was found for provider" << data.id(); // No cached feature data was found for the provider, // create the provider to get the feature list and store it in the cache bool newlyCreated; ProviderPointer _provider = providerFromId( data.id(), &newlyCreated ); features = _provider->features(); featureStrings = ServiceProviderGlobal::featureStrings( features ); const QStringList featuresNames = ServiceProviderGlobal::featureNames( features ); // Remove provider from the list again to delete it if ( newlyCreated ) { m_providers.remove( data.id() ); } // If no features are supported write "(none)" to the cache // to indicate that features have been written to the cache if ( featureStrings.isEmpty() ) { featureStrings.append( "(none)" ); } // Write features to the return value dataServiceProvider.insert( "features", featureStrings ); dataServiceProvider.insert( "featureNames", featuresNames ); // Write features to cache providerGroup.writeEntry( "features", featureStrings ); } } return dataServiceProvider; } QVariantHash PublicTransportEngine::locations() { QVariantHash ret; const QStringList providers = ServiceProviderGlobal::installedProviders(); // Update ServiceProviders source to fill m_erroneousProviders updateServiceProviderSource(); foreach( const QString &provider, providers ) { if ( QFileInfo(provider).isSymLink() ) { // Service provider XML file is a symlink for a default service provider, skip it continue; } const QString providerFileName = QFileInfo( provider ).fileName(); const QString providerId = ServiceProviderGlobal::idFromFileName( providerFileName ); if ( m_erroneousProviders.contains(providerId) ) { // Service provider is erroneous continue; } const int pos = providerFileName.indexOf('_'); if ( pos > 0 ) { // Found an underscore (not the first character) // Cut location code from the service providers XML filename const QString location = providerFileName.mid( 0, pos ).toLower(); if ( !ret.contains(location) ) { // Location is not already added to [ret] // Get the filename of the default provider for the current location const QString defaultProviderFileName = ServiceProviderGlobal::defaultProviderForLocation( location ); // Extract service provider ID from the filename const QString defaultProviderId = ServiceProviderGlobal::idFromFileName( defaultProviderFileName ); // Store location values in a hash and insert it into [ret] QVariantHash locationHash; locationHash.insert( "name", location ); if ( location == "international" ) { locationHash.insert( "description", i18n("International providers. " "There is one for getting flight departures/arrivals.") ); } else { locationHash.insert( "description", i18n("Service providers for %1.", KGlobal::locale()->countryCodeToName(location)) ); } locationHash.insert( "defaultProvider", defaultProviderId ); ret.insert( location, locationHash ); } } } return ret; } PublicTransportEngine::ProviderPointer PublicTransportEngine::providerFromId( const QString &id, bool *newlyCreated ) { if ( m_providers.contains(id) ) { // The provider was already created and is currently used by the engine if ( newlyCreated ) { *newlyCreated = false; } return m_providers[ id ]; } else if ( m_cachedProviders.contains(id) ) { // The provider was already created, is now unused by the engine, // but is still cached (cleanup timeout not reached yet) if ( newlyCreated ) { *newlyCreated = false; } // Move provider back from the list of cached providers to the list of used providers const ProviderPointer provider = m_cachedProviders.take( id ); m_providers.insert( id, provider ); return provider; } else { // Provider not currently used or cached if ( newlyCreated ) { *newlyCreated = true; } // Try to create the provider ServiceProvider *provider = createProvider( id, this ); if ( !provider ) { // Return an invalid ProviderPointer, when the provider could not be created return ProviderPointer::create(); } // Check the state of the provider, it needs to be "ready" ProvidersDataSource *dataSource = providersDataSource(); if ( dataSource ) { const QString state = dataSource->providerState( id ); if ( state != QLatin1String("ready") ) { qWarning() << "Provider" << id << "is not ready, state is" << state; return ProviderPointer::create(); } } // Connect provider, when it was created successfully connect( provider, SIGNAL(departuresReceived(ServiceProvider*,QUrl,DepartureInfoList,GlobalTimetableInfo,DepartureRequest)), this, SLOT(departuresReceived(ServiceProvider*,QUrl,DepartureInfoList,GlobalTimetableInfo,DepartureRequest)) ); connect( provider, SIGNAL(arrivalsReceived(ServiceProvider*,QUrl,ArrivalInfoList,GlobalTimetableInfo,ArrivalRequest)), this, SLOT(arrivalsReceived(ServiceProvider*,QUrl,ArrivalInfoList,GlobalTimetableInfo,ArrivalRequest)) ); connect( provider, SIGNAL(journeysReceived(ServiceProvider*,QUrl,JourneyInfoList,GlobalTimetableInfo,JourneyRequest)), this, SLOT(journeysReceived(ServiceProvider*,QUrl,JourneyInfoList,GlobalTimetableInfo,JourneyRequest)) ); connect( provider, SIGNAL(stopsReceived(ServiceProvider*,QUrl,StopInfoList,StopSuggestionRequest)), this, SLOT(stopsReceived(ServiceProvider*,QUrl,StopInfoList,StopSuggestionRequest)) ); connect( provider, SIGNAL(additionalDataReceived(ServiceProvider*,QUrl,TimetableData,AdditionalDataRequest)), this, SLOT(additionalDataReceived(ServiceProvider*,QUrl,TimetableData,AdditionalDataRequest)) ); connect( provider, SIGNAL(requestFailed(ServiceProvider*,ErrorCode,QString,QUrl,const AbstractRequest*)), this, SLOT(requestFailed(ServiceProvider*,ErrorCode,QString,QUrl,const AbstractRequest*)) ); // Create a ProviderPointer for the created provider and // add it to the list of currently used providers const ProviderPointer pointer( provider ); m_providers.insert( id, pointer ); return pointer; } } bool PublicTransportEngine::updateServiceProviderForCountrySource( const SourceRequestData &data ) { QString providerId; if ( data.defaultParameter.contains('_') ) { // Seems that a service provider ID is given providerId = data.defaultParameter; } else { // Assume a country code in name if ( !updateServiceProviderSource() || !updateLocationSource() ) { return false; } // The defaultParameter stored in data is a location code // (ie. "international" or a two letter country code) QVariantHash locations = m_dataSources[ sourceTypeKeyword(LocationsSource) ]->data(); QVariantHash locationCountry = locations[ data.defaultParameter.toLower() ].toHash(); QString defaultProvider = locationCountry[ "defaultProvider" ].toString(); if ( defaultProvider.isEmpty() ) { // No provider for the location found return false; } providerId = defaultProvider; } updateProviderData( providerId ); publishData( providersDataSource(), providerId ); return true; } bool PublicTransportEngine::updateProviderData( const QString &providerId, const QSharedPointer &cache ) { QVariantHash providerData; QString errorMessage; ProvidersDataSource *providersSource = providersDataSource(); // Test if the provider is valid if ( testServiceProvider(providerId, &providerData, &errorMessage, cache) ) { QVariantHash stateData; const QString state = updateProviderState( providerId, &stateData, providerData["type"].toString(), providerData.value("feedUrl").toString() ); providerData["error"] = false; providersSource->addProvider( providerId, ProvidersDataSource::ProviderData(providerData, state, stateData) ); return true; } else { // Invalid provider providerData["id"] = providerId; providerData["error"] = true; providerData["errorMessage"] = errorMessage; // Prepare state data with the error message and a boolean whether or not the provider // is installed or could not be found (only possible if the provider data source was // manually requested) QVariantHash stateData; stateData["statusMessage"] = errorMessage; stateData["isInstalled"] = ServiceProviderGlobal::isProviderInstalled( providerId ); providersSource->addProvider( providerId, ProvidersDataSource::ProviderData(providerData, "error", stateData) ); return false; } } bool PublicTransportEngine::updateServiceProviderSource() { const QString name = sourceTypeKeyword( ServiceProvidersSource ); ProvidersDataSource *providersSource = providersDataSource(); if ( providersSource->isDirty() ) { const QStringList providers = ServiceProviderGlobal::installedProviders(); if ( providers.isEmpty() ) { qWarning() << "Could not find any service provider plugins"; } else { QStringList loadedProviders; m_erroneousProviders.clear(); QSharedPointer cache = ServiceProviderGlobal::cache(); foreach( const QString &provider, providers ) { const QString providerId = ServiceProviderGlobal::idFromFileName( QUrl(provider).fileName() ); if ( updateProviderData(providerId, cache) ) { loadedProviders << providerId; } } // Print information about loaded/erroneous providers qDebug() << "Loaded" << loadedProviders.count() << "service providers"; if ( !m_erroneousProviders.isEmpty() ) { qWarning() << "Erroneous service provider plugins, that could not be loaded:" << m_erroneousProviders; } } // Mark and update all providers that are no longer installed QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); const QStringList uninstalledProviderIDs = providersSource->markUninstalledProviders(); foreach ( const QString &uninstalledProviderID, uninstalledProviderIDs ) { // Clear all values stored in the cache for the provider ServiceProviderGlobal::clearCache( uninstalledProviderID, cache ); // Delete the provider and update it's state and other data deleteProvider( uninstalledProviderID ); updateProviderData( uninstalledProviderID, cache ); } // Insert the data source m_dataSources.insert( name, providersSource ); } // Remove all old data, some service providers may have been updated and are now erroneous removeAllData( name ); publishData( providersSource ); return true; } QString PublicTransportEngine::updateProviderState( const QString &providerId, QVariantHash *stateData, const QString &providerType, const QString &feedUrl, bool readFromCache ) { Q_ASSERT( stateData ); QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); KConfigGroup group = cache->group( providerId ); const QString cachedState = readFromCache ? group.readEntry("state", QString()) : QString(); #ifdef BUILD_PROVIDER_TYPE_GTFS // Currently type is only used for GTFS const Enums::ServiceProviderType type = ServiceProviderGlobal::typeFromString( providerType ); #endif // BUILD_PROVIDER_TYPE_GTFS // Test if there is an error if ( m_erroneousProviders.contains(providerId) ) { stateData->insert( "statusMessage", m_erroneousProviders[providerId].toString() ); return "error"; } if ( !cachedState.isEmpty() ) { // State is stored in the cache, // also read state data from cache KConfigGroup stateGroup = group.group( "stateData" ); foreach ( const QString &key, stateGroup.keyList() ) { stateData->insert( key, stateGroup.readEntry(key) ); } #ifdef BUILD_PROVIDER_TYPE_GTFS if ( type == Enums::GtfsProvider ) { // Update state and add dynamic state data const QString state = ServiceProviderGtfs::updateGtfsDatabaseState( providerId, feedUrl, cache, stateData ); if ( state != QLatin1String("ready") ) { // The database is invalid/deleted, but the cache says the import was finished deleteProvider( providerId ); } return state; } else #endif // BUILD_PROVIDER_TYPE_GTFS { if ( cachedState != QLatin1String("ready") ) { // Provider not ready, cannot use it deleteProvider( providerId ); } // State of non-GTFS providers does not need more tests, // if there is an error only the fields "error" and "errorMessage" are available // in the data source, no fields "state" or "stateData" return cachedState; } } // !state.isEmpty() // State is not stored in the cache or is out of date QString state = "ready"; #ifdef BUILD_PROVIDER_TYPE_GTFS if ( type == Enums::GtfsProvider ) { state = ServiceProviderGtfs::updateGtfsDatabaseState( providerId, feedUrl, cache, stateData ); } #endif // BUILD_PROVIDER_TYPE_GTFS // Ensure a status message is given (or at least warn if not) if ( stateData->value("statusMessage").toString().isEmpty() ) { if ( state == QLatin1String("ready") ) { stateData->insert( "statusMessage", i18nc("@info/plain", "The provider is ready to use") ); } else { qWarning() << "Missing status message explaining why the provider" << providerId << "is not ready"; } } // Store the state in the cache group.writeEntry( "state", state ); // Write state data to the cache KConfigGroup stateGroup = group.group( "stateData" ); for ( QVariantHash::ConstIterator it = stateData->constBegin(); it != stateData->constEnd(); ++it ) { if ( isStateDataCached(it.key()) ) { stateGroup.writeEntry( it.key(), it.value() ); } } return state; } bool PublicTransportEngine::isStateDataCached( const QString &stateDataKey ) { return stateDataKey != QLatin1String("progress") && stateDataKey != QLatin1String("gtfsDatabasePath") && stateDataKey != QLatin1String("gtfsDatabaseSize") && stateDataKey != QLatin1String("gtfsDatabaseModifiedTime") && stateDataKey != QLatin1String("gtfsFeedImported") && stateDataKey != QLatin1String("gtfsFeedSize") && stateDataKey != QLatin1String("gtfsFeedModifiedTime"); } bool PublicTransportEngine::testServiceProvider( const QString &providerId, QVariantHash *providerData, QString *errorMessage, const QSharedPointer &_cache ) { const bool providerUsed = m_providers.contains( providerId ); const bool providerCached = m_cachedProviders.contains( providerId ); if ( providerUsed || providerCached ) { // The provider is cached in the engine, ie. it is valid, // use it's ServiceProviderData object const ProviderPointer provider = providerUsed ? m_providers[providerId] : m_cachedProviders[providerId]; *providerData = serviceProviderData( provider.data() ); errorMessage->clear(); return true; } // Read cached data for the provider QSharedPointer cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; ServiceProviderTestData testData = ServiceProviderTestData::read( providerId, cache ); // TODO Needs to be done for each provider sub class here foreach ( Enums::ServiceProviderType type, ServiceProviderGlobal::availableProviderTypes() ) { switch ( type ) { #ifdef BUILD_PROVIDER_TYPE_SCRIPT case Enums::ScriptedProvider: if ( !ServiceProviderScript::isTestResultUnchanged(providerId, cache) ) { qDebug() << "Script changed" << providerId; testData.setSubTypeTestStatus( ServiceProviderTestData::Pending ); testData.write( providerId, cache ); } break; #endif case Enums::GtfsProvider: break; case Enums::InvalidProvider: default: qWarning() << "Provider type unknown" << type; break; } } // Check the cache if the provider plugin .pts file can be read if ( testData.xmlStructureTestStatus() == ServiceProviderTestData::Failed ) { // The XML structure of the provider plugin .pts file is marked as failed in the cache // Cannot add provider data to data sources, the file needs to be fixed first qWarning() << "Provider plugin" << providerId << "is invalid."; qDebug() << "Fix the provider file at" << ServiceProviderGlobal::fileNameFromId(providerId); qDebug() << testData.errorMessage(); qDebug() << "************************************"; providerData->clear(); *errorMessage = testData.errorMessage(); m_erroneousProviders.insert( providerId, testData.errorMessage() ); updateErroneousServiceProviderSource(); return false; } // The sub type test may already be marked as failed in the cache, // but when provider data can be read it should be added to provider data source // also when the provider plugin is invalid // Read provider data from the XML file QString _errorMessage; const QScopedPointer data( ServiceProviderDataReader::read(providerId, &_errorMessage) ); if ( data.isNull() ) { // Could not read provider data if ( testData.isXmlStructureTestPending() ) { // Store error message in cache and do not reread unchanged XMLs everytime testData.setXmlStructureTestStatus( ServiceProviderTestData::Failed, _errorMessage ); testData.write( providerId, cache ); } providerData->clear(); *errorMessage = _errorMessage; m_erroneousProviders.insert( providerId, _errorMessage ); updateErroneousServiceProviderSource(); return false; } // Check if support for the used provider type has been build into the engine if ( !ServiceProviderGlobal::isProviderTypeAvailable(data->type()) ) { providerData->clear(); _errorMessage = i18nc("@info/plain", "Support for provider type %1 is not available", ServiceProviderGlobal::typeName(data->type(), ServiceProviderGlobal::ProviderTypeNameWithoutUnsupportedHint)); *errorMessage = _errorMessage; m_erroneousProviders.insert( providerId, _errorMessage ); updateErroneousServiceProviderSource(); return false; } // Mark the XML test as passed if not done already if ( testData.isXmlStructureTestPending() ) { testData.setXmlStructureTestStatus( ServiceProviderTestData::Passed ); testData.write( providerId, cache ); } // XML file structure test is passed, run provider type test if not done already switch ( testData.subTypeTestStatus() ) { case ServiceProviderTestData::Pending: { // Need to create the provider to run tests in derived classes (in the constructor) const QScopedPointer provider( createProviderForData(data.data(), this, cache) ); data->setParent( 0 ); // Prevent deletion of data when the provider gets deleted // Read test data again, because it may have been changed in the provider constructor testData = ServiceProviderTestData::read( providerId, cache ); // Run the sub type test if it is still pending testData = provider->runSubTypeTest( testData, cache ); // Read test data again, updated in the ServiceProvider constructor if ( testData.results().testFlag(ServiceProviderTestData::SubTypeTestFailed) ) { // Sub-type test failed qWarning() << "Test failed for" << providerId << testData.errorMessage(); providerData->clear(); *errorMessage = testData.errorMessage(); m_erroneousProviders.insert( providerId, testData.errorMessage() ); updateErroneousServiceProviderSource(); return false; } // The provider is already created, use it in serviceProviderData(), if needed *providerData = serviceProviderData( provider.data() ); break; } case ServiceProviderTestData::Failed: // Test is marked as failed in the cache *errorMessage = testData.errorMessage(); m_erroneousProviders.insert( providerId, testData.errorMessage() ); updateErroneousServiceProviderSource(); *providerData = serviceProviderData( *data ); return false; case ServiceProviderTestData::Passed: // Test is marked as passed in the cache *providerData = serviceProviderData( *data ); break; } m_erroneousProviders.remove( providerId ); const QLatin1String name = sourceTypeKeyword( ErroneousServiceProvidersSource ); removeData( name, providerId ); errorMessage->clear(); return true; } bool PublicTransportEngine::updateErroneousServiceProviderSource() { 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(); // Check if additional data is already included or was already requested const QString additionalDataState = item["additionalDataState"].toString(); if ( additionalDataState == QLatin1String("included") ) { emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Additional data is already included for item %1").arg(itemNumber) ); return false; } else if ( additionalDataState == QLatin1String("busy") ) { qDebug() << "Additional data for item" << itemNumber << "already requested, please wait"; emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Additional data was already requested for item %1, please wait") .arg(itemNumber) ); return false; } // Check if the timetable item is valid, // extract values needed for the additional data request job const QDateTime dateTime = item[ "DepartureDateTime" ].toDateTime(); const QString transportLine = item[ "TransportLine" ].toString(); const QString target = item[ "Target" ].toString(); const QString routeDataUrl = item[ "RouteDataUrl" ].toString(); if ( routeDataUrl.isEmpty() && (!dateTime.isValid() || transportLine.isEmpty() || target.isEmpty()) ) { emit additionalDataRequestFinished( sourceName, itemNumber, false, QString("Item to update is invalid: %1, %2, %3") .arg(dateTime.toString()).arg(transportLine, target) ); return false; } // Store state of additional data in the timetable item item["additionalDataState"] = "busy"; items[ itemNumber ] = item; dataSource->setTimetableItems( items ); // Found data of the timetable item to update const SourceRequestData sourceData( dataSource->name() ); const ProviderPointer provider = providerFromId( dataSource->providerId() ); Q_ASSERT( provider ); provider->requestAdditionalData( AdditionalDataRequest(dataSource->name(), itemNumber, sourceData.request->stop(), sourceData.request->stopId(), dateTime, transportLine, target, sourceData.request->city(), routeDataUrl) ); return true; } QString PublicTransportEngine::fixProviderId( const QString &providerId ) { if ( !providerId.isEmpty() ) { return providerId; } // No service provider ID given, use the default one for the users country const QString country = KGlobal::locale()->country(); // Try to find the XML filename of the default accessor for [country] const QString filePath = ServiceProviderGlobal::defaultProviderForLocation( country ); if ( filePath.isEmpty() ) { return 0; } // Extract service provider ID from filename const QString defaultProviderId = ServiceProviderGlobal::idFromFileName( filePath ); qDebug() << "No service provider ID given, using the default one for country" << country << "which is" << defaultProviderId; return defaultProviderId; } QString PublicTransportEngine::disambiguateSourceName( const QString &sourceName ) { // Remove count argument QString ret = sourceName; ret.remove( QRegExp("(count=[^\\|]+)") ); // Round time parameter values to 15 minutes precision QRegExp rx( "(time=[^\\|]+|datetime=[^\\|]+)" ); if ( rx.indexIn(ret) != -1 ) { // Get the time value const QString timeParameter = rx.cap( 1 ); QDateTime time; if ( timeParameter.startsWith(QLatin1String("time=")) ) { time = QDateTime( QDate::currentDate(), QTime::fromString(timeParameter.mid(5), "hh:mm") ); } else { // startsWith "datetime=" time = QDateTime::fromString( timeParameter.mid(9), Qt::ISODate ); if ( !time.isValid() ) { time = QDateTime::fromString( timeParameter.mid(9) ); } } // Round 15 minutes qint64 msecs = time.toMSecsSinceEpoch(); time = QDateTime::fromMSecsSinceEpoch( msecs - msecs % (1000 * 60 * 15) ); // Replace old time parameter with a new one ret.replace( rx.pos(), rx.matchedLength(), QLatin1String("datetime=") + time.toString(Qt::ISODate) ); } // Read parameters to reorder them afterwards const SourceType type = sourceTypeFromName( ret ); const QString typeKeyword = sourceTypeKeyword( type ); const QStringList parameterPairs = ret.mid( typeKeyword.length() ) .trimmed().split( '|', QString::SkipEmptyParts ); QString defaultParameter; QHash< QString, QString > parameters; for ( int i = 0; i < parameterPairs.length(); ++i ) { const QString parameter = parameterPairs.at( i ).trimmed(); const int pos = parameter.indexOf( '=' ); if ( pos == -1 ) { // No parameter name given, this is the default parameter, eg. the provider ID defaultParameter = fixProviderId( parameter ); } else { // Only add parameters with non-empty parameter name and value, // make parameter value lower case (eg. stop names) const QString parameterName = parameter.left( pos ); const QString parameterValue = parameter.mid( pos + 1 ).trimmed(); if ( !parameterName.isEmpty() && !parameterValue.isEmpty() ) { parameters[ parameterName ] = parameterValue; } } } // Build non-ambiguous source name with standardized parameter order ret = typeKeyword + ' ' + defaultParameter; if ( parameters.contains(QLatin1String("city")) ) { ret += "|city=" + parameters["city"].toLower(); } if ( parameters.contains(QLatin1String("stop")) ) { ret += "|stop=" + parameters["stop"].toLower(); } if ( parameters.contains(QLatin1String("stopid")) ) { ret += "|stopid=" + parameters["stopid"]; } if ( parameters.contains(QLatin1String("originstop")) ) { ret += "|originstop=" + parameters["originstop"].toLower(); } if ( parameters.contains(QLatin1String("originstopid")) ) { ret += "|originstopid=" + parameters["originstopid"]; } if ( parameters.contains(QLatin1String("targetstop")) ) { ret += "|targetstop=" + parameters["targetstop"].toLower(); } if ( parameters.contains(QLatin1String("targetstopid")) ) { ret += "|targetstopid=" + parameters["targetstopid"]; } if ( parameters.contains(QLatin1String("timeoffset")) ) { ret += "|timeoffset=" + parameters["timeoffset"]; } if ( parameters.contains(QLatin1String("time")) ) { ret += "|time=" + parameters["time"]; } if ( parameters.contains(QLatin1String("datetime")) ) { ret += "|datetime=" + parameters["datetime"]; } if ( parameters.contains(QLatin1String("count")) ) { ret += "|count=" + parameters["count"]; } if ( parameters.contains(QLatin1String("longitude")) ) { ret += "|longitude=" + parameters["longitude"]; } if ( parameters.contains(QLatin1String("latitude")) ) { ret += "|latitude=" + parameters["latitude"]; } return ret; } void PublicTransportEngine::serviceProviderDirChanged( const QString &path ) { Q_UNUSED( path ) // Use a timer to prevent loading all service providers again and again, for every changed file // in a possibly big list of files. It reloads the providers maximally every 250ms. // Otherwise it can freeze plasma for a while if eg. all provider files are changed at once. if ( !m_providerUpdateDelayTimer ) { m_providerUpdateDelayTimer = new QTimer( this ); connect( m_providerUpdateDelayTimer, SIGNAL(timeout()), this, SLOT(reloadChangedProviders()) ); } m_providerUpdateDelayTimer->start( 250 ); } void PublicTransportEngine::deleteProvider( const QString &providerId, bool keepProviderDataSources ) { // Clear all cached data const QStringList cachedSources = m_dataSources.keys(); foreach( const QString &cachedSource, cachedSources ) { const QString currentProviderId = providerIdFromSourceName( cachedSource ); if ( currentProviderId == providerId ) { // Disconnect provider and abort all running requests, // take provider from the provider list without caching it ProviderPointer provider = m_providers.take( providerId ); if ( provider ) { disconnect( provider.data(), 0, this, 0 ); provider->abortAllRequests(); } m_runningSources.removeOne( cachedSource ); if ( keepProviderDataSources ) { // Update data source for the provider TimetableDataSource *timetableSource = dynamic_cast< TimetableDataSource* >( m_dataSources[cachedSource] ); if ( timetableSource ) { // Stop automatic updates timetableSource->stopUpdateTimer(); // Update manually foreach ( const QString &sourceName, timetableSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } } } else { // Delete data source for the provider delete m_dataSources.take( cachedSource ); } } } } void PublicTransportEngine::reloadChangedProviders() { qDebug() << "Reload service providers (the service provider dir changed)"; delete m_providerUpdateDelayTimer; m_providerUpdateDelayTimer = 0; // Notify the providers data source about the changed provider directory. // Do not remove it here, so that it can track which providers have changed ProvidersDataSource *providersSource = providersDataSource(); if ( providersSource ) { providersSource->providersHaveChanged(); } // Remove cached locations source to have it updated delete m_dataSources.take( sourceTypeKeyword(LocationsSource) ); // Clear all cached data (use the new provider to parse the data again) const QStringList cachedSources = m_dataSources.keys(); const QSharedPointer< KConfig > cache = ServiceProviderGlobal::cache(); foreach( const QString &cachedSource, cachedSources ) { const QString providerId = providerIdFromSourceName( cachedSource ); if ( !providerId.isEmpty() && (ServiceProviderGlobal::isSourceFileModified(providerId, cache) #ifdef BUILD_PROVIDER_TYPE_SCRIPT || !ServiceProviderScript::isTestResultUnchanged(providerId, cache) #endif ) ) { m_providers.remove( providerId ); m_cachedProviders.remove( providerId ); m_erroneousProviders.remove( providerId ); updateProviderData( providerId, cache ); TimetableDataSource *timetableSource = dynamic_cast< TimetableDataSource* >( m_dataSources[cachedSource] ); if ( timetableSource ) { // Stop automatic updates timetableSource->stopUpdateTimer(); // Update manually foreach ( const QString &sourceName, timetableSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } } } } updateLocationSource(); updateServiceProviderSource(); updateErroneousServiceProviderSource(); } const QLatin1String PublicTransportEngine::sourceTypeKeyword( SourceType sourceType ) { switch ( sourceType ) { case ServiceProviderSource: return QLatin1String("ServiceProvider"); case ServiceProvidersSource: return QLatin1String("ServiceProviders"); case ErroneousServiceProvidersSource: return QLatin1String("ErroneousServiceProviders"); case LocationsSource: return QLatin1String("Locations"); case VehicleTypesSource: return QLatin1String("VehicleTypes"); case DeparturesSource: return QLatin1String("Departures"); case ArrivalsSource: return QLatin1String("Arrivals"); case StopsSource: return QLatin1String("Stops"); case JourneysSource: return QLatin1String("Journeys"); case JourneysDepSource: return QLatin1String("JourneysDep"); case JourneysArrSource: return QLatin1String("JourneysArr"); default: return QLatin1String(""); } } PublicTransportEngine::SourceType PublicTransportEngine::sourceTypeFromName( const QString &sourceName ) { // Get type of the source, do not match case insensitive, otherwise there can be multiple // sources with the same data but only different case if ( sourceName.startsWith(sourceTypeKeyword(ServiceProviderSource) + ' ') ) { return ServiceProviderSource; } else if ( sourceName.compare(sourceTypeKeyword(ServiceProvidersSource)) == 0 ) { return ServiceProvidersSource; } else if ( sourceName.compare(sourceTypeKeyword(ErroneousServiceProvidersSource)) == 0 ) { return ErroneousServiceProvidersSource; } else if ( sourceName.compare(sourceTypeKeyword(LocationsSource)) == 0 ) { return LocationsSource; } else if ( sourceName.compare(sourceTypeKeyword(VehicleTypesSource)) == 0 ) { return VehicleTypesSource; } else if ( sourceName.startsWith(sourceTypeKeyword(DeparturesSource)) ) { return DeparturesSource; } else if ( sourceName.startsWith(sourceTypeKeyword(ArrivalsSource)) ) { return ArrivalsSource; } else if ( sourceName.startsWith(sourceTypeKeyword(StopsSource)) ) { return StopsSource; } else if ( sourceName.startsWith(sourceTypeKeyword(JourneysDepSource)) ) { return JourneysDepSource; } else if ( sourceName.startsWith(sourceTypeKeyword(JourneysArrSource)) ) { return JourneysArrSource; } else if ( sourceName.startsWith(sourceTypeKeyword(JourneysSource)) ) { return JourneysSource; } else { return InvalidSourceName; } } PublicTransportEngine::SourceRequestData::SourceRequestData( const QString &name ) : name(name), type(sourceTypeFromName(name)), parseMode(parseModeFromSourceType(type)), request(0) { if ( isDataRequestingSourceType(type) ) { // Extract parameters, which follow after the source type keyword in name // and are delimited with '|' QStringList parameters = name.mid( QString(sourceTypeKeyword(type)).length() ) .trimmed().split( '|', QString::SkipEmptyParts ); switch ( parseMode ) { case ParseForDepartures: request = new DepartureRequest( name, parseMode ); break; case ParseForArrivals: request = new ArrivalRequest( name, parseMode ); break; case ParseForStopSuggestions: { bool hasLongitude = false, hasLatitude = false; for ( int i = 0; i < parameters.length(); ++i ) { const QString parameter = parameters.at( i ).trimmed(); const QString parameterName = parameter.left( parameter.indexOf('=') ); if ( parameterName == QLatin1String("longitude") ) { hasLongitude = true; } else if ( parameterName == QLatin1String("latitude") ) { hasLatitude = true; } } if ( hasLongitude && hasLatitude ) { request = new StopsByGeoPositionRequest( name, parseMode ); } else { request = new StopSuggestionRequest( name, parseMode ); } break; } case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: request = new JourneyRequest( name, parseMode ); break; default: qWarning() << "Cannot create a request for parse mode" << parseMode; return; } // Read parameters for ( int i = 0; i < parameters.length(); ++i ) { const QString parameter = parameters.at( i ).trimmed(); const int pos = parameter.indexOf( '=' ); if ( pos == -1 ) { if ( !defaultParameter.isEmpty() ) { qWarning() << "More than one parameters without name given:" << defaultParameter << parameter; } // No parameter name given, assume the service provider ID defaultParameter = parameter; } else { const QString parameterName = parameter.left( pos ); const QString parameterValue = parameter.mid( pos + 1 ).trimmed(); if ( parameterValue.isEmpty() ) { qWarning() << "Empty parameter value for parameter" << parameterName; } else if ( parameterName == QLatin1String("city") ) { request->setCity( parameterValue ); } else if ( parameterName == QLatin1String("stop") ) { request->setStop( parameterValue ); } else if ( parameterName == QLatin1String("stopid") ) { request->setStopId( parameterValue ); } else if ( parameterName == QLatin1String("targetstop") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'targetstop' parameter is only used for journey requests"; } else { journeyRequest->setTargetStop( parameterValue ); } } else if ( parameterName == QLatin1String("targetstopid") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'targetstopId' parameter is only used for journey requests"; } else { journeyRequest->setTargetStopId( parameterValue ); } } else if ( parameterName == QLatin1String("originstop") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'originstop' parameter is only used for journey requests"; } else { journeyRequest->setStop( parameterValue ); } } else if ( parameterName == QLatin1String("originstopid") ) { JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "The 'originstopId' parameter is only used for journey requests"; } else { journeyRequest->setStopId( parameterValue ); } } else if ( parameterName == QLatin1String("timeoffset") ) { request->setDateTime( QDateTime::currentDateTime().addSecs(parameterValue.toInt() * 60) ); } else if ( parameterName == QLatin1String("time") ) { request->setDateTime( QDateTime(QDate::currentDate(), QTime::fromString(parameterValue, "hh:mm")) ); } else if ( parameterName == QLatin1String("datetime") ) { request->setDateTime( QDateTime::fromString(parameterValue, Qt::ISODate) ); if ( !request->dateTime().isValid() ) { request->setDateTime( QDateTime::fromString(parameterValue) ); } } else if ( parameterName == QLatin1String("count") ) { bool ok; request->setCount( parameterValue.toInt(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'count' in source name:" << parameterValue; request->setCount( 20 ); } } else if ( dynamic_cast(request) ) { StopsByGeoPositionRequest *stopRequest = dynamic_cast< StopsByGeoPositionRequest* >( request ); bool ok; if ( parameterName == QLatin1String("longitude") ) { stopRequest->setLongitude( parameterValue.toFloat(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'longitude' in source name:" << parameterValue; } } else if ( parameterName == QLatin1String("latitude") ) { stopRequest->setLatitude( parameterValue.toFloat(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'latitude' in source name:" << parameterValue; } } else if ( parameterName == QLatin1String("distance") ) { stopRequest->setDistance( parameterValue.toInt(&ok) ); if ( !ok ) { qWarning() << "Bad value for 'distance' in source name:" << parameterValue; } } else { qWarning() << "Unknown argument" << parameterName; } } else { qWarning() << "Unknown argument" << parameterName; } } } if ( !request->dateTime().isValid() ) { // No date/time value given, use default offset from now request->setDateTime( QDateTime::currentDateTime().addSecs(DEFAULT_TIME_OFFSET * 60) ); } // The default parameter is the provider ID for data requesting sources, // use the default provider for the users country if no ID is given defaultParameter = PublicTransportEngine::fixProviderId( defaultParameter ); } else { // Extract provider ID or country code, which follow after the source type keyword in name defaultParameter = name.mid( QString(sourceTypeKeyword(type)).length() ).trimmed(); } } PublicTransportEngine::SourceRequestData::~SourceRequestData() { delete request; } bool PublicTransportEngine::SourceRequestData::isValid() const { if ( type == InvalidSourceName ) { qWarning() << "Invalid source name" << name; return false; } if ( isDataRequestingSourceType(type) ) { if ( parseMode == ParseForDepartures || parseMode == ParseForArrivals ) { // Check if the stop name/ID is missing if ( !request || (request->stop().isEmpty() && request->stopId().isEmpty()) ) { qWarning() << "No stop ID or name in data source name" << name; return false; } } else if ( parseMode == ParseForStopSuggestions ) { // Check if the stop name or geo coordinates are missing if ( !request || (!dynamic_cast(request) && request->stop().isEmpty()) ) { qWarning() << "Stop name (part) is missing in data source name" << name; return false; } } else if ( parseMode == ParseForJourneysByArrivalTime || parseMode == ParseForJourneysByDepartureTime ) { // Make sure non empty originstop(id) and targetstop(id) parameters are available JourneyRequest *journeyRequest = dynamic_cast< JourneyRequest* >( request ); if ( !journeyRequest ) { qWarning() << "Internal error: Not a JourneyRequest object but parseMode is" << parseMode; return false; } if ( journeyRequest->stop().isEmpty() && journeyRequest->stopId().isEmpty() ) { qWarning() << "No stop ID or name for the origin stop in data source name" << name; return false; } if ( journeyRequest->targetStop().isEmpty() && journeyRequest->targetStopId().isEmpty() ) { qWarning() << "No stop ID or name for the target stop in data source name" << name; return false; } } } return true; } bool PublicTransportEngine::sourceRequestEvent( const QString &name ) { // If name is associated with a data source that runs asynchronously, // create the source first with empty data, gets updated once the request has finished SourceRequestData data( name ); if ( data.isValid() && isDataRequestingSourceType(data.type) ) { setData( name, DataEngine::Data() ); } return requestOrUpdateSourceEvent( data ); } bool PublicTransportEngine::updateSourceEvent( const QString &name ) { return requestOrUpdateSourceEvent( SourceRequestData(name), true ); } bool PublicTransportEngine::requestOrUpdateSourceEvent( const SourceRequestData &sourceData, bool update ) { if ( !sourceData.isValid() ) { return false; } switch ( sourceData.type ) { case ServiceProviderSource: return updateServiceProviderForCountrySource( sourceData ); case ServiceProvidersSource: return updateServiceProviderSource(); case ErroneousServiceProvidersSource: return updateErroneousServiceProviderSource(); case LocationsSource: return updateLocationSource(); case DeparturesSource: case ArrivalsSource: case StopsSource: case JourneysSource: case JourneysArrSource: case JourneysDepSource: { return updateTimetableDataSource( sourceData ); } // This data source never changes, ie. needs no updates case VehicleTypesSource: if ( !update ) { initVehicleTypesSource(); } return true; case InvalidSourceName: default: qDebug() << "Source name incorrect" << sourceData.name; return false; } } void PublicTransportEngine::initVehicleTypesSource() { QVariantHash vehicleTypes; const int index = Enums::staticMetaObject.indexOfEnumerator("VehicleType"); const QMetaEnum enumerator = Enums::staticMetaObject.enumerator( index ); // Start at i = 1 to skip Enums::InvalidVehicleType for ( int i = 1; i < enumerator.keyCount(); ++i ) { Enums::VehicleType vehicleType = static_cast< Enums::VehicleType >( enumerator.value(i) ); QVariantHash vehicleTypeData; vehicleTypeData.insert( "id", enumerator.key(i) ); vehicleTypeData.insert( "name", Global::vehicleTypeToString(vehicleType) ); vehicleTypeData.insert( "namePlural", Global::vehicleTypeToString(vehicleType, true) ); vehicleTypeData.insert( "iconName", Global::vehicleTypeToIcon(vehicleType) ); vehicleTypes.insert( QString::number(enumerator.value(i)), vehicleTypeData ); } setData( sourceTypeKeyword(VehicleTypesSource), vehicleTypes ); } void PublicTransportEngine::timetableDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &items, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool deleteDepartureInfos, bool isDepartureData ) { Q_UNUSED( requestUrl ); Q_UNUSED( provider ); Q_UNUSED( deleteDepartureInfos ); const QString sourceName = request.sourceName(); DEBUG_ENGINE_JOBS( items.count() << (isDepartureData ? "departures" : "arrivals") << "received" << sourceName ); const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Data source already removed"; return; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); Q_ASSERT( dataSource ); m_runningSources.removeOne( nonAmbiguousName ); QVariantList departuresData; const QString itemKey = isDepartureData ? "departures" : "arrivals"; foreach( const DepartureInfoPtr &departureInfo, items ) { QVariantHash departureData; TimetableData departure = departureInfo->data(); for ( TimetableData::ConstIterator it = departure.constBegin(); it != departure.constEnd(); ++it ) { if ( it.value().isValid() ) { departureData.insert( Global::timetableInformationToString(it.key()), it.value() ); } } departureData.insert( "Nightline", departureInfo->isNightLine() ); departureData.insert( "Expressline", departureInfo->isExpressLine() ); // Add existing additional data const uint hash = TimetableDataSource::hashForDeparture( departure, isDepartureData ); if ( dataSource->additionalData().contains(hash) ) { // Found already downloaded additional data, add it to the updated departure data const TimetableData additionalData = dataSource->additionalData( hash ); if ( !additionalData.isEmpty() ) { bool hasAdditionalData = false; for ( TimetableData::ConstIterator it = additionalData.constBegin(); it != additionalData.constEnd(); ++it ) { if ( it.key() != Enums::RouteDataUrl ) { hasAdditionalData = true; } departureData.insert( Global::timetableInformationToString(it.key()), it.value() ); } departureData[ "additionalDataState" ] = hasAdditionalData ? "included" : "error"; } } if ( !departureData.contains("additionalDataState") ) { departureData[ "additionalDataState" ] = provider->features().contains(Enums::ProvidesAdditionalData) ? "notrequested" : "notsupported"; } departuresData << departureData; } // Cleanup the data source later startDataSourceCleanupLater( dataSource ); dataSource->setValue( itemKey, departuresData ); // if ( deleteDepartureInfos ) { // qDebug() << "Delete" << items.count() << "departures/arrivals"; // qDeleteAll( departures ); // } // Fill the data source with general information const QDateTime dateTime = QDateTime::currentDateTime(); dataSource->setValue( "serviceProvider", provider->id() ); dataSource->setValue( "delayInfoAvailable", globalInfo.delayInfoAvailable ); dataSource->setValue( "requestUrl", requestUrl ); dataSource->setValue( "parseMode", request.parseModeName() ); dataSource->setValue( "error", false ); dataSource->setValue( "updated", dateTime ); // Store a proposal for the next download time QDateTime last = items.isEmpty() ? dateTime : items.last()->value(Enums::DepartureDateTime).toDateTime(); dataSource->setNextDownloadTimeProposal( dateTime.addSecs(dateTime.secsTo(last) / 3) ); const QDateTime nextUpdateTime = provider->nextUpdateTime( dataSource->updateFlags(), dateTime, dataSource->nextDownloadTimeProposal(), dataSource->data() ); const QDateTime minManualUpdateTime = provider->nextUpdateTime( dataSource->updateFlags() | UpdateWasRequestedManually, dateTime, dataSource->nextDownloadTimeProposal(), dataSource->data() ); // Store update times in the data source dataSource->setValue( "nextAutomaticUpdate", nextUpdateTime ); dataSource->setValue( "minManualUpdateTime", minManualUpdateTime ); // Publish the data source and cache it publishData( dataSource ); m_dataSources[ nonAmbiguousName ] = dataSource; int msecsUntilUpdate = dateTime.msecsTo( nextUpdateTime ); Q_ASSERT( msecsUntilUpdate >= 10000 ); // Make sure to not produce too many updates by mistake DEBUG_ENGINE_JOBS( "Update data source in" << KGlobal::locale()->prettyFormatDuration(msecsUntilUpdate) ); if ( !dataSource->updateTimer() ) { QTimer *updateTimer = new QTimer( this ); connect( updateTimer, SIGNAL(timeout()), this, SLOT(updateTimeout()) ); dataSource->setUpdateTimer( updateTimer ); } dataSource->updateTimer()->start( msecsUntilUpdate ); } void PublicTransportEngine::startDataSourceCleanupLater( TimetableDataSource *dataSource ) { if ( !dataSource->cleanupTimer() ) { QTimer *cleanupTimer = new QTimer( this ); cleanupTimer->setInterval( 2500 ); connect( cleanupTimer, SIGNAL(timeout()), this, SLOT(cleanupTimeout()) ); dataSource->setCleanupTimer( cleanupTimer ); } dataSource->cleanupTimer()->start(); } void PublicTransportEngine::updateTimeout() { // Find the timetable data source to which the timer belongs which timeout() signal was emitted QTimer *timer = qobject_cast< QTimer* >( sender() ); TimetableDataSource *dataSource = dataSourceFromTimer( timer ); if ( !dataSource ) { // No data source found that started the timer, // should not happen the data source should have deleted the timer on destruction qWarning() << "Timeout received from an unknown update timer"; return; } // Found the timetable data source of the timer, // requests updates for all connected sources (possibly multiple combined stops) foreach ( const QString &sourceName, dataSource->usingDataSources() ) { updateTimetableDataSource( SourceRequestData(sourceName) ); } // TODO FIXME Do not update while running additional data requests? } void PublicTransportEngine::cleanupTimeout() { // Find the timetable data source to which the timer belongs which timeout() signal was emitted QTimer *timer = qobject_cast< QTimer* >( sender() ); TimetableDataSource *dataSource = dataSourceFromTimer( timer ); if ( !dataSource ) { // No data source found that started the timer, // should not happen the data source should have deleted the timer on destruction qWarning() << "Timeout received from an unknown cleanup timer"; return; } // Found the timetable data source of the timer dataSource->cleanup(); dataSource->setCleanupTimer( 0 ); // Deletes the timer } void PublicTransportEngine::additionalDataReceived( ServiceProvider *provider, const QUrl &requestUrl, const TimetableData &data, const AdditionalDataRequest &request ) { Q_UNUSED( provider ); Q_UNUSED( requestUrl ); // Check if the destination data source exists const QString nonAmbiguousName = disambiguateSourceName( request.sourceName() ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Additional data received for a source that was already removed:" << nonAmbiguousName; emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), false, "Data source to update was already removed" ); return; } // Get the list of timetable items from the existing data source TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); Q_ASSERT( dataSource ); QVariantList items = dataSource->timetableItems(); if ( request.itemNumber() >= items.count() ) { emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), false, QString("Item %1 not found in the data source (with %2 items)") .arg(request.itemNumber()).arg(items.count()) ); return; } // Get the timetable item for which additional data was received // and insert the new data into it bool newDataInserted = false; TimetableData _data = data; QVariantHash item = items[ request.itemNumber() ].toHash(); for ( TimetableData::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { // Check if there already is data in the current additional data field const QString key = Global::timetableInformationToString( it.key() ); const bool isRouteDataUrl = key == Enums::toString(Enums::RouteDataUrl); if ( item.contains(key) && item[key].isValid() && !isRouteDataUrl ) { // The timetable item already contains data for the current field, // do not allow updates here, only additional data qWarning() << "Cannot update timetable data in additional data requests"; qWarning() << "Tried to update field" << key << "to value" << it.value() << "from value" << item[key] << "in data source" << request.sourceName(); } else { // Allow to add the additional data field item.insert( key, it.value() ); if ( !isRouteDataUrl ) { newDataInserted = true; if ( key == Enums::toString(Enums::RouteStops) ) { // Added a RouteStops field, automatically generate RouteStopsShortened const QString routeStopsShortenedKey = Global::timetableInformationToString( Enums::RouteStopsShortened ); if ( !item.contains(routeStopsShortenedKey) ) { const QStringList routeStopsShortened = removeCityNameFromStops( it.value().toStringList() ); item.insert( routeStopsShortenedKey, routeStopsShortened ); _data.insert( Enums::RouteStopsShortened, routeStopsShortened ); } } } } } if ( !newDataInserted ) { const QString errorMessage = i18nc("@info/plain", "No additional data found"); emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), false, errorMessage ); item[ "additionalDataState" ] = "error"; item[ "additionalDataError" ] = errorMessage; items[ request.itemNumber() ] = item; dataSource->setTimetableItems( items ); return; } item[ "additionalDataState" ] = "included"; // Store the changed item back into the item list of the data source items[ request.itemNumber() ] = item; dataSource->setTimetableItems( items ); // Also store received additional data separately // to not loose additional data after updating the data source const uint hash = TimetableDataSource::hashForDeparture( item, dataSource->timetableItemKey() != QLatin1String("arrivals") ); dataSource->setAdditionalData( hash, _data ); startDataSourceCleanupLater( dataSource ); QTimer *updateDelayTimer; if ( dataSource->updateAdditionalDataDelayTimer() ) { // Use existing timer, restart it with a longer interval and // publish the new data of the data source after the timeout updateDelayTimer = dataSource->updateAdditionalDataDelayTimer(); updateDelayTimer->setInterval( 250 ); } else { // Create timer with a shorter interval, but directly publish the new data of the // data source. The timer is used here to delay further publishing of new data, // ie. combine multiple updates and publish them at once. publishData( dataSource ); updateDelayTimer = new QTimer( this ); updateDelayTimer->setInterval( 150 ); connect( updateDelayTimer, SIGNAL(timeout()), this, SLOT(updateDataSourcesWithNewAdditionData()) ); dataSource->setUpdateAdditionalDataDelayTimer( updateDelayTimer ); } // (Re-)start the additional data update timer updateDelayTimer->start(); // Emit result emit additionalDataRequestFinished( request.sourceName(), request.itemNumber(), true ); } TimetableDataSource *PublicTransportEngine::dataSourceFromTimer( QTimer *timer ) const { for ( QHash< QString, DataSource* >::ConstIterator it = m_dataSources.constBegin(); it != m_dataSources.constEnd(); ++it ) { TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( *it ); if ( dataSource && (dataSource->updateAdditionalDataDelayTimer() == timer || dataSource->updateTimer() == timer || dataSource->cleanupTimer() == timer) ) { return dataSource; } } // The timer is not used any longer return 0; } void PublicTransportEngine::updateDataSourcesWithNewAdditionData() { QTimer *updateDelayTimer = qobject_cast< QTimer* >( sender() ); Q_ASSERT( updateDelayTimer ); TimetableDataSource *dataSource = dataSourceFromTimer( updateDelayTimer ); if ( !dataSource ) { qWarning() << "Data source was already removed"; return; } const int interval = updateDelayTimer->interval(); dataSource->setUpdateAdditionalDataDelayTimer( 0 ); // Deletes the timer // Do the upate, but only after long delays, // because the update is already done before short delays if ( interval > 150 ) { // Was delayed for a longer time publishData( dataSource ); } } void PublicTransportEngine::departuresReceived( ServiceProvider *provider, const QUrl &requestUrl, const DepartureInfoList &departures, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool deleteDepartureInfos ) { timetableDataReceived( provider, requestUrl, departures, globalInfo, request, deleteDepartureInfos, true ); } void PublicTransportEngine::arrivalsReceived( ServiceProvider *provider, const QUrl &requestUrl, const ArrivalInfoList &arrivals, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool deleteDepartureInfos ) { timetableDataReceived( provider, requestUrl, arrivals, globalInfo, request, deleteDepartureInfos, false ); } void PublicTransportEngine::journeysReceived( ServiceProvider* provider, const QUrl &requestUrl, const JourneyInfoList &journeys, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool deleteJourneyInfos ) { Q_UNUSED( provider ); Q_UNUSED( deleteJourneyInfos ); const QString sourceName = request.sourceName(); DEBUG_ENGINE_JOBS( journeys.count() << "journeys received" << sourceName ); const QString nonAmbiguousName = disambiguateSourceName( sourceName ); m_runningSources.removeOne( nonAmbiguousName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Data source already removed" << nonAmbiguousName; return; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Data source already deleted" << nonAmbiguousName; return; } dataSource->clear(); QVariantList journeysData; foreach( const JourneyInfoPtr &journeyInfo, journeys ) { if ( !journeyInfo->isValid() ) { continue; } QVariantHash journeyData; TimetableData journey = journeyInfo->data(); for ( TimetableData::ConstIterator it = journey.constBegin(); it != journey.constEnd(); ++it ) { if ( it.value().isValid() ) { journeyData.insert( Global::timetableInformationToString(it.key()), it.value() ); } } journeysData << journeyData; } dataSource->setValue( "journeys", journeysData ); int journeyCount = journeys.count(); QDateTime first, last; if ( journeyCount > 0 ) { first = journeys.first()->value(Enums::DepartureDateTime).toDateTime(); last = journeys.last()->value(Enums::DepartureDateTime).toDateTime(); } else { first = last = QDateTime::currentDateTime(); } // if ( deleteJourneyInfos ) { // qDeleteAll( journeys ); TODO // } // Store a proposal for the next download time int secs = ( journeyCount / 3 ) * first.secsTo( last ); QDateTime downloadTime = QDateTime::currentDateTime().addSecs( secs ); dataSource->setNextDownloadTimeProposal( downloadTime ); // Store received data in the data source map dataSource->setValue( "serviceProvider", provider->id() ); dataSource->setValue( "delayInfoAvailable", globalInfo.delayInfoAvailable ); dataSource->setValue( "requestUrl", requestUrl ); dataSource->setValue( "parseMode", request.parseModeName() ); dataSource->setValue( "error", false ); dataSource->setValue( "updated", QDateTime::currentDateTime() ); dataSource->setValue( "nextAutomaticUpdate", downloadTime ); dataSource->setValue( "minManualUpdateTime", downloadTime ); // TODO setData( sourceName, dataSource->data() ); m_dataSources[ nonAmbiguousName ] = dataSource; } void PublicTransportEngine::stopsReceived( ServiceProvider *provider, const QUrl &requestUrl, const StopInfoList &stops, const StopSuggestionRequest &request, bool deleteStopInfos ) { Q_UNUSED( provider ); Q_UNUSED( deleteStopInfos ); const QString sourceName = request.sourceName(); m_runningSources.removeOne( disambiguateSourceName(sourceName) ); DEBUG_ENGINE_JOBS( stops.count() << "stop suggestions received" << sourceName ); QVariantList stopsData; foreach( const StopInfoPtr &stopInfo, stops ) { QVariantHash stopData; TimetableData stop = stopInfo->data(); for ( TimetableData::ConstIterator it = stop.constBegin(); it != stop.constEnd(); ++it ) { if ( it.value().isValid() ) { stopData.insert( Global::timetableInformationToString(it.key()), it.value() ); } } stopsData << stopData; } setData( sourceName, "stops", stopsData ); setData( sourceName, "serviceProvider", provider->id() ); setData( sourceName, "requestUrl", requestUrl ); setData( sourceName, "parseMode", request.parseModeName() ); setData( sourceName, "error", false ); setData( sourceName, "updated", QDateTime::currentDateTime() ); // if ( deleteStopInfos ) { // qDeleteAll( stops ); TODO // } } void PublicTransportEngine::requestFailed( ServiceProvider *provider, ErrorCode errorCode, const QString &errorMessage, const QUrl &requestUrl, const AbstractRequest *request ) { Q_UNUSED( provider ); qDebug() << errorCode << errorMessage << "- Requested URL:" << requestUrl; const AdditionalDataRequest *additionalDataRequest = dynamic_cast< const AdditionalDataRequest* >( request ); if ( additionalDataRequest ) { // A request for additional data failed emit additionalDataRequestFinished( request->sourceName(), additionalDataRequest->itemNumber(), false, errorMessage ); // Check if the destination data source exists const QString nonAmbiguousName = disambiguateSourceName( request->sourceName() ); if ( m_dataSources.contains(nonAmbiguousName) ) { // Get the list of timetable items from the existing data source TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); Q_ASSERT( dataSource ); QVariantList items = dataSource->timetableItems(); if ( additionalDataRequest->itemNumber() < items.count() ) { // Found the item for which the request failed, // reset additional data included/waiting fields QVariantHash item = items[ additionalDataRequest->itemNumber() ].toHash(); item[ "additionalDataState" ] = "error"; item[ "additionalDataError" ] = errorMessage; items[ additionalDataRequest->itemNumber() ] = item; dataSource->setTimetableItems( items ); // Publish updated fields publishData( dataSource ); } else { qWarning() << "Timetable item" << additionalDataRequest->itemNumber() << "not found in data source" << request->sourceName() << "additional data error discarded"; } } else { qWarning() << "Data source" << request->sourceName() << "not found, additional data error discarded"; } // Do not mark the whole timetable data source as erroneous // when a request for additional data has failed return; } // Remove erroneous source from running sources list m_runningSources.removeOne( disambiguateSourceName(request->sourceName()) ); const QString sourceName = request->sourceName(); setData( sourceName, "serviceProvider", provider->id() ); setData( sourceName, "requestUrl", requestUrl ); setData( sourceName, "parseMode", request->parseModeName() ); setData( sourceName, "receivedData", "nothing" ); setData( sourceName, "error", true ); setData( sourceName, "errorCode", errorCode ); setData( sourceName, "errorMessage", errorMessage ); setData( sourceName, "updated", QDateTime::currentDateTime() ); } bool PublicTransportEngine::requestUpdate( const QString &sourceName ) { // Find the TimetableDataSource object for sourceName const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Not an existing timetable data source:" << sourceName; emit updateRequestFinished( sourceName, false, i18nc("@info", "Data source is not an existing timetable data source: %1", sourceName) ); return false; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Internal error: Invalid pointer to the data source stored"; emit updateRequestFinished( sourceName, false, "Internal error: Invalid pointer to the data source stored" ); return false; } // Check if updates are blocked to prevent to many useless updates const QDateTime nextUpdateTime = sourceUpdateTime( dataSource, UpdateWasRequestedManually ); if ( QDateTime::currentDateTime() < nextUpdateTime ) { qDebug() << "Too early to update again, update request rejected" << nextUpdateTime; emit updateRequestFinished( sourceName, false, i18nc("@info", "Update request rejected, earliest update time: %1", nextUpdateTime.toString()) ); return false; } // Stop automatic updates dataSource->stopUpdateTimer(); // Start the request const bool result = request( sourceName ); if ( result ) { emit updateRequestFinished( sourceName ); } else { emit updateRequestFinished( sourceName, false, i18nc("@info", "Request failed") ); } return result; } bool PublicTransportEngine::requestMoreItems( const QString &sourceName, Enums::MoreItemsDirection direction ) { // Find the TimetableDataSource object for sourceName const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { qWarning() << "Not an existing timetable data source:" << sourceName; emit moreItemsRequestFinished( sourceName, direction, false, i18nc("@info", "Data source is not an existing timetable data source: %1", sourceName) ); return false; } else if ( m_runningSources.contains(nonAmbiguousName) ) { // The data source currently gets processed or updated, including requests for more items qDebug() << "Source currently gets processed, please wait" << sourceName; emit moreItemsRequestFinished( sourceName, direction, false, i18nc("@info", "Source currently gets processed, please try again later") ); return false; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Internal error: Invalid pointer to the data source stored"; emit moreItemsRequestFinished( sourceName, direction, false, "Internal error" ); return false; } const SourceRequestData data( sourceName ); qDebug() << data.defaultParameter; const ProviderPointer provider = providerFromId( data.defaultParameter ); if ( provider.isNull() ) { // Service provider couldn't be created, should only happen after provider updates, // where the provider worked to get timetable items, but the new provider version // does not work any longer and no more items can be requested emit moreItemsRequestFinished( sourceName, direction, false, "Provider could not be created" ); return false; } // Start the request QSharedPointer< AbstractRequest > request = dataSource->request( sourceName ); const QVariantList items = dataSource->timetableItems(); if ( items.isEmpty() ) { // TODO qWarning() << "No timetable items in data source" << sourceName; emit moreItemsRequestFinished( sourceName, direction, false, i18nc("@info", "No timetable items in data source") ); return false; } const QVariantHash item = (direction == Enums::EarlierItems ? items.first() : items.last()).toHash(); const QVariantMap _requestData = item[Enums::toString(Enums::RequestData)].toMap(); // TODO Convert to hash from map.. QVariantHash requestData; for ( QVariantMap::ConstIterator it = _requestData.constBegin(); it != _requestData.constEnd(); ++it ) { requestData.insert( it.key(), it.value() ); } // Start the request m_runningSources << nonAmbiguousName; provider->requestMoreItems( MoreItemsRequest(sourceName, request, requestData, direction) ); emit moreItemsRequestFinished( sourceName, direction ); return true; } ErrorCode PublicTransportEngine::errorCodeFromState( const QString &stateId ) { if ( stateId == QLatin1String("ready") ) { return NoError; } else if ( stateId == QLatin1String("gtfs_feed_import_pending") || stateId == QLatin1String("importing_gtfs_feed") ) { return ErrorNeedsImport; } else { return ErrorParsingFailed; } } bool PublicTransportEngine::request( const SourceRequestData &data ) { // Try to get the specific provider from m_providers (if it's not in there it is created) const ProviderPointer provider = providerFromId( data.defaultParameter ); if ( provider.isNull() || provider->type() == Enums::InvalidProvider ) { // Service provider couldn't be created // Remove erroneous source from running sources list const QString sourceName = data.request->sourceName(); m_runningSources.removeOne( disambiguateSourceName(sourceName) ); ProvidersDataSource *dataSource = providersDataSource(); const QString state = dataSource->providerState( data.defaultParameter ); if ( state != QLatin1String("ready") ) { const QVariantHash stateData = dataSource->providerStateData( data.defaultParameter ); setData( sourceName, "serviceProvider", data.defaultParameter ); setData( sourceName, "parseMode", data.request->parseModeName() ); setData( sourceName, "receivedData", "nothing" ); setData( sourceName, "error", true ); setData( sourceName, "errorCode", errorCodeFromState(state) ); setData( sourceName, "errorMessage", stateData["statusMessage"].toString() ); setData( sourceName, "updated", QDateTime::currentDateTime() ); } return false; } else if ( provider->useSeparateCityValue() && data.request->city().isEmpty() && !dynamic_cast(data.request) ) { qDebug() << QString("Service provider %1 needs a separate city value. Add to source name " "'|city=X', where X stands for the city name.") .arg( data.defaultParameter ); return false; // Service provider needs a separate city value } else if ( !provider->features().contains(Enums::ProvidesJourneys) && (data.parseMode == ParseForJourneysByDepartureTime || data.parseMode == ParseForJourneysByArrivalTime) ) { qDebug() << QString("Service provider %1 doesn't support journey searches.") .arg( data.defaultParameter ); return false; // Service provider doesn't support journey searches } // Store source name as currently being processed, to not start another // request if there is already a running one m_runningSources << disambiguateSourceName( data.name ); // Start the request provider->request( data.request ); return true; } int PublicTransportEngine::secsUntilUpdate( const QString &sourceName, QString *errorMessage ) { return getSecsUntilUpdate( sourceName, errorMessage ); } int PublicTransportEngine::minSecsUntilUpdate( const QString &sourceName, QString *errorMessage ) { return getSecsUntilUpdate( sourceName, errorMessage, UpdateWasRequestedManually ); } int PublicTransportEngine::getSecsUntilUpdate( const QString &sourceName, QString *errorMessage, UpdateFlags updateFlags ) { const QString nonAmbiguousName = disambiguateSourceName( sourceName ); if ( !m_dataSources.contains(nonAmbiguousName) ) { // The data source is not a timetable data source or does not exist if ( errorMessage ) { *errorMessage = i18nc("@info", "Data source is not an existing timetable " "data source: %1", sourceName); } return -1; } TimetableDataSource *dataSource = dynamic_cast< TimetableDataSource* >( m_dataSources[nonAmbiguousName] ); if ( !dataSource ) { qWarning() << "Internal error: Invalid pointer to the data source stored"; return -1; } const QDateTime time = sourceUpdateTime( dataSource, updateFlags ); return qMax( -1, 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( publictransport, PublicTransportEngine ) // this is needed since PublicTransportEngine is a QObject #include "build/publictransportdataengine.moc" diff --git a/engine/script/script_thread.cpp b/engine/script/script_thread.cpp index 290a4fa..1dbbf1d 100644 --- a/engine/script/script_thread.cpp +++ b/engine/script/script_thread.cpp @@ -1,907 +1,908 @@ /* * 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 "script_thread.h" // Own includes #include "config.h" #include "scriptapi.h" #include "script/serviceproviderscript.h" #include "serviceproviderdata.h" #include "request.h" #include "serviceproviderglobal.h" // KDE includes #include #include #include #include #include -#include + #include // Qt includes #include #include #include #include #include #include #include #include #include +#include ScriptJob::ScriptJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, QObject* parent ) : ThreadWeaver::Job(parent), m_engine(0), m_mutex(new QMutex(QMutex::Recursive)), m_data(data), m_eventLoop(0), m_published(0), m_quit(false), m_success(true) { Q_ASSERT_X( data.isValid(), "ScriptJob constructor", "Needs valid script data" ); // Use global storage, may contain non-persistent data from earlier requests. // Does not need to live in this thread, it does not create any new QObjects. QMutexLocker locker( m_mutex ); m_objects.storage = scriptStorage; qRegisterMetaType( "DepartureRequest" ); qRegisterMetaType( "ArrivalRequest" ); qRegisterMetaType( "JourneyRequest" ); qRegisterMetaType( "StopSuggestionRequest" ); qRegisterMetaType( "StopsByGeoPositionRequest" ); qRegisterMetaType( "AdditionalDataRequest" ); } ScriptJob::~ScriptJob() { // Abort, if still running requestAbort(); // Do some cleanup if not done already cleanup(); delete m_mutex; } void ScriptJob::requestAbort() { m_mutex->lock(); if ( !m_engine ) { // Is already finished m_mutex->unlock(); return; } else if ( m_quit ) { // Is already aborting, wait for the script engine to get destroyed QEventLoop loop; connect( m_engine, SIGNAL(destroyed(QObject*)), &loop, SLOT(quit()) ); m_mutex->unlock(); // Run the event loop, waiting for the engine to get destroyed or a 1 second timeout QTimer::singleShot( 1000, &loop, SLOT(quit()) ); loop.exec(); return; } // Is still running, remember to abort m_quit = true; // Abort running network requests, if any if ( m_objects.network->hasRunningRequests() ) { m_objects.network->abortAllRequests(); } // Abort script evaluation if ( m_engine ) { m_engine->abortEvaluation(); } // Wake waiting event loops if ( m_eventLoop ) { QEventLoop *loop = m_eventLoop; m_eventLoop = 0; loop->quit(); } m_mutex->unlock(); // Wait until signals are processed qApp->processEvents(); } ScriptAgent::ScriptAgent( QScriptEngine* engine, QObject *parent ) : QObject(parent), QScriptEngineAgent(engine) { } void ScriptAgent::functionExit( qint64 scriptId, const QScriptValue& returnValue ) { Q_UNUSED( scriptId ); Q_UNUSED( returnValue ); QTimer::singleShot( 250, this, SLOT(checkExecution()) ); } void ScriptAgent::checkExecution() { if ( !engine()->isEvaluating() ) { emit scriptFinished(); } } void ScriptJob::run() { m_mutex->lock(); if ( !loadScript(m_data.program) ) { kDebug() << "Script could not be loaded correctly"; m_mutex->unlock(); return; } QScriptEngine *engine = m_engine; ScriptObjects objects = m_objects; const QString scriptFileName = m_data.provider.scriptFileName(); const QScriptValueList arguments = QScriptValueList() << request()->toScriptValue( engine ); const ParseDocumentMode parseMode = request()->parseMode(); m_mutex->unlock(); // Add call to the appropriate function QString functionName; switch ( parseMode ) { case ParseForDepartures: case ParseForArrivals: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE; break; case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS; break; case ParseForStopSuggestions: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS; break; case ParseForAdditionalData: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA; break; default: kDebug() << "Parse mode unsupported:" << parseMode; break; } if ( functionName.isEmpty() ) { // This should never happen, therefore no i18n handleError( "Unknown parse mode" ); return; } // Check if the script function is implemented QScriptValue function = engine->globalObject().property( functionName ); if ( !function.isFunction() ) { handleError( i18nc("@info/plain", "Function %1 not implemented by " "the script %1", functionName, scriptFileName) ); return; } // Store start time of the script QElapsedTimer timer; timer.start(); // Call script function QScriptValue result = function.call( QScriptValue(), arguments ); if ( engine->hasUncaughtException() ) { // TODO Get filename where the exception occured, maybe use ScriptAgent for that handleError( i18nc("@info/plain", "Error in script function %1, " "line %2: %3.", functionName, engine->uncaughtExceptionLineNumber(), engine->uncaughtException().toString()) ); return; } // Inform about script run time DEBUG_ENGINE_JOBS( "Script finished in" << (timer.elapsed() / 1000.0) << "seconds: " << scriptFileName << parseMode ); GlobalTimetableInfo globalInfo; globalInfo.requestDate = QDate::currentDate(); globalInfo.delayInfoAvailable = !objects.result->isHintGiven( ResultObject::NoDelaysForStop ); // The called function returned, but asynchronous network requests may have been started. // Slots in the script may be connected to those requests and start script execution again. // In the execution new network requests may get started and so on. // // Wait until all network requests are finished and the script is not evaluating at the same // time. Define a global timeout, each waitFor() call subtracts the elapsed time. // // NOTE Network requests by default use a 30 seconds timeout, the global timeout should // be bigger. Synchronous requests that were started by signals of running asynchronous // requests are accounted as script execution time here. Synchronous requests that were // started before are already done. const int startTimeout = 60000; int timeout = startTimeout; while ( objects.network->hasRunningRequests() || engine->isEvaluating() ) { // Wait until all asynchronous network requests are finished if ( !waitFor(objects.network.data(), SIGNAL(allRequestsFinished()), WaitForNetwork, &timeout) ) { if ( timeout <= 0 ) { // Timeout expired while waiting for network requests to finish, // abort all requests that are still running objects.network->abortAllRequests(); QMutexLocker locker( m_mutex ); m_success = false; m_errorString = i18nc("@info", "Timeout expired after %1 while waiting for " "network requests to finish", KGlobal::locale()->prettyFormatDuration(startTimeout)); } cleanup(); return; } // Wait for script execution to finish, ie. slots connected to the last finished request(s) ScriptAgent agent( engine ); if ( !waitFor(&agent, SIGNAL(scriptFinished()), WaitForScriptFinish, &timeout) ) { if ( timeout <= 0 ) { // Timeout expired while waiting for script execution to finish, abort evaluation engine->abortEvaluation(); QMutexLocker locker( m_mutex ); m_success = false; m_errorString = i18nc("@info", "Timeout expired after %1 while waiting for script " "execution to finish", KGlobal::locale()->prettyFormatDuration(startTimeout)); } cleanup(); return; } // Check for new exceptions after waiting for script execution to finish (again) if ( engine->hasUncaughtException() ) { // TODO Get filename where the exception occured, maybe use ScriptAgent for that handleError( i18nc("@info/plain", "Error in script function %1, " "line %2: %3.", functionName, engine->uncaughtExceptionLineNumber(), engine->uncaughtException().toString()) ); return; } } // Publish remaining items, if any publish(); // Cleanup cleanup(); } void ScriptJob::cleanup() { QMutexLocker locker( m_mutex ); if ( !m_objects.storage.isNull() ) { m_objects.storage->checkLifetime(); } if ( !m_objects.result.isNull() ) { disconnect( m_objects.result.data(), 0, this, 0 ); } if ( m_engine ) { m_engine->deleteLater(); m_engine = 0; } } void ScriptJob::handleError( const QString &errorMessage ) { QMutexLocker locker( m_mutex ); kDebug() << "Error:" << errorMessage; kDebug() << "Backtrace:" << m_engine->uncaughtExceptionBacktrace().join("\n"); m_errorString = errorMessage; m_success = false; cleanup(); } QScriptValue networkRequestToScript( QScriptEngine *engine, const NetworkRequestPtr &request ) { return engine->newQObject( const_cast(request), QScriptEngine::QtOwnership, QScriptEngine::PreferExistingWrapperObject ); } void networkRequestFromScript( const QScriptValue &object, NetworkRequestPtr &request ) { request = qobject_cast( object.toQObject() ); } bool importExtension( QScriptEngine *engine, const QString &extension ) { if ( !ServiceProviderScript::allowedExtensions().contains(extension, Qt::CaseInsensitive) ) { if ( engine->availableExtensions().contains(extension, Qt::CaseInsensitive) ) { kDebug() << "Extension" << extension << "is not allowed currently"; } else { kDebug() << "Extension" << extension << "could not be found"; kDebug() << "Available extensions:" << engine->availableExtensions(); } kDebug() << "Allowed extensions:" << ServiceProviderScript::allowedExtensions(); return false; } else { if ( engine->importExtension(extension).isUndefined() ) { return true; } else { if ( engine->hasUncaughtException() ) { kDebug() << "Could not import extension" << extension << engine->uncaughtExceptionLineNumber() << engine->uncaughtException().toString(); kDebug() << "Backtrace:" << engine->uncaughtExceptionBacktrace().join("\n"); } return false; } } } bool ScriptJob::waitFor( QObject *sender, const char *signal, WaitForType type, int *timeout ) { if ( *timeout <= 0 ) { return false; } // Do not wait if the job was aborted or failed QMutexLocker locker( m_mutex ); if ( !m_success || m_quit ) { return false; } QScriptEngine *engine = m_engine; ScriptObjects objects = m_objects; // Test if the target condition is already met if ( (type == WaitForNetwork && objects.network->hasRunningRequests()) || (type == WaitForScriptFinish && engine->isEvaluating()) ) { // Not finished yet, wait for the given signal, // that should get emitted when the target condition is met QEventLoop loop; connect( sender, signal, &loop, SLOT(quit()) ); // Add a timeout to not wait forever (eg. because of an infinite loop in the script). // Use a QTimer variable to be able to check if the timeout caused the event loop to quit // rather than the given signal QTimer timer; timer.setSingleShot( true ); connect( &timer, SIGNAL(timeout()), &loop, SLOT(quit()) ); // Store a pointer to the event loop, to be able to quit it on abort. // Then start the timeout. m_eventLoop = &loop; QElapsedTimer elapsedTimer; elapsedTimer.start(); timer.start( *timeout ); m_mutex->unlock(); // Start the event loop waiting for the given signal / timeout. // QScriptEngine continues execution here or network requests continue to get handled and // may call script slots on finish. loop.exec(); // Test if the timeout has expired, ie. if the timer is no longer running (single shot) m_mutex->lock(); const bool timeoutExpired = !timer.isActive(); // Update remaining time of the timeout if ( timeoutExpired ) { *timeout = 0; } else { *timeout -= elapsedTimer.elapsed(); } // Test if the job was aborted while waiting if ( !m_eventLoop || m_quit ) { // Job was aborted m_eventLoop = 0; return false; } m_eventLoop = 0; // Generate errors if the timeout has expired and abort network requests / script execution if ( timeoutExpired ) { return false; } } // The target condition was met in time or was already met return true; } QScriptValue include( QScriptContext *context, QScriptEngine *engine ) { // Check argument count, include() call in global context? const QScriptContextInfo contextInfo( context->parentContext() ); if ( context->argumentCount() < 1 ) { context->throwError( i18nc("@info/plain", "One argument expected for include()") ); return engine->undefinedValue(); } else if ( context->parentContext() && context->parentContext()->parentContext() ) { QScriptContext *parentContext = context->parentContext()->parentContext(); bool error = false; while ( parentContext ) { const QScriptContextInfo parentContextInfo( parentContext ); if ( !parentContextInfo.fileName().isEmpty() && parentContextInfo.fileName() == contextInfo.fileName() ) { // Parent context is in the same file, error error = true; break; } parentContext = parentContext->parentContext(); } if ( error ) { context->throwError( i18nc("@info/plain", "include() calls must be in global context") ); return engine->undefinedValue(); } } // Check if this include() call is before all other statements QVariantHash includeData = context->callee().data().toVariant().toHash(); if ( includeData.contains(contextInfo.fileName()) ) { const quint16 maxIncludeLine = includeData[ contextInfo.fileName() ].toInt(); if ( contextInfo.lineNumber() > maxIncludeLine ) { context->throwError( i18nc("@info/plain", "include() calls must be the first statements") ); return engine->undefinedValue(); } } // Get argument and check that it's not pointing to another directory const QString fileName = context->argument(0).toString(); if ( fileName.contains('/') ) { context->throwError( i18nc("@info/plain", "Cannot include files from other directories") ); return engine->undefinedValue(); } // Find the script to be included const QString subDirectory = ServiceProviderGlobal::installationSubDirectory(); - const QString filePath = KGlobal::dirs()->findResource( "data", subDirectory + fileName ); + const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, subDirectory + fileName ); // Check if the script was already included QStringList includedFiles = engine->globalObject().property( "includedFiles" ).toVariant().toStringList(); if ( includedFiles.contains(filePath) ) { qWarning() << "File already included" << filePath; return engine->undefinedValue(); } // Try to open the file to be included QFile scriptFile( filePath ); if ( !scriptFile.open(QIODevice::ReadOnly) ) { context->throwError( i18nc("@info/plain", "Cannot find file to be included: " "%1", fileName) ); return engine->undefinedValue(); } // Read the file QTextStream stream( &scriptFile ); const QString program = stream.readAll(); scriptFile.close(); if ( !includeData.contains(scriptFile.fileName()) ) { includeData[ scriptFile.fileName() ] = maxIncludeLine( program ); QScriptValue includeFunction = engine->globalObject().property("include"); Q_ASSERT( includeFunction.isValid() ); includeFunction.setData( qScriptValueFromValue(engine, includeData) ); engine->globalObject().setProperty( "include", includeFunction, QScriptValue::KeepExistingFlags ); } // Set script context QScriptContext *parent = context->parentContext(); if ( parent ) { context->setActivationObject( parent->activationObject() ); context->setThisObject( parent->thisObject() ); } // Store included files in global property "includedFiles" includedFiles << filePath; includedFiles.removeDuplicates(); QScriptValue::PropertyFlags flags = QScriptValue::ReadOnly | QScriptValue::Undeletable; engine->globalObject().setProperty( "includedFiles", engine->newVariant(includedFiles), flags ); // Evaluate script return engine->evaluate( program, filePath ); } quint16 maxIncludeLine( const QString &program ) { // Get first lines of script code until the first statement which is not an include() call // The regular expression matches in blocks: Multiline comments (needs non-greedy regexp), // one line comments, whitespaces & newlines, include("...") calls and semi-colons (needed, // because the regexp is non-greedy) QRegExp programRegExp( "\\s*(?:\\s*//[^\\n]*\\n|/\\*.*\\*/|[\\s\\n]+|include\\s*\\(\\s*\"[^\"]*\"\\s*\\)|;)+" ); programRegExp.setMinimal( true ); int pos = 0, nextPos = 0; QString programBegin; while ( (pos = programRegExp.indexIn(program, pos)) == nextPos ) { const QString capture = programRegExp.cap(); programBegin.append( capture ); pos += programRegExp.matchedLength(); nextPos = pos; } return programBegin.count( '\n' ); } bool ScriptJob::loadScript( const QSharedPointer< QScriptProgram > &script ) { if ( !script ) { kDebug() << "Invalid script data"; return false; } // Create script engine QMutexLocker locker( m_mutex ); m_engine = new QScriptEngine(); foreach ( const QString &extension, m_data.provider.scriptExtensions() ) { if ( !importExtension(m_engine, extension) ) { m_errorString = i18nc("@info/plain", "Could not load script extension " "%1.", extension); m_success = false; cleanup(); return false; } } // Create and attach script objects. // The Storage object is already created and will not be replaced by a new instance. // It lives in the GUI thread and gets used in all thread jobs to not erase non-persistently // stored data after each request. m_objects.createObjects( m_data ); m_objects.attachToEngine( m_engine, m_data ); // Connect the publish() signal directly (the result object lives in the thread that gets // run by this job, this job itself lives in the GUI thread). Connect directly to ensure // the script objects and the request are still valid (prevent crashes). connect( m_objects.result.data(), SIGNAL(publish()), this, SLOT(publish()), Qt::DirectConnection ); // Load the script program m_mutex->unlock(); m_engine->evaluate( *script ); m_mutex->lock(); if ( m_engine->hasUncaughtException() ) { kDebug() << "Error in the script" << m_engine->uncaughtExceptionLineNumber() << m_engine->uncaughtException().toString(); kDebug() << "Backtrace:" << m_engine->uncaughtExceptionBacktrace().join("\n"); m_errorString = i18nc("@info/plain", "Error in script, line %1: %2.", m_engine->uncaughtExceptionLineNumber(), m_engine->uncaughtException().toString()); m_success = false; cleanup(); return false; } else { return true; } } bool ScriptJob::hasDataToBePublished() const { QMutexLocker locker( m_mutex ); return !m_objects.isValid() ? false : m_objects.result->count() > m_published; } void ScriptJob::publish() { // This slot gets run in the thread of this job // Only publish, if there is data which is not already published if ( hasDataToBePublished() ) { m_mutex->lock(); GlobalTimetableInfo globalInfo; const QList< TimetableData > data = m_objects.result->data().mid( m_published ); const ResultObject::Features features = m_objects.result->features(); const ResultObject::Hints hints = m_objects.result->hints(); const QString lastUserUrl = m_objects.network->lastUserUrl(); const bool couldNeedForcedUpdate = m_published > 0; const MoreItemsRequest *moreItemsRequest = dynamic_cast(request()); const AbstractRequest *_request = moreItemsRequest ? moreItemsRequest->request().data() : request(); const ParseDocumentMode parseMode = request()->parseMode(); m_lastUrl = m_objects.network->lastUrl(); // TODO Store all URLs m_lastUserUrl = m_objects.network->lastUserUrl(); m_published += data.count(); // Unlock mutex after copying the request object, then emit it with an xxxReady() signal switch ( parseMode ) { case ParseForDepartures: { Q_ASSERT(dynamic_cast(_request)); const DepartureRequest departureRequest = *dynamic_cast(_request); m_mutex->unlock(); emit departuresReady( data, features, hints, lastUserUrl, globalInfo, departureRequest, couldNeedForcedUpdate ); break; } case ParseForArrivals: { Q_ASSERT(dynamic_cast(_request)); const ArrivalRequest arrivalRequest = *dynamic_cast(_request); m_mutex->unlock(); emit arrivalsReady( data, features, hints, lastUserUrl, globalInfo, arrivalRequest, couldNeedForcedUpdate ); break; } case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: { Q_ASSERT(dynamic_cast(_request)); const JourneyRequest journeyRequest = *dynamic_cast(_request); m_mutex->unlock(); emit journeysReady( data, features, hints, lastUserUrl, globalInfo, journeyRequest, couldNeedForcedUpdate ); break; } case ParseForStopSuggestions: { Q_ASSERT(dynamic_cast(_request)); const StopSuggestionRequest stopSuggestionRequest = *dynamic_cast< const StopSuggestionRequest* >( _request ); m_mutex->unlock(); emit stopSuggestionsReady( data, features, hints, lastUserUrl, globalInfo, stopSuggestionRequest, couldNeedForcedUpdate ); break; } case ParseForAdditionalData: { if ( data.count() > 1 ) { qWarning() << "The script added more than one result in an additional data request"; kDebug() << "All received additional data for item" << dynamic_cast(_request)->itemNumber() << ':' << data; m_errorString = i18nc("@info/plain", "The script added more than one result in " "an additional data request."); m_success = false; cleanup(); m_mutex->unlock(); return; } // Additional data gets requested per timetable item, only one result expected const TimetableData additionalData = data.first(); if ( additionalData.isEmpty() ) { qWarning() << "Did not find any additional data."; m_errorString = i18nc("@info/plain", "TODO."); // TODO m_success = false; cleanup(); m_mutex->unlock(); return; } Q_ASSERT(dynamic_cast(_request)); const AdditionalDataRequest additionalDataRequest = *dynamic_cast< const AdditionalDataRequest* >( _request ); m_mutex->unlock(); emit additionalDataReady( additionalData, features, hints, lastUserUrl, globalInfo, additionalDataRequest, couldNeedForcedUpdate ); break; } default: m_mutex->unlock(); kDebug() << "Parse mode unsupported:" << parseMode; break; } } } class DepartureJobPrivate { public: DepartureJobPrivate( const DepartureRequest &request ) : request(request) {}; DepartureRequest request; }; DepartureJob::DepartureJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const DepartureRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new DepartureJobPrivate(request)) { } DepartureJob::~DepartureJob() { delete d; } const AbstractRequest *DepartureJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } QString ScriptJob::sourceName() const { QMutexLocker locker( m_mutex ); return request()->sourceName(); } const AbstractRequest *ScriptJob::cloneRequest() const { QMutexLocker locker( m_mutex ); return request()->clone(); } class ArrivalJobPrivate { public: ArrivalJobPrivate( const ArrivalRequest &request ) : request(request) {}; ArrivalRequest request; }; ArrivalJob::ArrivalJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const ArrivalRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new ArrivalJobPrivate(request)) { } ArrivalJob::~ArrivalJob() { delete d; } const AbstractRequest *ArrivalJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class JourneyJobPrivate { public: JourneyJobPrivate( const JourneyRequest &request ) : request(request) {}; JourneyRequest request; }; JourneyJob::JourneyJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const JourneyRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new JourneyJobPrivate(request)) { } JourneyJob::~JourneyJob() { delete d; } const AbstractRequest *JourneyJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class StopSuggestionsJobPrivate { public: StopSuggestionsJobPrivate( const StopSuggestionRequest &request ) : request(request) {}; StopSuggestionRequest request; }; StopSuggestionsJob::StopSuggestionsJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const StopSuggestionRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new StopSuggestionsJobPrivate(request)) { } StopSuggestionsJob::~StopSuggestionsJob() { delete d; } const AbstractRequest *StopSuggestionsJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class StopsByGeoPositionJobPrivate { public: StopsByGeoPositionJobPrivate( const StopsByGeoPositionRequest &request ) : request(request) {}; StopsByGeoPositionRequest request; }; StopsByGeoPositionJob::StopsByGeoPositionJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const StopsByGeoPositionRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new StopsByGeoPositionJobPrivate(request)) { } StopsByGeoPositionJob::~StopsByGeoPositionJob() { delete d; } const AbstractRequest *StopsByGeoPositionJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class AdditionalDataJobPrivate { public: AdditionalDataJobPrivate( const AdditionalDataRequest &request ) : request(request) {}; AdditionalDataRequest request; }; AdditionalDataJob::AdditionalDataJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const AdditionalDataRequest &request, QObject *parent ) : ScriptJob(data, scriptStorage, parent), d(new AdditionalDataJobPrivate(request)) { } AdditionalDataJob::~AdditionalDataJob() { delete d; } const AbstractRequest *AdditionalDataJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class MoreItemsJobPrivate { public: MoreItemsJobPrivate( const MoreItemsRequest &request ) : request(request) {}; MoreItemsRequest request; }; MoreItemsJob::MoreItemsJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const MoreItemsRequest &request, QObject *parent ) : ScriptJob(data, scriptStorage, parent), d(new MoreItemsJobPrivate(request)) { } MoreItemsJob::~MoreItemsJob() { delete d; } const AbstractRequest *MoreItemsJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } bool ScriptJob::success() const { QMutexLocker locker( m_mutex ); return m_success; } int ScriptJob::publishedItems() const { QMutexLocker locker( m_mutex ); return m_published; } QString ScriptJob::errorString() const { QMutexLocker locker( m_mutex ); return m_errorString; } QString ScriptJob::lastDownloadUrl() const { QMutexLocker locker( m_mutex ); return m_lastUrl; } QString ScriptJob::lastUserUrl() const { QMutexLocker locker( m_mutex ); return m_lastUserUrl; } diff --git a/engine/script/scriptapi.cpp b/engine/script/scriptapi.cpp index 37a7f69..32b4aa6 100644 --- a/engine/script/scriptapi.cpp +++ b/engine/script/scriptapi.cpp @@ -1,2379 +1,2380 @@ /* * 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 "scriptapi.h" // Own includes #include "config.h" #include "global.h" #include "serviceproviderglobal.h" // KDE includes -#include + #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Other includes #include +#include namespace ScriptApi { NetworkRequest::NetworkRequest( QObject *parent ) : QObject(parent), m_mutex(new QMutex(QMutex::Recursive)), m_network(0), m_isFinished(false), m_request(0), m_reply(0) { } NetworkRequest::NetworkRequest( const QString &url, const QString &userUrl, Network *network, QObject* parent ) : QObject(parent), m_mutex(new QMutex(QMutex::Recursive)), m_url(url), m_userUrl(userUrl.isEmpty() ? url : userUrl), m_network(network), m_isFinished(false), m_request(new QNetworkRequest(url)), m_reply(0) { } NetworkRequest::~NetworkRequest() { abort(); delete m_request; delete m_mutex; } void NetworkRequest::abort() { const bool timedOut = qobject_cast< QTimer* >( sender() ); if ( !isRunning() ) { if ( timedOut ) { kDebug() << "Timeout, but request already finished"; } return; } // isRunning() returned true => m_reply != 0 m_mutex->lockInline(); disconnect( m_reply, 0, this, 0 ); m_reply->abort(); m_reply->deleteLater(); m_reply = 0; const QString url = m_url; const QVariant userData = m_userData; m_mutex->unlockInline(); emit aborted( timedOut ); emit finished( QByteArray(), true, i18nc("@info/plain", "The request was aborted"), -1, 0, url, userData ); } void NetworkRequest::slotReadyRead() { m_mutex->lockInline(); if ( !m_reply ) { // Prevent crashes on exit m_mutex->unlockInline(); qWarning() << "Reply object already deleted, aborted?"; return; } // Read all data, decode it and give it to the script QByteArray data = m_reply->readAll(); m_data.append( data ); if ( data.isEmpty() ) { qWarning() << "Error downloading" << m_url << m_reply->errorString(); } m_mutex->unlockInline(); emit readyRead( data ); } QByteArray gzipDecompress( QByteArray compressData ) { // Strip header and trailer compressData.remove( 0, 10 ); compressData.chop( 12 ); // FIXME Decompress in one chunk, because otherwise it fails with error -3 "distance too far back", // estimate size of uncompressed data, expect that the data was compressed at best to 20% size, // limit to 512KB const int chunkSize = qMin( int(compressData.size() / 0.2), 512 * 1024 ); kDebug() << "Chunk size:" << chunkSize; if ( chunkSize == 512 * 1024 ) { qWarning() << "Maximum chunk size for decompression reached, may fail"; } unsigned char buffer[ chunkSize ]; z_stream stream; stream.next_in = (Bytef*)(compressData.data()); stream.avail_in = compressData.size(); stream.total_in = 0; stream.total_out = 0; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; if ( inflateInit2(&stream, -8) != Z_OK ) { kDebug() << "cmpr_stream error!"; return QByteArray(); } QByteArray uncompressed; do { stream.next_out = buffer; stream.avail_out = chunkSize; int status = inflate( &stream, Z_SYNC_FLUSH ); if ( status == Z_OK || status == Z_STREAM_END ) { uncompressed.append( QByteArray::fromRawData( (char*)buffer, chunkSize - stream.avail_out) ); if ( status == Z_STREAM_END ) { break; } } else { qWarning() << "Error while decompressing" << status << stream.msg; if ( status == Z_NEED_DICT || status == Z_DATA_ERROR || status == Z_MEM_ERROR ) { inflateEnd( &stream ); } return QByteArray(); } } while( stream.avail_out == 0 ); inflateEnd( &stream ); return uncompressed; } void NetworkRequest::slotFinished() { m_mutex->lockInline(); if ( !m_reply ) { // Prevent crashes on exit m_mutex->unlockInline(); qWarning() << "Reply object already deleted, aborted?"; return; } if ( m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid() ) { if ( m_redirectUrl.isValid() ) { qWarning() << "Only one redirection allowed, from" << m_url << "to" << m_redirectUrl; qWarning() << "New redirection to" << m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); } else { m_redirectUrl = m_reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toUrl(); DEBUG_NETWORK("Redirection to" << m_redirectUrl); // Delete the reply m_reply->deleteLater(); m_reply = 0; delete m_request; m_request = new QNetworkRequest( m_redirectUrl ); const QUrl redirectUrl = m_redirectUrl; m_mutex->unlockInline(); emit redirected( redirectUrl ); return; } } const int size = m_reply->size(); const int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Read all data, decode it and give it to the script m_data.append( m_reply->readAll() ); if ( m_data.isEmpty() ) { qWarning() << "Error downloading" << m_url << (m_reply ? m_reply->errorString() : "Reply already deleted"); } // Check if the data is compressed and was not decompressed by QNetworkAccessManager if ( m_data.length() >= 2 && quint8(m_data[0]) == 0x1f && quint8(m_data[1]) == 0x8b ) { // Data is compressed, uncompress it // NOTE qUncompress() did not work here, "invalid zip" m_data = gzipDecompress( m_data ); DEBUG_NETWORK("Uncompressed data from" << size << "Bytes to" << m_data.size() << "Bytes, ratio:" << (100 - (m_data.isEmpty() ? 100 : qRound(100 * size / m_data.size()))) << '%'); } DEBUG_NETWORK("Request finished" << m_reply->url()); if ( m_reply->url().isEmpty() ) { qWarning() << "Empty URL in QNetworkReply!"; } const bool hasError = m_reply->error() != QNetworkReply::NoError; const QString errorString = m_reply->errorString(); m_reply->deleteLater(); m_reply = 0; m_isFinished = true; const QByteArray data = m_data; const QString url = m_url; const QVariant userData = m_userData; m_mutex->unlockInline(); emit finished( data, hasError, errorString, statusCode, size, url, userData ); } void NetworkRequest::started( QNetworkReply* reply, int timeout ) { m_mutex->lockInline(); if ( !m_network ) { qWarning() << "Can't decode, no m_network given..."; m_mutex->unlockInline(); return; } m_data.clear(); m_reply = reply; if ( timeout > 0 ) { QTimer::singleShot( timeout, this, SLOT(abort()) ); } // Connect to signals of reply only when the associated signals of this class are connected if ( receivers(SIGNAL(readyRead(QByteArray))) > 0 ) { connect( m_reply, SIGNAL(readyRead()), this, SLOT(slotReadyRead()) ); } connect( m_reply, SIGNAL(finished()), this, SLOT(slotFinished()) ); m_mutex->unlockInline(); emit started(); } bool NetworkRequest::isValid() const { QMutexLocker locker( m_mutex ); if ( m_request ) { return true; } else { // Default constructor used, not available to scripts kDebug() << "Request is invalid"; return false; } } QByteArray NetworkRequest::getCharset( const QString& charset ) const { QMutexLocker locker( m_mutex ); QByteArray baCharset; if ( charset.isEmpty() ) { // No charset given, use the one specified in the ContentType header baCharset = m_request->header( QNetworkRequest::ContentTypeHeader ).toByteArray(); // No ContentType header, use utf8 if ( baCharset.isEmpty() ) { baCharset = "utf8"; } } else { baCharset = charset.toUtf8(); } return baCharset; } QByteArray NetworkRequest::postDataByteArray() const { QMutexLocker locker( m_mutex ); return m_postData; } void NetworkRequest::setPostData( const QString& postData, const QString& charset ) { if ( !isValid() ) { return; } if ( isRunning() ) { kDebug() << "Cannot set POST data for an already running request!"; return; } QByteArray baCharset = getCharset( charset ); QTextCodec *codec = QTextCodec::codecForName( baCharset ); QMutexLocker locker( m_mutex ); if ( codec ) { m_request->setHeader( QNetworkRequest::ContentTypeHeader, baCharset ); m_postData = codec->fromUnicode( postData ); } else { kDebug() << "Codec" << baCharset << "couldn't be found to encode the data " "to post, now using UTF-8"; m_request->setHeader( QNetworkRequest::ContentTypeHeader, "utf8" ); m_postData = postData.toUtf8(); } } QString NetworkRequest::header( const QString &header, const QString& charset ) const { if ( !isValid() ) { return QString(); } QByteArray baCharset = getCharset( charset ); QTextCodec *codec = QTextCodec::codecForName( baCharset ); QMutexLocker locker( m_mutex ); return m_request->rawHeader( codec ? codec->fromUnicode(header) : header.toUtf8() ); } void NetworkRequest::setHeader( const QString& header, const QString& value, const QString& charset ) { if ( !isValid() ) { return; } if ( isRunning() ) { kDebug() << "Cannot set headers for an already running request!"; return; } QByteArray baCharset = getCharset( charset ); QTextCodec *codec = QTextCodec::codecForName( baCharset ); QMutexLocker locker( m_mutex ); if ( codec ) { m_request->setRawHeader( codec->fromUnicode(header), codec->fromUnicode(value) ); } else { kDebug() << "Codec" << baCharset << "couldn't be found to encode the data " "to post, now using UTF-8"; m_request->setRawHeader( header.toUtf8(), value.toUtf8() ); } } QNetworkRequest* NetworkRequest::request() const { QMutexLocker locker( m_mutex ); return m_request; } Network::Network( const QByteArray &fallbackCharset, QObject* parent ) : QObject(parent), m_mutex(new QMutex(QMutex::Recursive)), m_fallbackCharset(fallbackCharset), m_manager(new QNetworkAccessManager()), m_quit(false), m_synchronousRequestCount(0), m_lastDownloadAborted(false) { qRegisterMetaType< NetworkRequest* >( "NetworkRequest*" ); qRegisterMetaType< NetworkRequest::Ptr >( "NetworkRequest::Ptr" ); } Network::~Network() { // Quit event loop of possibly running synchronous requests m_mutex->lock(); m_quit = true; m_mutex->unlock(); // Notify about the abort request to abort running synchronous requests abortSynchronousRequests(); qApp->processEvents(); // Handle the signal const QList< NetworkRequest::Ptr > _runningRequests = runningRequests(); if ( !_runningRequests.isEmpty() ) { qWarning() << "Deleting Network object with" << _runningRequests.count() << "running requests"; foreach ( const NetworkRequest::Ptr &request, _runningRequests ) { request->abort(); } } delete m_mutex; m_manager->deleteLater(); } NetworkRequest* Network::createRequest( const QString& url, const QString &userUrl ) { NetworkRequest *request = new NetworkRequest( url, userUrl, this, parent() ); NetworkRequest::Ptr requestPtr( request ); m_mutex->lock(); m_requests << requestPtr; m_mutex->unlock(); connect( request, SIGNAL(started()), this, SLOT(slotRequestStarted()) ); connect( request, SIGNAL(finished(QByteArray,bool,QString,int,int)), this, SLOT(slotRequestFinished(QByteArray,bool,QString,int,int)) ); connect( request, SIGNAL(aborted()), this, SLOT(slotRequestAborted()) ); connect( request, SIGNAL(redirected(QUrl)), this, SLOT(slotRequestRedirected(QUrl)) ); return request; } NetworkRequest::Ptr Network::getSharedRequest( NetworkRequest *request ) const { QMutexLocker locker( m_mutex ); foreach ( const NetworkRequest::Ptr &sharedRequest, m_requests ) { if ( sharedRequest.data() == request ) { return sharedRequest; } } return NetworkRequest::Ptr(); } void Network::slotRequestStarted() { NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest m_mutex->lockInline(); m_lastDownloadAborted = false; m_mutex->unlockInline(); emit requestStarted( sharedRequest ); } void Network::slotRequestFinished( const QByteArray &data, bool error, const QString &errorString, int statusCode, int size ) { NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest // Remove finished request from request list and add it to the list of finished requests // to not have it deleted here. The script might try to use the request object later, // eg. because of connected slots, and will crash if the request object was already deleted. // This way NetworkRequest objects stay alive at least as long as the Network object does. m_mutex->lockInline(); const QDateTime timestamp = QDateTime::currentDateTime(); m_requests.removeOne( sharedRequest ); m_finishedRequests << sharedRequest; m_mutex->unlockInline(); emit requestFinished( sharedRequest, data, error, errorString, timestamp, statusCode, size ); if ( !hasRunningRequests() ) { emit allRequestsFinished(); } } void Network::slotRequestRedirected( const QUrl &newUrl ) { NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest m_mutex->lockInline(); QNetworkReply *reply = m_manager->get( *request->request() ); m_lastUrl = newUrl.toString(); m_mutex->unlockInline(); request->started( reply ); DEBUG_NETWORK("Redirected to" << newUrl); emit requestRedirected( sharedRequest, newUrl ); } void Network::slotRequestAborted() { m_mutex->lockInline(); if ( m_quit ) { m_mutex->unlockInline(); return; } NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); if ( !sharedRequest ) { qWarning() << "Network object already deleted?"; } // Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest DEBUG_NETWORK("Aborted" << request->url()); m_lastDownloadAborted = true; m_mutex->unlockInline(); emit requestAborted( sharedRequest ); } bool Network::checkRequest( NetworkRequest* request ) { // Wrong argument type from script or no argument if ( !request ) { kDebug() << "Need a NetworkRequest object as argument, create it with " "'network.createRequest(url)'"; return false; } // The same request can not be executed more than once at a time if ( request->isRunning() ) { kDebug() << "Request is currently running" << request->url(); return false; } else if ( request->isFinished() ) { kDebug() << "Request is already finished" << request->url(); return false; } return request->isValid(); } void Network::get( NetworkRequest* request, int timeout ) { if ( !checkRequest(request) ) { return; } // Create a get request m_mutex->lockInline(); QNetworkReply *reply = m_manager->get( *request->request() ); m_lastUrl = request->url(); m_lastUserUrl = request->userUrl(); m_mutex->unlockInline(); request->started( reply, timeout ); } void Network::head( NetworkRequest* request, int timeout ) { if ( !checkRequest(request) ) { return; } // Create a head request m_mutex->lockInline(); QNetworkReply *reply = m_manager->head( *request->request() ); m_lastUrl = request->url(); m_lastUserUrl = request->userUrl(); m_mutex->unlockInline(); request->started( reply, timeout ); } void Network::post( NetworkRequest* request, int timeout ) { if ( !checkRequest(request) ) { return; } // Create a head request m_mutex->lockInline(); QNetworkReply *reply = m_manager->post( *request->request(), request->postDataByteArray() ); m_lastUrl = request->url(); m_lastUserUrl = request->userUrl(); m_mutex->unlockInline(); request->started( reply, timeout ); } void Network::abortAllRequests() { const QList< NetworkRequest::Ptr > requests = runningRequests(); DEBUG_NETWORK("Abort" << requests.count() << "request(s)"); for ( int i = requests.count() - 1; i >= 0; --i ) { // Calling abort automatically removes the aborted request from m_createdRequests requests[i]->abort(); } // Notify about the abort request to abort running synchronous requests abortSynchronousRequests(); } void Network::abortSynchronousRequests() { emit doAbortSynchronousRequests(); } QByteArray Network::getSynchronous( const QString &url, const QString &userUrl, int timeout ) { // Create a get request QNetworkRequest request( url ); DEBUG_NETWORK("Start synchronous request" << url); m_mutex->lockInline(); QNetworkReply *reply = m_manager->get( request ); ++m_synchronousRequestCount; m_lastUrl = url; m_lastUserUrl = userUrl.isEmpty() ? url : userUrl; m_lastDownloadAborted = false; m_mutex->unlockInline(); emit synchronousRequestStarted( url ); const QTime start = QTime::currentTime(); int redirectCount = 0; const int maxRedirections = 3; forever { // Use an event loop to wait for execution of the request, // ie. make netAccess.download() synchronous for scripts QEventLoop eventLoop; connect( reply, SIGNAL(finished()), &eventLoop, SLOT(quit()) ); connect( this, SIGNAL(doAbortSynchronousRequests()), &eventLoop, SLOT(quit()) ); if ( timeout > 0 ) { QTimer::singleShot( timeout, &eventLoop, SLOT(quit()) ); } if ( !reply->isFinished() ) { eventLoop.exec( QEventLoop::ExcludeUserInputEvents ); } m_mutex->lock(); const bool quit = m_quit || m_lastDownloadAborted || reply->isRunning(); m_mutex->unlock(); // Check if the timeout occured before the request finished if ( quit ) { DEBUG_NETWORK("Cancelled, destroyed or timeout while downloading" << url); emitSynchronousRequestFinished( url, QByteArray(), true ); return QByteArray(); } if ( reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid() ) { ++redirectCount; if ( redirectCount > maxRedirections ) { reply->deleteLater(); emitSynchronousRequestFinished( url, QByteArray("Too many redirections"), true, 200, start.msecsTo(QTime::currentTime()) ); return QByteArray(); } // Request the redirection target const QUrl redirectUrl = reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toUrl(); request.setUrl( redirectUrl ); DEBUG_NETWORK("Redirected to" << redirectUrl); m_mutex->lock(); delete reply; reply = m_manager->get( request ); m_lastUrl = redirectUrl.toString(); m_mutex->unlock(); emit synchronousRequestRedirected( redirectUrl.toEncoded() ); } else { // No (more) redirection break; } } const int time = start.msecsTo( QTime::currentTime() ); const int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); DEBUG_NETWORK("Waited" << (time / 1000.0) << "seconds for download of" << url << "Status:" << statusCode); // Read all data, decode it and give it to the script QByteArray data = reply->readAll(); reply->deleteLater(); if ( data.isEmpty() ) { qWarning() << "Error downloading" << url << reply->errorString(); emitSynchronousRequestFinished( url, QByteArray(), true, statusCode, time ); return QByteArray(); } else { emitSynchronousRequestFinished( url, data, false, statusCode, time, data.size() ); return data; } } void Network::emitSynchronousRequestFinished( const QString &url, const QByteArray &data, bool cancelled, int statusCode, int waitTime, int size ) { m_mutex->lock(); --m_synchronousRequestCount; m_mutex->unlock(); emit synchronousRequestFinished( url, data, cancelled, statusCode, waitTime, size ); if ( !hasRunningRequests() ) { emit allRequestsFinished(); } } ResultObject::ResultObject( QObject* parent ) : QObject(parent), m_mutex(new QMutex()), m_features(DefaultFeatures), m_hints(NoHint) { } ResultObject::~ResultObject() { delete m_mutex; } bool ResultObject::isHintGiven( ResultObject::Hint hint ) const { QMutexLocker locker( m_mutex ); return m_hints.testFlag( hint ); } void ResultObject::giveHint( ResultObject::Hint hint, bool enable ) { QMutexLocker locker( m_mutex ); if ( enable ) { // Remove incompatible flags of hint if any if ( hint == CityNamesAreLeft && m_hints.testFlag(CityNamesAreRight) ) { m_hints &= ~CityNamesAreRight; } else if ( hint == CityNamesAreRight && m_hints.testFlag(CityNamesAreLeft) ) { m_hints &= ~CityNamesAreLeft; } m_hints |= hint; } else { m_hints &= ~hint; } } bool ResultObject::isFeatureEnabled( ResultObject::Feature feature ) const { QMutexLocker locker( m_mutex ); return m_features.testFlag( feature ); } void ResultObject::enableFeature( ResultObject::Feature feature, bool enable ) { QMutexLocker locker( m_mutex ); if ( enable ) { m_features |= feature; } else { m_features &= ~feature; } } void ResultObject::dataList( const QList< TimetableData > &dataList, PublicTransportInfoList *infoList, ParseDocumentMode parseMode, Enums::VehicleType defaultVehicleType, const GlobalTimetableInfo *globalInfo, ResultObject::Features features, ResultObject::Hints hints ) { // TODO Use features and hints QDate curDate; QTime lastTime; int dayAdjustment = hints.testFlag(DatesNeedAdjustment) ? QDate::currentDate().daysTo(globalInfo->requestDate) : 0; if ( dayAdjustment != 0 ) { kDebug() << "Dates get adjusted by" << dayAdjustment << "days"; } // 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 // This is the range of occurrences of one word in stop names, // that causes that word to be removed const int minWordOccurrence = qMax( 2, dataList.count() ); const int maxWordOccurrence = qMax( 3, dataList.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; // Read timetable data from the script for ( int i = 0; i < dataList.count(); ++i ) { TimetableData timetableData = dataList[ i ]; // Set default vehicle type if none is set if ( !timetableData.contains(Enums::TypeOfVehicle) || timetableData[Enums::TypeOfVehicle].toString().isEmpty() ) { timetableData[ Enums::TypeOfVehicle ] = static_cast( defaultVehicleType ); } if ( parseMode != ParseForStopSuggestions ) { QDateTime dateTime = timetableData[ Enums::DepartureDateTime ].toDateTime(); QDate departureDate = timetableData[ Enums::DepartureDate ].toDate(); QTime departureTime = timetableData[ Enums::DepartureTime ].toTime(); if ( !dateTime.isValid() && !departureTime.isValid() ) { kDebug() << "No departure time given!" << timetableData[Enums::DepartureTime]; kDebug() << "Use eg. helper.matchTime() to convert a string to a time object"; } if ( !dateTime.isValid() ) { if ( departureDate.isValid() ) { dateTime = QDateTime( departureDate, departureTime ); } else if ( curDate.isNull() ) { // First departure if ( QTime::currentTime().hour() < 3 && departureTime.hour() > 21 ) { dateTime.setDate( QDate::currentDate().addDays(-1) ); } else if ( QTime::currentTime().hour() > 21 && departureTime.hour() < 3 ) { dateTime.setDate( QDate::currentDate().addDays(1) ); } else { dateTime.setDate( QDate::currentDate() ); } } else if ( lastTime.secsTo(departureTime) < -5 * 60 ) { // Time too much ealier than last time, estimate it's tomorrow dateTime.setDate( curDate.addDays(1) ); } else { dateTime.setDate( curDate ); } timetableData[ Enums::DepartureDateTime ] = dateTime; } if ( dayAdjustment != 0 ) { dateTime.setDate( dateTime.date().addDays(dayAdjustment) ); timetableData[ Enums::DepartureDateTime ] = dateTime; } curDate = dateTime.date(); lastTime = departureTime; } // Create info object for the timetable data PublicTransportInfoPtr info; if ( parseMode == ParseForJourneysByDepartureTime || parseMode == ParseForJourneysByArrivalTime ) { info = QSharedPointer( new JourneyInfo(timetableData) ); } else if ( parseMode == ParseForDepartures || parseMode == ParseForArrivals ) { info = QSharedPointer( new DepartureInfo(timetableData) ); } else if ( parseMode == ParseForStopSuggestions ) { info = QSharedPointer( new StopInfo(timetableData) ); } if ( !info->isValid() ) { info.clear(); continue; } // Find word to remove from beginning/end of stop names, if not already found // TODO Use hint from the data engine.. if ( features.testFlag(AutoRemoveCityFromStopNames) && removeFirstWord.isEmpty() && removeLastWord.isEmpty() ) { // First count the first/last word of the target stop name const QString target = info->value( Enums::Target ).toString(); int pos = target.indexOf( ' ' ); if ( pos > 0 && ++firstWordCounts[target.left(pos)] >= maxWordOccurrence ) { removeFirstWord = target.left(pos); } if ( rxLastWord.indexIn(target) != -1 && ++lastWordCounts[rxLastWord.cap()] >= maxWordOccurrence ) { removeLastWord = rxLastWord.cap(); } // Check if route stop names are available if ( info->contains(Enums::RouteStops) ) { QStringList stops = info->value( Enums::RouteStops ).toStringList(); QString target = info->value( Enums::Target ).toString(); // TODO Break if 70% or at least three of the route stop names // begin/end with the same word // int minCount = qMax( 3, int(stops.count() * 0.7) ); foreach ( const QString &stop, stops ) { // Test first word pos = stop.indexOf( ' ' ); const QString newFirstWord = stop.left( pos ); if ( pos > 0 && ++firstWordCounts[newFirstWord] >= maxWordOccurrence ) { removeFirstWord = newFirstWord; break; } // Test last word if ( rxLastWord.indexIn(stop) != -1 && ++lastWordCounts[rxLastWord.cap()] >= maxWordOccurrence ) { removeLastWord = rxLastWord.cap(); break; } } } } infoList->append( info ); } // Remove word with most occurrences from beginning/end of stop names if ( features.testFlag(AutoRemoveCityFromStopNames) ) { 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 for ( int i = 0; i < infoList->count(); ++i ) { QSharedPointer info = infoList->at( i ); QString target = info->value( Enums::Target ).toString(); if ( target.startsWith(removeFirstWord) ) { target = target.mid( removeFirstWord.length() + 1 ); info->insert( Enums::TargetShortened, target ); } QStringList stops = info->value( Enums::RouteStops ).toStringList(); for ( int i = 0; i < stops.count(); ++i ) { if ( stops[i].startsWith(removeFirstWord) ) { stops[i] = stops[i].mid( removeFirstWord.length() + 1 ); } } info->insert( Enums::RouteStopsShortened, stops ); } } else if ( !removeLastWord.isEmpty() ) { // Remove removeLastWord from all stop names for ( int i = 0; i < infoList->count(); ++i ) { QSharedPointer info = infoList->at( i ); QString target = info->value( Enums::Target ).toString(); if ( target.endsWith(removeLastWord) ) { target = target.left( target.length() - removeLastWord.length() ); info->insert( Enums::TargetShortened, target ); } QStringList stops = info->value( Enums::RouteStops ).toStringList(); for ( int i = 0; i < stops.count(); ++i ) { if ( stops[i].endsWith(removeLastWord) ) { stops[i] = stops[i].left( stops[i].length() - removeLastWord.length() ); } } info->insert( Enums::RouteStopsShortened, stops ); } } } } QString Helper::decodeHtmlEntities( const QString& html ) { return Global::decodeHtmlEntities( html ); } QString Helper::encodeHtmlEntities( const QString &html ) { return Global::encodeHtmlEntities( html ); } QString Helper::decodeHtml( const QByteArray &document, const QByteArray &fallbackCharset ) { return Global::decodeHtml( document, fallbackCharset ); } QString Helper::decode( const QByteArray &document, const QByteArray &charset ) { return Global::decode( document, charset ); } Helper::Helper( const QString &serviceProviderId, QObject *parent ) : QObject(parent), m_mutex(new QMutex()), m_serviceProviderId(serviceProviderId), m_errorMessageRepetition(0) { } Helper::~Helper() { emitRepeatedMessageWarning(); delete m_mutex; } void Helper::emitRepeatedMessageWarning() { QMutexLocker locker( m_mutex ); if ( m_errorMessageRepetition > 0 ) { kDebug() << "(Last error message repeated" << m_errorMessageRepetition << "times)"; m_errorMessageRepetition = 0; emit messageReceived( i18nc("@info/plain", "Last error message repeated %1 times", m_errorMessageRepetition), QScriptContextInfo() ); } } void Helper::messageReceived( const QString &message, const QString &failedParseText, ErrorSeverity severity ) { m_mutex->lock(); if ( message == m_lastErrorMessage ) { ++m_errorMessageRepetition; m_mutex->unlock(); return; } m_lastErrorMessage = message; QScriptContextInfo info( context()->parentContext() ); const QString serviceProviderId = m_serviceProviderId; m_mutex->unlock(); emitRepeatedMessageWarning(); emit messageReceived( message, info, failedParseText, severity ); // Output debug message and a maximum count of 200 characters of the text where the parsing failed QString shortParseText = failedParseText.trimmed().left(350); int diff = failedParseText.length() - shortParseText.length(); if ( diff > 0 ) { shortParseText.append(QString("... <%1 more chars>").arg(diff)); } shortParseText = shortParseText.replace('\n', "\n "); // Indent #ifdef ENABLE_DEBUG_SCRIPT_ERROR m_mutex->lock(); const QString name = severity == Information ? "Information" : (severity == Warning ? "Warning" : "Fatal error"); DEBUG_SCRIPT_ERROR( QString("%1 in %2-script, function %3(), file %4, line %5") .arg(name) .arg(m_serviceProviderId) .arg(info.functionName().isEmpty() ? "[anonymous]" : info.functionName()) .arg(QFileInfo(info.fileName()).fileName()) .arg(info.lineNumber()) ); m_mutex->unlock(); DEBUG_SCRIPT_ERROR( message ); if ( !shortParseText.isEmpty() ) { DEBUG_SCRIPT_ERROR( QString("The text of the document where parsing failed is: \"%1\"") .arg(shortParseText) ); } #endif // Log the complete message to the log file. - QString logFileName = KGlobal::dirs()->saveLocation( "data", "plasma_engine_publictransport" ); + QString logFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "plasma_engine_publictransport" ); logFileName.append( "serviceproviders.log" ); if ( !logFileName.isEmpty() ) { QFile logFile(logFileName); if ( logFile.size() > 1024 * 512 ) { // == 0.5 MB if ( !logFile.remove() ) { kDebug() << "Error: Couldn't delete old log file."; } else { kDebug() << "Deleted old log file, because it was getting too big."; } } if ( !logFile.open(QIODevice::Append | QIODevice::Text) ) { kDebug() << "Couldn't open the log file in append mode" << logFileName << logFile.errorString(); return; } logFile.write( QString("%1 (%2, in function %3(), file %4, line %5):\n \"%6\"\n Failed while reading this text: \"%7\"\n-------------------------------------\n\n") .arg(serviceProviderId) .arg(QDateTime::currentDateTime().toString()) .arg(info.functionName().isEmpty() ? "[anonymous]" : info.functionName()) .arg(QFileInfo(info.fileName()).fileName()) .arg(info.lineNumber()) .arg(message) .arg(failedParseText.trimmed()).toUtf8() ); logFile.close(); } } void ResultObject::addData( const QVariantMap &item ) { m_mutex->lockInline(); TimetableData data; for ( QVariantMap::ConstIterator it = item.constBegin(); it != item.constEnd(); ++it ) { bool ok; Enums::TimetableInformation info = static_cast< Enums::TimetableInformation >( it.key().toInt(&ok) ); if ( !ok || info == Enums::Nothing ) { info = Global::timetableInformationFromString( it.key() ); } const QVariant value = it.value(); if ( info == Enums::Nothing ) { qWarning() << "Invalid property name" << it.key() << "with value" << value; QString message; if ( it.key() == QLatin1String("length") && value.canConvert(QVariant::Int) && item.count() == value.toInt() + 1 ) // +1 for the "length" property { // Seems like a list was given and autoconverted to QVariantMap in this way message = i18nc("@info/plain", "Invalid property name \"%1\" " "with value \"%2\", seems like a list was passed to " "result.addData() instead of an object with properties.", it.key(), value.toString()); context()->throwError( message ); } else { message = i18nc("@info/plain", "Invalid property name \"%1\" with value \"%2\"", it.key(), value.toString()); } const int count = m_timetableData.count(); m_mutex->unlockInline(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); m_mutex->lockInline(); continue; } else if ( value.isNull() ) { // Null value received, simply leave the data empty continue; } else if ( !value.isValid() ) { qWarning() << "Value for" << info << "is invalid or null" << value; const QString message = i18nc("@info/plain", "Invalid value received for \"%1\"", it.key()); const int count = m_timetableData.count(); m_mutex->unlockInline(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); m_mutex->lockInline(); continue; } else if ( info == Enums::TypeOfVehicle && static_cast(value.toInt()) == Enums::InvalidVehicleType && Global::vehicleTypeFromString(value.toString()) == Enums::InvalidVehicleType ) { qWarning() << "Invalid type of vehicle value" << value; const QString message = i18nc("@info/plain", "Invalid type of vehicle received: \"%1\"", value.toString()); const int count = m_timetableData.count(); m_mutex->unlockInline(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); m_mutex->lockInline(); } else if ( info == Enums::TypesOfVehicleInJourney || info == Enums::RouteTypesOfVehicles ) { const QVariantList types = value.toList(); foreach ( const QVariant &type, types ) { if ( static_cast(type.toInt()) == Enums::InvalidVehicleType && Global::vehicleTypeFromString(type.toString()) == Enums::InvalidVehicleType ) { kDebug() << "Invalid type of vehicle value in" << Global::timetableInformationToString(info) << value; const QString message = i18nc("@info/plain", "Invalid type of vehicle received in \"%1\": \"%2\"", Global::timetableInformationToString(info), type.toString()); const int count = m_timetableData.count(); m_mutex->unlockInline(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); m_mutex->lockInline(); } } } else if ( info == Enums::JourneyNewsUrl ) { QString url = value.toString(); if ( url.startsWith('/') ) { // Prepend provider URL to relative URLs qWarning() << "Prepending provider URL to relative JourneyNewsUrls is not implemented"; // url.prepend( m_ ); TODO } } else if ( info == Enums::JourneyNewsOther ) { // DEPRECATED qWarning() << "JourneyNewsOther is deprecated, use JourneyNews instead"; info = Enums::JourneyNews; } if ( m_features.testFlag(AutoDecodeHtmlEntities) ) { if ( value.canConvert(QVariant::String) && (info == Enums::StopName || info == Enums::Target || info == Enums::StartStopName || info == Enums::TargetStopName || info == Enums::Operator || info == Enums::TransportLine || info == Enums::Platform || info == Enums::DelayReason || info == Enums::Status || info == Enums::Pricing) ) { // Decode HTML entities in string values data[ info ] = Global::decodeHtmlEntities( value.toString() ).trimmed(); } else if ( value.canConvert(QVariant::StringList) && (info == Enums::RouteStops || info == Enums::RoutePlatformsDeparture || info == Enums::RoutePlatformsArrival) ) { // Decode HTML entities in string list values QStringList stops = value.toStringList(); for ( QStringList::Iterator it = stops.begin(); it != stops.end(); ++it ) { *it = Helper::trim( Global::decodeHtmlEntities(*it) ); } data[ info ] = stops; } else { // Other values don't need decoding data[ info ] = value; } } else { data[ info ] = value; } } m_timetableData << data; if ( m_features.testFlag(AutoPublish) && m_timetableData.count() == 10 ) { // Publish the first 10 data items automatically m_mutex->unlockInline(); emit publish(); } else { m_mutex->unlockInline(); } } QString Helper::trim( const QString& str ) { return QString(str).trimmed() .replace( QRegExp("^( )+|( )+$", Qt::CaseInsensitive), QString() ) .trimmed(); } QString Helper::simplify( const QString &str ) { return QString(str).replace( QRegExp("( )+", Qt::CaseInsensitive), QString() ) .simplified(); } QString Helper::stripTags( const QString& str ) { const QString attributePattern = "\\w+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\"'>\\s]+))?"; QRegExp rx( QString("<\\/?\\w+(?:\\s+%1)*(?:\\s*/)?>").arg(attributePattern) ); rx.setMinimal( true ); return QString( str ).remove( rx ); } QString Helper::camelCase( const QString& str ) { QString ret = str.toLower(); QRegExp rx( "(^\\w)|\\W(\\w)" ); int pos = 0; while ( (pos = rx.indexIn(ret, pos)) != -1 ) { if ( rx.pos(2) == -1 || rx.pos(2) >= ret.length() ) { ret[ rx.pos(1) ] = ret[ rx.pos(1) ].toUpper(); } else { ret[ rx.pos(2) ] = ret[ rx.pos(2) ].toUpper(); } pos += rx.matchedLength(); } return ret; } QString Helper::extractBlock( const QString& str, const QString& beginString, const QString& endString ) { int pos = str.indexOf( beginString ); if ( pos == -1 ) { return ""; } int end = str.indexOf( endString, pos + 1 ); return str.mid( pos, end - pos ); } QVariantMap Helper::matchTime( const QString& str, const QString& format ) { QString pattern = QRegExp::escape( format ); pattern = pattern.replace( "hh", "\\d{2}" ) .replace( "h", "\\d{1,2}" ) .replace( "mm", "\\d{2}" ) .replace( "m", "\\d{1,2}" ) .replace( "AP", "(AM|PM)" ) .replace( "ap", "(am|pm)" ); QVariantMap ret; QRegExp rx( pattern ); if ( rx.indexIn(str) != -1 ) { QTime time = QTime::fromString( rx.cap(), format ); ret.insert( "hour", time.hour() ); ret.insert( "minute", time.minute() ); } else if ( format != "hh:mm" ) { // Try default format if the one specified doesn't work QRegExp rx2( "\\d{1,2}:\\d{2}" ); if ( rx2.indexIn(str) != -1 ) { QTime time = QTime::fromString( rx2.cap(), "hh:mm" ); ret.insert( "hour", time.hour() ); ret.insert( "minute", time.minute() ); } else { ret.insert( "error", true ); DEBUG_SCRIPT_HELPER("Couldn't match time in" << str << pattern); } } else { ret.insert( "error", true ); DEBUG_SCRIPT_HELPER("Couldn't match time in" << str << pattern); } return ret; } QDate Helper::matchDate( const QString& str, const QString& format ) { QString pattern = QRegExp::escape( format ).replace( "d", "D" ); pattern = pattern.replace( "DD", "\\d{2}" ) .replace( "D", "\\d{1,2}" ) .replace( "MM", "\\d{2}" ) .replace( "M", "\\d{1,2}" ) .replace( "yyyy", "\\d{4}" ) .replace( "yy", "\\d{2}" ); QRegExp rx( pattern ); QDate date; if ( rx.indexIn(str) != -1 ) { date = QDate::fromString( rx.cap(), format ); } else if ( format != "yyyy-MM-dd" ) { // Try default format if the one specified doesn't work QRegExp rx2( "\\d{2,4}-\\d{2}-\\d{2}" ); if ( rx2.indexIn(str) != -1 ) { date = QDate::fromString( rx2.cap(), "yyyy-MM-dd" ); } } if ( !date.isValid() ) { DEBUG_SCRIPT_HELPER("Couldn't match date in" << str << pattern); } // Adjust date, needed for formats with only two "yy" for year matching // A year 12 means 2012, not 1912 if ( date.year() < 1970 ) { date.setDate( date.year() + 100, date.month(), date.day() ); } return date; } QString Helper::formatTime( int hour, int minute, const QString& format ) { return QTime( hour, minute ).toString( format ); } QString Helper::formatDate( int year, int month, int day, const QString& format ) { return QDate( year, month, day ).toString( format ); } QString Helper::formatDateTime( const QDateTime& dateTime, const QString& format ) { return dateTime.toString( format ); } int Helper::duration( const QString& sTime1, const QString& sTime2, const QString& format ) { QTime time1 = QTime::fromString( sTime1, format ); QTime time2 = QTime::fromString( sTime2, format ); if ( !time1.isValid() || !time2.isValid() ) { return -1; } return time1.secsTo( time2 ) / 60; } QString Helper::addMinsToTime( const QString& sTime, int minsToAdd, const QString& format ) { QTime time = QTime::fromString( sTime, format ); if ( !time.isValid() ) { DEBUG_SCRIPT_HELPER("Couldn't parse the given time" << sTime << format); return ""; } return time.addSecs( minsToAdd * 60 ).toString( format ); } QString Helper::addDaysToDate( const QString& sDate, int daysToAdd, const QString& format ) { QDate date = QDate::fromString( sDate, format ).addDays( daysToAdd ); if ( !date.isValid() ) { DEBUG_SCRIPT_HELPER("Couldn't parse the given date" << sDate << format); return sDate; } return date.toString( format ); } QDateTime Helper::addDaysToDate( const QDateTime& dateTime, int daysToAdd ) { return dateTime.addDays( daysToAdd ); } QStringList Helper::splitSkipEmptyParts( const QString& str, const QString& sep ) { return str.split( sep, QString::SkipEmptyParts ); } QVariantMap Helper::findFirstHtmlTag( const QString &str, const QString &tagName, const QVariantMap &options ) { // Set/overwrite maxCount option to match only the first tag using findHtmlTags() QVariantMap _options = options; _options[ "maxCount" ] = 1; QVariantList tags = findHtmlTags( str, tagName, _options ); // Copy values of first matched tag (if any) to the result object QVariantMap result; result.insert( "found", !tags.isEmpty() ); if ( !tags.isEmpty() ) { const QVariantMap firstTag = tags.first().toMap(); result.insert( "contents", firstTag["contents"] ); result.insert( "position", firstTag["position"] ); result.insert( "endPosition", firstTag["endPosition"] ); result.insert( "attributes", firstTag["attributes"] ); result.insert( "name", firstTag["name"] ); } return result; } QVariantList Helper::findHtmlTags( const QString &str, const QString &tagName, const QVariantMap &options ) { const QVariantMap &attributes = options[ "attributes" ].toMap(); const int maxCount = options.value( "maxCount", 0 ).toInt(); const bool noContent = options.value( "noContent", false ).toBool(); const bool noNesting = options.value( "noNesting", false ).toBool(); const bool debug = options.value( "debug", false ).toBool(); const QString contentsRegExpPattern = options.value( "contentsRegExp", QString() ).toString(); const QVariantMap namePosition = options[ "namePosition" ].toMap(); int position = options.value( "position", 0 ).toInt(); const bool namePositionIsAttribute = namePosition["type"].toString().toLower().compare( QLatin1String("attribute"), Qt::CaseInsensitive ) == 0; const QString namePositionRegExpPattern = namePosition.contains("regexp") ? namePosition["regexp"].toString() : QString(); // Create regular expression that matches HTML elements with or without attributes. // Since QRegExp offers no way to retreive multiple matches of the same capture group // the whole attribute string gets matched here and then analyzed in another loop // using attributeRegExp. // Matching the attributes with all details here is required to prevent eg. having a match // end after a ">" character in a string in an attribute. const QString attributePattern = "\\w+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\"'>\\s]+))?"; QRegExp htmlTagRegExp( noContent ? QString("<%1((?:\\s+%2)*)(?:\\s*/)?>").arg(tagName).arg(attributePattern) : QString("<%1((?:\\s+%2)*)>").arg(tagName).arg(attributePattern), // TODO TEST does this need a "\\s*" before the ">"? // : QString("<%1((?:\\s+%2)*)>%3").arg(tagName).arg(attributePattern) // .arg(contentsRegExpPattern), Qt::CaseInsensitive ); QRegExp htmlCloseTagRegExp( QString("").arg(tagName), Qt::CaseInsensitive ); QRegExp contentsRegExp( contentsRegExpPattern, Qt::CaseInsensitive ); htmlTagRegExp.setMinimal( true ); // Match attributes with or without value, with single/double/not quoted value QRegExp attributeRegExp( "(\\w+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\"'>\\s]+)))?", Qt::CaseInsensitive ); QVariantList foundTags; while ( (foundTags.count() < maxCount || maxCount <= 0) && (position = htmlTagRegExp.indexIn(str, position)) != -1 ) { if ( debug ) { kDebug() << "Test match at" << position << htmlTagRegExp.cap().left(500); } const QString attributeString = htmlTagRegExp.cap( 1 ); QString tagContents; // = noContent ? QString() : htmlTagRegExp.cap( 2 ); QVariantMap foundAttributes; int attributePos = 0; while ( (attributePos = attributeRegExp.indexIn(attributeString, attributePos)) != -1 ) { const int valueCap = attributeRegExp.cap(2).isEmpty() ? (attributeRegExp.cap(3).isEmpty() ? 4 : 3) : 2; foundAttributes.insert( attributeRegExp.cap(1), attributeRegExp.cap(valueCap) ); attributePos += attributeRegExp.matchedLength(); } if ( debug ) { kDebug() << "Found attributes" << foundAttributes << "in" << attributeString; } // Test if the attributes match bool attributesMatch = true; for ( QVariantMap::ConstIterator it = attributes.constBegin(); it != attributes.constEnd(); ++it ) { if ( !foundAttributes.contains(it.key()) ) { // Did not find exact attribute name, try to use it as regular expression pattern attributesMatch = false; QRegExp attributeNameRegExp( it.key(), Qt::CaseInsensitive ); foreach ( const QString &attributeName, foundAttributes.keys() ) { if ( attributeNameRegExp.indexIn(attributeName) != -1 ) { // Matched the attribute name attributesMatch = true; break; } } if ( !attributesMatch ) { if ( debug ) { kDebug() << "Did not find attribute" << it.key(); } break; } } // Attribute exists, test it's value const QString value = foundAttributes[ it.key() ].toString(); const QString valueRegExpPattern = it.value().toString(); if ( !(value.isEmpty() && valueRegExpPattern.isEmpty()) ) { QRegExp valueRegExp( valueRegExpPattern, Qt::CaseInsensitive ); if ( valueRegExp.indexIn(value) == -1 ) { // Attribute value regexp did not matched attributesMatch = false; if ( debug ) { kDebug() << "Value" << value << "did not match pattern" << valueRegExpPattern; } break; } else if ( valueRegExp.captureCount() > 0 ) { // Attribute value regexp matched, store captures foundAttributes[ it.key() ] = valueRegExp.capturedTexts(); } } } // Search for new opening HTML tags (with same tag name) before the closing HTML tag int endPosition = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); if ( !attributesMatch ) { position = endPosition; continue; } if ( !noContent ) { if ( noNesting ) { // "noNesting" option set, simply search for next closing tag, no matter if it is // a nested tag or not const int posClosing = htmlCloseTagRegExp.indexIn( str, endPosition ); const int contentsBegin = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); tagContents = str.mid( contentsBegin, posClosing - contentsBegin ); endPosition = htmlCloseTagRegExp.pos() + htmlCloseTagRegExp.matchedLength(); } else { // Find next closing tag, skipping nested tags. // Get string after the opening HTML tag const QString rest = str.mid( htmlTagRegExp.pos() + htmlTagRegExp.matchedLength() ); int posClosing = htmlCloseTagRegExp.indexIn( rest ); if ( posClosing == -1 ) { position = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); if ( debug ) { kDebug() << "Closing tag" << tagName << "could not be found"; } position = endPosition; continue; } // Search for nested opening tags in between the main opening tag and the // next closing tag int posOpening = htmlTagRegExp.indexIn( rest.left(posClosing) ); while ( posOpening != -1 ) { // Found a nested tag, find the next closing tag posClosing = htmlCloseTagRegExp.indexIn( rest, posClosing + htmlCloseTagRegExp.matchedLength() ); if ( posClosing == -1 ) { position = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); if ( debug ) { kDebug() << "Closing tag" << tagName << "could not be found"; } break; } // Search for more nested opening tags posOpening = htmlTagRegExp.indexIn( rest.left(posClosing), posOpening + htmlTagRegExp.matchedLength() ); } tagContents = rest.left( posClosing ); endPosition += htmlCloseTagRegExp.pos() + htmlCloseTagRegExp.matchedLength(); } } // Match contents, only use regular expression if one was given in the options argument if ( !contentsRegExpPattern.isEmpty() ) { if ( contentsRegExp.indexIn(tagContents) == -1 ) { if ( debug ) { kDebug() << "Did not match tag contents" << tagContents.left(500); } position = endPosition; continue; } else { // Use first matched group as contents string, if any // Otherwise use the whole match as contents string tagContents = contentsRegExp.cap( contentsRegExp.captureCount() <= 1 ? 0 : 1 ); } } else { // No regexp pattern for contents, use complete contents, but trimmed tagContents = tagContents.trimmed(); } // Construct a result object QVariantMap result; result.insert( "contents", tagContents ); result.insert( "position", position ); result.insert( "endPosition", endPosition ); result.insert( "attributes", foundAttributes ); // Find name if a "namePosition" option is given if ( !namePosition.isEmpty() ) { const QString name = getTagName( result, namePosition["type"].toString(), namePositionRegExpPattern, namePositionIsAttribute ? namePosition["name"].toString() : QString() ); result.insert( "name", name ); } if ( debug ) { kDebug() << "Found HTML tag" << tagName << "at" << position << foundAttributes; } foundTags << result; position = endPosition; } if ( debug ) { if ( foundTags.isEmpty() ) { kDebug() << "Found no" << tagName << "HTML tags in HTML" << str; } else { kDebug() << "Found" << foundTags.count() << tagName << "HTML tags"; } } return foundTags; } QString Helper::getTagName( const QVariantMap &searchResult, const QString &type, const QString ®Exp, const QString attributeName ) { const bool namePositionIsAttribute = type.toLower().compare( QLatin1String("attribute"), Qt::CaseInsensitive ) == 0; QString name = trim( namePositionIsAttribute ? searchResult["attributes"].toMap()[ attributeName ].toString() : searchResult["contents"].toString() ); if ( !regExp.isEmpty() ) { // Use "regexp" property of namePosition to match the header name QRegExp namePositionRegExp( regExp, Qt::CaseInsensitive ); if ( namePositionRegExp.indexIn(name) != -1 ) { name = namePositionRegExp.cap( qMin(1, namePositionRegExp.captureCount()) ); } } return name; } QVariantMap Helper::findNamedHtmlTags( const QString &str, const QString &tagName, const QVariantMap &options ) { QVariantMap namePosition; if ( !options.contains("namePosition") ) { namePosition[ "type" ] = "contents"; // Can be "attribute", "contents" } else { namePosition = options[ "namePosition" ].toMap(); } const bool namePositionIsAttribute = namePosition["type"].toString().toLower().compare( QLatin1String("attribute"), Qt::CaseInsensitive ) == 0; const QString namePositionRegExpPattern = namePosition.contains("regexp") ? namePosition["regexp"].toString() : QString(); const QString ambiguousNameResolution = options.contains("ambiguousNameResolution") ? options["ambiguousNameResolution"].toString().toLower() : "replace"; const bool debug = options.value( "debug", false ).toBool(); const QVariantList foundTags = findHtmlTags( str, tagName, options ); QVariantMap foundTagsMap; foreach ( const QVariant &foundTag, foundTags ) { QString name = getTagName( foundTag.toMap(), namePosition["type"].toString(), namePositionRegExpPattern, namePositionIsAttribute ? namePosition["name"].toString() : QString() ); if ( name.isEmpty() ) { if ( debug ) { kDebug() << "Empty name in" << str; } continue; } // Check if the newly found name was already found // and decide what to do based on the "ambiguousNameResolution" option if ( ambiguousNameResolution == QLatin1String("addnumber") && foundTagsMap.contains(name) ) { QRegExp rx( "(\\d+)$" ); if ( rx.indexIn(name) != -1 ) { name += QString::number( rx.cap(1).toInt() + 1 ); } else { name += "2"; } } foundTagsMap[ name ] = foundTag; // TODO Use lists here? The same name could be found multiply times } // Store list of names in the "names" property, therefore "names" should not be a found tag name if ( !foundTagsMap.contains("names") ) { foundTagsMap[ "names" ] = QVariant::fromValue( foundTagsMap.keys() ); } else if ( debug ) { kDebug() << "A tag with the name 'names' was found. Normally a property 'names' gets " "added to the object returned by this functionm, which lists all found " "names in a list."; } return foundTagsMap; } const char* Storage::LIFETIME_ENTRYNAME_SUFFIX = "__expires__"; class StoragePrivate { public: StoragePrivate( const QString &serviceProvider ) : readWriteLock(new QReadWriteLock(QReadWriteLock::Recursive)), readWriteLockPersistent(new QReadWriteLock(QReadWriteLock::Recursive)), serviceProvider(serviceProvider), lastLifetimeCheck(0), config(0) { }; ~StoragePrivate() { delete readWriteLock; delete readWriteLockPersistent; delete config; }; void readPersistentData() { // Delete already read config object if ( config ) { delete config; } config = new KConfig( ServiceProviderGlobal::cacheFileName(), KConfig::SimpleConfig ); }; KConfigGroup persistentGroup() { if ( !config ) { readPersistentData(); } return config->group( serviceProvider ).group( QLatin1String("storage") ); }; quint16 checkLength( const QByteArray &data ) { if ( data.length() > 65535 ) { kDebug() << "Data is too long, only 65535 bytes are supported" << data.length(); } return static_cast( data.length() ); }; QReadWriteLock *readWriteLock; QReadWriteLock *readWriteLockPersistent; QVariantMap data; const QString serviceProvider; uint lastLifetimeCheck; // as time_t KConfig *config; KConfigGroup lastPersistentGroup; }; Storage::Storage( const QString &serviceProviderId, QObject *parent ) : QObject(parent), d(new StoragePrivate(serviceProviderId)) { // Delete persistently stored data which lifetime has expired checkLifetime(); } Storage::~Storage() { delete d; } void Storage::write( const QVariantMap& data ) { for ( QVariantMap::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { write( it.key(), it.value() ); } } void Storage::write( const QString& name, const QVariant& data ) { QWriteLocker locker( d->readWriteLock ); d->data.insert( name, data ); } QVariantMap Storage::read() { QReadLocker locker( d->readWriteLock ); return d->data; } QVariant Storage::read( const QString& name, const QVariant& defaultData ) { QReadLocker locker( d->readWriteLock ); return d->data.contains(name) ? d->data[name] : defaultData; } void Storage::remove( const QString& name ) { QWriteLocker locker( d->readWriteLock ); d->data.remove( name ); } void Storage::clear() { QWriteLocker locker( d->readWriteLock ); d->data.clear(); } int Storage::lifetime( const QString& name ) { QReadLocker locker( d->readWriteLockPersistent ); return lifetime( name, d->persistentGroup() ); } int Storage::lifetime( const QString& name, const KConfigGroup& group ) { QReadLocker locker( d->readWriteLockPersistent ); return lifetimeNoLock( name, group ); } int Storage::lifetimeNoLock( const QString &name, const KConfigGroup &group ) { const uint lifetimeTime_t = group.readEntry( name + LIFETIME_ENTRYNAME_SUFFIX, 0 ); return QDateTime::currentDateTime().daysTo( QDateTime::fromTime_t(lifetimeTime_t) ); } void Storage::checkLifetime() { QWriteLocker locker( d->readWriteLockPersistent ); if ( QDateTime::currentDateTime().toTime_t() - d->lastLifetimeCheck < MIN_LIFETIME_CHECK_INTERVAL * 60 ) { // Last lifetime check was less than 15 minutes ago return; } // Try to load script features from a cache file KConfigGroup group = d->persistentGroup(); QMap< QString, QString > data = group.entryMap(); for ( QMap< QString, QString >::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { if ( it.key().endsWith(LIFETIME_ENTRYNAME_SUFFIX) ) { // Do not check lifetime of entries which store the lifetime of the real data entries continue; } const int remainingLifetime = lifetimeNoLock( it.key(), group ); if ( remainingLifetime <= 0 ) { // Lifetime has expired kDebug() << "Lifetime of storage data" << it.key() << "for" << d->serviceProvider << "has expired" << remainingLifetime; removePersistent( it.key(), group ); } } d->lastLifetimeCheck = QDateTime::currentDateTime().toTime_t(); } bool Storage::hasData( const QString &name ) const { QReadLocker locker( d->readWriteLock ); return d->data.contains( name ); } bool Storage::hasPersistentData( const QString &name ) const { QReadLocker locker( d->readWriteLockPersistent ); KConfigGroup group = d->persistentGroup(); return group.hasKey( name ); } QByteArray Storage::encodeData( const QVariant &data ) const { // Store the type of the variant, because it is needed to read the QVariant with the correct // type using KConfigGroup::readEntry() const uchar type = static_cast( data.type() ); if ( type >= QVariant::LastCoreType ) { kDebug() << "Invalid data type, only QVariant core types are supported" << data.type(); return QByteArray(); } // Write type into the first byte QByteArray encodedData; encodedData[0] = type; // Write data if ( data.canConvert(QVariant::ByteArray) ) { encodedData += data.toByteArray(); } else if ( data.canConvert(QVariant::String) ) { encodedData += data.toString().toUtf8(); } else { switch ( data.type() ) { case QVariant::StringList: case QVariant::List: { // Lists are stored like this (one entry after the other): // "<2 Bytes: Value length>" const QVariantList list = data.toList(); foreach ( const QVariant &item, list ) { // Encode current list item QByteArray encodedItem = encodeData( item ); // Construct a QByteArray which contains the length in two bytes (0 .. 65535) const quint16 length = d->checkLength( encodedItem ); const QByteArray baLength( (const char*)&length, sizeof(length) ); // Use 2 bytes for the length of the data, append data encodedData += baLength; encodedData += encodedItem; } break; } case QVariant::Map: { // Maps are stored like this (one entry after the other): // "<2 Bytes: Key length><2 Bytes: Value length>" const QVariantMap map = data.toMap(); for ( QVariantMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it ) { // Encode current key and value QByteArray encodedKey = it.key().toUtf8(); QByteArray encodedValue = encodeData( it.value() ); // Construct QByteArrays which contain the lengths in two bytes each (0 .. 65535) const quint16 lengthKey = d->checkLength( encodedKey ); const quint16 lengthValue = d->checkLength( encodedValue ); const QByteArray baLengthKey( (const char*)&lengthKey, sizeof(lengthKey) ); const QByteArray baLengthValue( (const char*)&lengthValue, sizeof(lengthValue) ); // Use 2 bytes for the length of the key, append key encodedData += baLengthKey; encodedData += encodedKey; // Use 2 bytes for the length of the value, append value encodedData += baLengthValue; encodedData += encodedValue; } break; } default: kDebug() << "Cannot convert from type" << data.type(); return QByteArray(); } } return encodedData; } QVariant Storage::decodeData( const QByteArray &data ) const { const QVariant::Type type = static_cast( data[0] ); if ( type >= QVariant::LastCoreType ) { kDebug() << "Invalid encoding for data" << data; return QVariant(); } const QByteArray encodedValue = data.mid( 1 ); QVariant value( type ); value.setValue( encodedValue ); if ( value.canConvert( type ) ) { value.convert( type ); return value; } else { switch ( type ) { case QVariant::Date: // QVariant::toString() uses Qt::ISODate to convert QDate to string return QDate::fromString( value.toString(), Qt::ISODate ); case QVariant::Time: return QTime::fromString( value.toString() ); case QVariant::DateTime: // QVariant::toString() uses Qt::ISODate to convert QDateTime to string return QDateTime::fromString( value.toString(), Qt::ISODate ); case QVariant::StringList: case QVariant::List: { // Lists are stored like this (one entry after the other): // "<2 Bytes: Value length>" QVariantList decoded; QByteArray encoded = value.toByteArray(); int pos = 0; while ( pos + 2 < encoded.length() ) { const quint16 length = *reinterpret_cast( encoded.mid(pos, 2).data() ); if ( pos + 2 + length > encoded.length() ) { kDebug() << "Invalid list data" << encoded; return QVariant(); } const QByteArray encodedValue = encoded.mid( pos + 2, length ); const QVariant decodedValue = decodeData( encodedValue ); decoded << decodedValue; pos += 2 + length; } return decoded; } case QVariant::Map: { // Maps are stored like this (one entry after the other): // "<2 Bytes: Key length><2 Bytes: Value length>" QVariantMap decoded; const QByteArray encoded = value.toByteArray(); int pos = 0; while ( pos + 4 < encoded.length() ) { // Decode two bytes to quint16, this is the key length const quint16 keyLength = *reinterpret_cast( encoded.mid(pos, 2).data() ); if ( pos + 4 + keyLength > encoded.length() ) { // + 4 => 2 Bytes for keyLength + 2 Bytes for valueLength kDebug() << "Invalid map data" << encoded; return QVariant(); } // Extract key, starting after the two bytes for the key length const QString key = encoded.mid( pos + 2, keyLength ); pos += 2 + keyLength; // Decode two bytes to quint16, this is the value length const quint16 valueLength = *reinterpret_cast( encoded.mid(pos, 2).data() ); if ( pos + 2 + valueLength > encoded.length() ) { kDebug() << "Invalid map data" << encoded; return QVariant(); } // Extract and decode value, starting after the two bytes for the value length const QByteArray encodedValue = encoded.mid( pos + 2, valueLength ); const QVariant decodedValue = decodeData( encodedValue ); pos += 2 + valueLength; // Insert decoded value into the result map decoded.insert( key, decodedValue ); } return decoded; } default: kDebug() << "Cannot convert to type" << type; return QVariant(); } } } void Storage::writePersistent( const QVariantMap& data, uint lifetime ) { QWriteLocker locker( d->readWriteLockPersistent ); for ( QVariantMap::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { writePersistent( it.key(), it.value(), lifetime ); } } void Storage::writePersistent( const QString& name, const QVariant& data, uint lifetime ) { if ( lifetime > MAX_LIFETIME ) { lifetime = MAX_LIFETIME; } // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); KConfigGroup group = d->persistentGroup(); group.writeEntry( name + LIFETIME_ENTRYNAME_SUFFIX, QDateTime::currentDateTime().addDays(lifetime).toTime_t() ); group.writeEntry( name, encodeData(data) ); } QVariant Storage::readPersistent( const QString& name, const QVariant& defaultData ) { // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); const QByteArray data = d->persistentGroup().readEntry( name, QByteArray() ); return data.isEmpty() ? defaultData : decodeData(data); } void Storage::removePersistent( const QString& name, KConfigGroup& group ) { QWriteLocker locker( d->readWriteLockPersistent ); group.deleteEntry( name + LIFETIME_ENTRYNAME_SUFFIX ); group.deleteEntry( name ); } void Storage::removePersistent( const QString& name ) { // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); KConfigGroup group = d->persistentGroup(); removePersistent( name, group ); } void Storage::clearPersistent() { // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); d->persistentGroup().deleteGroup(); } QString Network::lastUrl() const { QMutexLocker locker( m_mutex ); return m_lastUrl; } QString Network::lastUserUrl() const { QMutexLocker locker( m_mutex ); return m_lastUserUrl; } void Network::clear() { QMutexLocker locker( m_mutex ); m_lastUrl.clear(); m_lastUserUrl.clear(); } bool Network::lastDownloadAborted() const { QMutexLocker locker( m_mutex ); return m_lastDownloadAborted; } bool Network::hasRunningRequests() const { QMutexLocker locker( m_mutex ); if ( m_synchronousRequestCount > 0 ) { // There is a synchronous request running return true; } foreach ( const NetworkRequest::Ptr &request, m_requests ) { if ( request->isRunning() ) { // Found a running asynchronous request return true; } } // No running request found return false; } QList< NetworkRequest::Ptr > Network::runningRequests() const { QMutexLocker locker( m_mutex ); QList< NetworkRequest::Ptr > requests; foreach ( const NetworkRequest::Ptr &request, m_requests ) { if ( request->isRunning() ) { requests << request; } } return requests; } int Network::runningRequestCount() const { QMutexLocker locker( m_mutex ); return runningRequests().count() + m_synchronousRequestCount; } QByteArray Network::fallbackCharset() const { QMutexLocker locker( m_mutex ); return m_fallbackCharset; } QList< TimetableData > ResultObject::data() const { QMutexLocker locker( m_mutex ); return m_timetableData; } QVariant ResultObject::data( int index, Enums::TimetableInformation information ) const { QMutexLocker locker( m_mutex ); if ( index < 0 || index >= m_timetableData.count() ) { context()->throwError( QScriptContext::RangeError, "Index out of range" ); return QVariant(); } return m_timetableData[ index ][ information ]; } bool ResultObject::hasData() const { QMutexLocker locker( m_mutex ); return !m_timetableData.isEmpty(); } int ResultObject::count() const { QMutexLocker locker( m_mutex ); return m_timetableData.count(); } ResultObject::Features ResultObject::features() const { QMutexLocker locker( m_mutex ); return m_features; } ResultObject::Hints ResultObject::hints() const { QMutexLocker locker( m_mutex ); return m_hints; } void ResultObject::clear() { QMutexLocker locker( m_mutex ); m_timetableData.clear(); } QScriptValue constructStream( QScriptContext *context, QScriptEngine *engine ) { QScriptValue argument = context->argument(0); DataStreamPrototype *object; if ( argument.instanceOf(context->callee()) ) { object = new DataStreamPrototype( qscriptvalue_cast(argument) ); } else if ( argument.isQObject() && qscriptvalue_cast(argument) ) { object = new DataStreamPrototype( qscriptvalue_cast(argument) ); } else if ( argument.isVariant() ) { const QVariant variant = argument.toVariant(); object = new DataStreamPrototype( variant.toByteArray() ); } else { return engine->undefinedValue(); } return engine->newQObject( object, QScriptEngine::ScriptOwnership ); } QScriptValue dataStreamToScript( QScriptEngine *engine, const DataStreamPrototypePtr &stream ) { return engine->newQObject( const_cast(stream), QScriptEngine::QtOwnership, QScriptEngine::PreferExistingWrapperObject ); } void dataStreamFromScript( const QScriptValue &object, DataStreamPrototypePtr &stream ) { stream = qobject_cast< DataStreamPrototype* >( object.toQObject() ); } DataStreamPrototype::DataStreamPrototype( QObject *parent ) : QObject( parent ) { } DataStreamPrototype::DataStreamPrototype( const QByteArray &byteArray, QObject *parent ) : QObject( parent ) { QBuffer *buffer = new QBuffer( const_cast(&byteArray), this ); buffer->open( QIODevice::ReadOnly ); m_dataStream = QSharedPointer< QDataStream >( new QDataStream(buffer) ); } DataStreamPrototype::DataStreamPrototype( QIODevice *device, QObject *parent ) : QObject( parent ) { if ( !device->isOpen() ) { qWarning() << "Device not opened"; } m_dataStream = QSharedPointer< QDataStream >( new QDataStream(device) ); } DataStreamPrototype::DataStreamPrototype( DataStreamPrototype *other ) : QObject(), m_dataStream(other->stream()) { } QDataStream *DataStreamPrototype::thisDataStream() const { return m_dataStream.data(); } qint8 DataStreamPrototype::readInt8() { qint8 i; thisDataStream()->operator >>( i ); return i; } quint8 DataStreamPrototype::readUInt8() { quint8 i; thisDataStream()->operator >>( i ); return i; } quint16 DataStreamPrototype::readUInt16() { quint16 i; thisDataStream()->operator >>( i ); return i; } qint32 DataStreamPrototype::readInt32() { qint32 i; thisDataStream()->operator >>( i ); return i; } qint16 DataStreamPrototype::readInt16() { qint16 i; thisDataStream()->operator >>( i ); return i; } quint32 DataStreamPrototype::readUInt32() { quint32 i; thisDataStream()->operator >>( i ); return i; } QString DataStreamPrototype::readString() { QString string; char character; while ( thisDataStream()->device()->getChar(&character) && character != 0 ) { string.append( character ); } return string; } QByteArray DataStreamPrototype::readBytes( uint byteCount ) { char *chars = new char[ byteCount ]; const uint bytesRead = thisDataStream()->readRawData( chars, byteCount ); if ( bytesRead != byteCount ) { qWarning() << "Did not read all requested bytes, read" << bytesRead << "of" << byteCount; } QByteArray bytes( chars ); delete[] chars; return bytes; } QByteArray DataStreamPrototype::readBytesUntilZero() { QByteArray bytes; char character; while ( thisDataStream()->device()->getChar(&character) && character != 0 ) { bytes.append( character ); } return bytes; } QString NetworkRequest::url() const { QMutexLocker locker( m_mutex ); return m_url; } QString NetworkRequest::userUrl() const { QMutexLocker locker( m_mutex ); return m_userUrl; } bool NetworkRequest::isRunning() const { QMutexLocker locker( m_mutex ); return m_reply; } bool NetworkRequest::isFinished() const { QMutexLocker locker( m_mutex ); return m_isFinished; } bool NetworkRequest::isRedirected() const { QMutexLocker locker( m_mutex ); return m_redirectUrl.isValid(); } QUrl NetworkRequest::redirectedUrl() const { QMutexLocker locker( m_mutex ); return m_redirectUrl; } QString NetworkRequest::postData() const { QMutexLocker locker( m_mutex ); return m_postData; } quint64 NetworkRequest::uncompressedSize() const { QMutexLocker locker( m_mutex ); return m_uncompressedSize; } void NetworkRequest::setUserData( const QVariant &userData ) { QMutexLocker locker( m_mutex ); m_userData = userData; } QVariant NetworkRequest::userData() const { QMutexLocker locker( m_mutex ); return m_userData; } #include "scriptapi.moc" } // namespace ScriptApi diff --git a/engine/script/serviceproviderscript.cpp b/engine/script/serviceproviderscript.cpp index bdeb3d4..358a4fb 100644 --- a/engine/script/serviceproviderscript.cpp +++ b/engine/script/serviceproviderscript.cpp @@ -1,689 +1,689 @@ /* * 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 "serviceproviderscript.h" // Own includes #include "scriptapi.h" #include "script_thread.h" #include "serviceproviderglobal.h" #include "serviceproviderdata.h" #include "serviceprovidertestdata.h" #include "departureinfo.h" #include "request.h" // KDE includes #include #include #include -#include + #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include const char *ServiceProviderScript::SCRIPT_FUNCTION_FEATURES = "features"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE = "getTimetable"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS = "getJourneys"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS = "getStopSuggestions"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA = "getAdditionalData"; ServiceProviderScript::ServiceProviderScript( const ServiceProviderData *data, QObject *parent, const QSharedPointer &cache ) : ServiceProvider(data, parent), m_scriptStorage(QSharedPointer(new Storage(data->id()))) { m_scriptState = WaitingForScriptUsage; m_scriptFeatures = readScriptFeatures( cache.isNull() ? ServiceProviderGlobal::cache() : cache ); qRegisterMetaType< QList >( "QList" ); qRegisterMetaType< TimetableData >( "TimetableData" ); qRegisterMetaType< QList >( "QList" ); qRegisterMetaType< GlobalTimetableInfo >( "GlobalTimetableInfo" ); qRegisterMetaType< ParseDocumentMode >( "ParseDocumentMode" ); qRegisterMetaType< NetworkRequest* >( "NetworkRequest*" ); qRegisterMetaType< NetworkRequest::Ptr >( "NetworkRequest::Ptr" ); qRegisterMetaType< QIODevice* >( "QIODevice*" ); qRegisterMetaType< DataStreamPrototype* >( "DataStreamPrototype*" ); qRegisterMetaType< ResultObject::Features >( "ResultObject::Features" ); // TEST, now done using Q_DECLARE_METATYPE? qRegisterMetaType< ResultObject::Hints >( "ResultObject::Hints" ); } ServiceProviderScript::~ServiceProviderScript() { abortAllRequests(); } void ServiceProviderScript::abortAllRequests() { // Abort all running jobs and wait for them to finish for proper cleanup foreach ( ScriptJob *job, m_runningJobs ) { kDebug() << "Abort job" << job; // Create an event loop to wait for the job to finish, // quit the loop when the done() signal gets emitted by the job QEventLoop loop; connect( job, SIGNAL(done(ThreadWeaver::Job*)), &loop, SLOT(quit()) ); // Disconnect all slots connected to the job and then abort it disconnect( job, 0, this, 0 ); job->requestAbort(); // Wait for the job to get aborted if ( !job->isFinished() ) { // The job is not finished, wait for it, maximally one second QTimer::singleShot( 1000, &loop, SLOT(quit()) ); loop.exec(); } // The job has finished or the timeout was reached if ( !job->isFinished() ) { // The job is still not finished, the timeout was reached before qWarning() << "Job not aborted before timeout, delete it" << job; } job->deleteLater(); } m_runningJobs.clear(); } QStringList ServiceProviderScript::allowedExtensions() { return QStringList() << "kross" << "qt" << "qt.core" << "qt.xml"; } bool ServiceProviderScript::lazyLoadScript() { // Read script QFile scriptFile( m_data->scriptFileName() ); if ( !scriptFile.open(QIODevice::ReadOnly) ) { kDebug() << "Script could not be opened for reading" << m_data->scriptFileName() << scriptFile.errorString(); return false; } const QByteArray ba = scriptFile.readAll(); scriptFile.close(); const QString scriptContents = QString::fromUtf8( ba ); // Initialize the script m_scriptData = ScriptData( m_data, QSharedPointer< QScriptProgram >( new QScriptProgram(scriptContents, m_data->scriptFileName())) ); return true; } bool ServiceProviderScript::runTests( QString *errorMessage ) const { if ( !QFile::exists(m_data->scriptFileName()) ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Script file not found: %1", m_data->scriptFileName()); } return false; } QFile scriptFile( m_data->scriptFileName() ); if ( !scriptFile.open(QIODevice::ReadOnly) ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Could not open script file: %1", m_data->scriptFileName()); } return false; } QTextStream stream( &scriptFile ); const QString program = stream.readAll(); scriptFile.close(); if ( program.isEmpty() ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Script file is empty: %1", m_data->scriptFileName()); } return false; } const QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax( program ); if ( syntax.state() != QScriptSyntaxCheckResult::Valid ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Syntax error in script file, line %1: %2", syntax.errorLineNumber(), syntax.errorMessage().isEmpty() ? i18nc("@info/plain", "Syntax error") : syntax.errorMessage()); } return false; } // No errors found return true; } bool ServiceProviderScript::isTestResultUnchanged( const QString &providerId, const QSharedPointer< KConfig > &cache ) { const KConfigGroup providerGroup = cache->group( providerId ); if ( !providerGroup.hasGroup("script") ) { // Not a scripted provider or modified time not stored yet return true; } // Check if included files have been marked as modified since the cache was last updated const KConfigGroup providerScriptGroup = providerGroup.group( "script" ); const bool includesUpToDate = providerScriptGroup.readEntry( "includesUpToDate", false ); if ( !includesUpToDate ) { // An included file was modified return false; } // Check if the script file was modified since the cache was last updated const QDateTime scriptModifiedTime = providerScriptGroup.readEntry("modifiedTime", QDateTime()); const QString scriptFilePath = providerScriptGroup.readEntry( "scriptFileName", QString() ); const QFileInfo scriptFileInfo( scriptFilePath ); if ( scriptFileInfo.lastModified() != scriptModifiedTime ) { kDebug() << "Script was modified:" << scriptFileInfo.fileName(); return false; } // Check all included files and update "includesUpToDate" fields in using providers if ( checkIncludedFiles(cache, providerId) ) { // An included file of the provider was modified return false; } return true; } bool ServiceProviderScript::isTestResultUnchanged( const QSharedPointer &cache ) const { return isTestResultUnchanged( id(), cache ); } QList ServiceProviderScript::readScriptFeatures( const QSharedPointer &cache ) { // Check if the script file was modified since the cache was last updated KConfigGroup providerGroup = cache->group( m_data->id() ); if ( providerGroup.hasGroup("script") && isTestResultUnchanged(cache) && providerGroup.hasKey("features") ) { // Return feature list stored in the cache bool ok; QStringList featureStrings = providerGroup.readEntry("features", QStringList()); featureStrings.removeOne("(none)"); QList features = ServiceProviderGlobal::featuresFromFeatureStrings( featureStrings, &ok ); if ( ok ) { // The stored feature list only contains valid strings return features; } else { qWarning() << "Invalid feature string stored for provider" << m_data->id(); } } // No actual cached information about the service provider kDebug() << "No up-to-date cache information for service provider" << m_data->id(); QList features; QStringList includedFiles; bool ok = lazyLoadScript(); QString errorMessage; if ( !ok ) { errorMessage = i18nc("@info/plain", "Cannot open script file %1", m_data->scriptFileName()); } else { // Create script engine QScriptEngine engine; foreach ( const QString &import, m_data->scriptExtensions() ) { if ( !importExtension(&engine, import) ) { ok = false; errorMessage = i18nc("@info/plain", "Cannot import script extension %1", import); break; } } if ( ok ) { ScriptObjects objects; objects.createObjects( m_scriptData ); objects.attachToEngine( &engine, m_scriptData ); engine.evaluate( *m_scriptData.program ); QVariantList result; if ( !engine.hasUncaughtException() ) { result = engine.globalObject().property( SCRIPT_FUNCTION_FEATURES ).call().toVariant().toList(); } if ( engine.hasUncaughtException() ) { kDebug() << "Error in the script" << engine.uncaughtExceptionLineNumber() << engine.uncaughtException().toString(); kDebug() << "Backtrace:" << engine.uncaughtExceptionBacktrace().join("\n"); ok = false; errorMessage = i18nc("@info/plain", "Uncaught exception in script " "%1, line %2: %3", QFileInfo(QScriptContextInfo(engine.currentContext()).fileName()).fileName(), engine.uncaughtExceptionLineNumber(), engine.uncaughtException().toString()); } else { includedFiles = engine.globalObject().property( "includedFiles" ) .toVariant().toStringList(); // Test if specific functions exist in the script if ( engine.globalObject().property(SCRIPT_FUNCTION_GETSTOPSUGGESTIONS).isValid() ) { features << Enums::ProvidesStopSuggestions; } if ( engine.globalObject().property(SCRIPT_FUNCTION_GETJOURNEYS).isValid() ) { features << Enums::ProvidesJourneys; } if ( engine.globalObject().property(SCRIPT_FUNCTION_GETADDITIONALDATA).isValid() ) { features << Enums::ProvidesAdditionalData; } // Test if features() script function is available if ( !engine.globalObject().property(SCRIPT_FUNCTION_FEATURES).isValid() ) { kDebug() << "The script has no" << SCRIPT_FUNCTION_FEATURES << "function"; } else { // Use values returned by features() script functions // to get additional features of the service provider foreach ( const QVariant &value, result ) { features << static_cast( value.toInt() ); } } } } } // Update script modified time in cache KConfigGroup scriptGroup = providerGroup.group( "script" ); scriptGroup.writeEntry( "scriptFileName", m_data->scriptFileName() ); scriptGroup.writeEntry( "modifiedTime", QFileInfo(m_data->scriptFileName()).lastModified() ); // Remove provider from cached data for include file(s) no longer used by the provider KConfigGroup globalScriptGroup = cache->group( "script" ); const QStringList globalScriptGroupNames = globalScriptGroup.groupList(); foreach ( const QString &globalScriptGroupName, globalScriptGroupNames ) { if ( !globalScriptGroupName.startsWith(QLatin1String("include_")) ) { continue; } QString includedFile = globalScriptGroupName; includedFile.remove( 0, 8 ); // Remove "include_" from beginning const QFileInfo fileInfo( includedFile ); KConfigGroup includeFileGroup = globalScriptGroup.group( "include_" + fileInfo.filePath() ); QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); if ( usingProviders.contains(m_data->id()) && !includedFiles.contains(includedFile) ) { // This provider is marked as using the include file, but it no longer uses that file usingProviders.removeOne( m_data->id() ); includeFileGroup.writeEntry( "usingProviders", usingProviders ); } } // Check if included file(s) were modified foreach ( const QString &includedFile, includedFiles ) { // Add this provider to the list of providers using the current include file const QFileInfo fileInfo( includedFile ); KConfigGroup includeFileGroup = globalScriptGroup.group( "include_" + fileInfo.filePath() ); QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); if ( !usingProviders.contains(m_data->id()) ) { usingProviders << m_data->id(); includeFileGroup.writeEntry( "usingProviders", usingProviders ); } // Check if the include file was modified checkIncludedFile( cache, fileInfo ); } // Update modified times of included files scriptGroup.writeEntry( "includesUpToDate", true ); // Was just updated // Set error in default cache group if ( !ok ) { ServiceProviderTestData newTestData = ServiceProviderTestData::read( id(), cache ); newTestData.setSubTypeTestStatus( ServiceProviderTestData::Failed, errorMessage ); newTestData.write( id(), cache ); } return features; } bool ServiceProviderScript::checkIncludedFile( const QSharedPointer &cache, const QFileInfo &fileInfo, const QString &providerId ) { // Use a config group in the global script group for each included file. // It stores the last modified time and a list of IDs of providers using the include file KConfigGroup globalScriptGroup = cache->group( "script" ); KConfigGroup includeFileGroup = globalScriptGroup.group( "include_" + fileInfo.filePath() ); const QDateTime lastModified = includeFileGroup.readEntry( "modifiedTime", QDateTime() ); // Update "includesUpToDate" field of using providers, if the include file was modified if ( lastModified != fileInfo.lastModified() ) { // The include file was modified, update all using providers (later). // isTestResultUnchanged() returns false if "includesUpToDate" is false const QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); foreach ( const QString &usingProvider, usingProviders ) { if ( cache->hasGroup(usingProvider) ) { KConfigGroup usingProviderScriptGroup = cache->group( usingProvider ).group( "script" ); usingProviderScriptGroup.writeEntry( "includesUpToDate", false ); } } includeFileGroup.writeEntry( "modifiedTime", fileInfo.lastModified() ); return providerId.isEmpty() || usingProviders.contains(providerId); } else { return false; } } bool ServiceProviderScript::checkIncludedFiles( const QSharedPointer &cache, const QString &providerId ) { bool modified = false; KConfigGroup globalScriptGroup = cache->group( "script" ); const QStringList globalScriptGroups = globalScriptGroup.groupList(); foreach ( const QString &globalScriptGroup, globalScriptGroups ) { if ( !globalScriptGroup.startsWith(QLatin1String("include_")) ) { continue; } QString includedFile = globalScriptGroup; includedFile.remove( 0, 8 ); // Remove "include_" from beginning const QFileInfo fileInfo( includedFile ); if ( checkIncludedFile(cache, fileInfo, providerId) ) { // The include file was modified and is used by this provider modified = true; } } return modified; } QList ServiceProviderScript::features() const { return m_scriptFeatures; } void ServiceProviderScript::departuresReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool couldNeedForcedUpdate ) { // TODO use hints if ( data.isEmpty() ) { kDebug() << "The script didn't find any departures" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18n("Error while parsing the departure document."), url, &request ); } else { // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results = (m_publishedData[request.sourceName()] << newResults); DepartureInfoList departures; foreach( const PublicTransportInfoPtr &info, results ) { departures << info.dynamicCast(); } emit departuresReceived( this, url, departures, globalInfo, request ); if ( couldNeedForcedUpdate ) { emit forceUpdate(); } } } void ServiceProviderScript::arrivalsReady( const QList< TimetableData > &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool couldNeedForcedUpdate ) { // TODO use hints if ( data.isEmpty() ) { kDebug() << "The script didn't find any arrivals" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18n("Error while parsing the arrival document."), url, &request ); } else { // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results = (m_publishedData[request.sourceName()] << newResults); ArrivalInfoList arrivals; foreach( const PublicTransportInfoPtr &info, results ) { arrivals << info.dynamicCast(); } emit arrivalsReceived( this, url, arrivals, globalInfo, request ); if ( couldNeedForcedUpdate ) { emit forceUpdate(); } } } void ServiceProviderScript::journeysReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool couldNeedForcedUpdate ) { Q_UNUSED( couldNeedForcedUpdate ); // TODO use hints if ( data.isEmpty() ) { kDebug() << "The script didn't find any journeys" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18n("Error while parsing the journey document."), url, &request ); } else { // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results = (m_publishedData[request.sourceName()] << newResults); JourneyInfoList journeys; foreach( const PublicTransportInfoPtr &info, results ) { journeys << info.dynamicCast(); } emit journeysReceived( this, url, journeys, globalInfo, request ); } } void ServiceProviderScript::stopSuggestionsReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const StopSuggestionRequest &request, bool couldNeedForcedUpdate ) { Q_UNUSED( couldNeedForcedUpdate ); // TODO use hints kDebug() << "Received" << data.count() << "items"; // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results( m_publishedData[request.sourceName()] << newResults ); kDebug() << "Results:" << results; StopInfoList stops; foreach( const PublicTransportInfoPtr &info, results ) { stops << info.dynamicCast(); } emit stopsReceived( this, url, stops, request ); } void ServiceProviderScript::additionalDataReady( const TimetableData &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const AdditionalDataRequest &request, bool couldNeedForcedUpdate ) { Q_UNUSED( features ); Q_UNUSED( hints ); Q_UNUSED( globalInfo ); Q_UNUSED( couldNeedForcedUpdate ); if ( data.isEmpty() ) { kDebug() << "The script didn't find any new data" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18nc("@info/plain", "No additional data found."), url, &request ); } else { emit additionalDataReceived( this, url, data, request ); } } void ServiceProviderScript::jobStarted( ThreadWeaver::Job* job ) { ScriptJob *scriptJob = qobject_cast< ScriptJob* >( job ); Q_ASSERT( scriptJob ); // Warn if there is published data for the request, // but not for additional data requests because they point to existing departure data sources. // There may be multiple AdditionalDataJob requests for the same data source but different // timetable items in the source. const QString sourceName = scriptJob->sourceName(); if ( !qobject_cast(scriptJob) && m_publishedData.contains(sourceName) && !m_publishedData[sourceName].isEmpty() ) { qWarning() << "Data source already exists for job" << scriptJob << sourceName; } } void ServiceProviderScript::jobDone( ThreadWeaver::Job* job ) { ScriptJob *scriptJob = qobject_cast< ScriptJob* >( job ); Q_ASSERT( scriptJob ); m_publishedData.remove( scriptJob->sourceName() ); m_runningJobs.removeOne( scriptJob ); scriptJob->deleteLater(); } void ServiceProviderScript::jobFailed( ThreadWeaver::Job* job ) { ScriptJob *scriptJob = qobject_cast< ScriptJob* >( job ); Q_ASSERT( scriptJob ); emit requestFailed( this, ErrorParsingFailed, scriptJob->errorString(), scriptJob->lastDownloadUrl(), scriptJob->cloneRequest() ); } void ServiceProviderScript::requestDepartures( const DepartureRequest &request ) { if ( lazyLoadScript() ) { DepartureJob *job = new DepartureJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)), this, SLOT(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestArrivals( const ArrivalRequest &request ) { if ( lazyLoadScript() ) { ArrivalJob *job = new ArrivalJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)), this, SLOT(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestJourneys( const JourneyRequest &request ) { if ( lazyLoadScript() ) { JourneyJob *job = new JourneyJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)), this, SLOT(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestStopSuggestions( const StopSuggestionRequest &request ) { if ( lazyLoadScript() ) { StopSuggestionsJob *job = new StopSuggestionsJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)), this, SLOT(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ) { if ( lazyLoadScript() ) { StopsByGeoPositionJob *job = new StopsByGeoPositionJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)), this, SLOT(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestAdditionalData( const AdditionalDataRequest &request ) { if ( lazyLoadScript() ) { AdditionalDataJob *job = new AdditionalDataJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(additionalDataReady(TimetableData,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,AdditionalDataRequest,bool)), this, SLOT(additionalDataReady(TimetableData,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,AdditionalDataRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestMoreItems( const MoreItemsRequest &moreItemsRequest ) { if ( lazyLoadScript() ) { // Create a MoreItemsJob and connect ready signals for more departures/arrivals/journeys MoreItemsJob *job = new MoreItemsJob( m_scriptData, m_scriptStorage, moreItemsRequest, this ); connect( job, SIGNAL(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)), this, SLOT(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)) ); connect( job, SIGNAL(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)), this, SLOT(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)) ); connect( job, SIGNAL(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)), this, SLOT(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::enqueue( ScriptJob *job ) { m_runningJobs << job; connect( job, SIGNAL(started(ThreadWeaver::Job*)), this, SLOT(jobStarted(ThreadWeaver::Job*)) ); connect( job, SIGNAL(done(ThreadWeaver::Job*)), this, SLOT(jobDone(ThreadWeaver::Job*)) ); connect( job, SIGNAL(failed(ThreadWeaver::Job*)), this, SLOT(jobFailed(ThreadWeaver::Job*)) ); ThreadWeaver::Weaver::instance()->enqueue( job ); } void ServiceProviderScript::import( const QString &import, QScriptEngine *engine ) { engine->importExtension( import ); } int ServiceProviderScript::minFetchWait( UpdateFlags updateFlags ) const { // If an update was requested manually wait minimally one minute, // otherwise wait minimally 15 minutes between automatic updates return qMax( updateFlags.testFlag(UpdateWasRequestedManually) ? 60 : 15 * 60, ServiceProvider::minFetchWait() ); } diff --git a/engine/serviceprovider.cpp b/engine/serviceprovider.cpp index 54197ad..805a120 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 #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 { 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 { 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/serviceproviderdatareader.cpp b/engine/serviceproviderdatareader.cpp index 3802acf..f328826 100644 --- a/engine/serviceproviderdatareader.cpp +++ b/engine/serviceproviderdatareader.cpp @@ -1,571 +1,571 @@ /* * 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 "serviceproviderdatareader.h" // Own includes #include "config.h" #include "serviceprovider.h" #include "serviceproviderdata.h" #include "serviceproviderglobal.h" #include "script/serviceproviderscript.h" #include "global.h" // KDE includes #include #include #include #include -#include + // Qt includes #include #include ServiceProviderData *ServiceProviderDataReader::read( const QString &providerId, QString *errorMessage, QString *comments ) { QString filePath; QString country = "international"; QString _serviceProviderId = providerId; if ( _serviceProviderId.isEmpty() ) { // No service provider ID given, use the default one for the users country country = KGlobal::locale()->country(); // Try to find the XML filename of the default accessor for [country] filePath = ServiceProviderGlobal::defaultProviderForLocation( country ); if ( filePath.isEmpty() ) { return 0; } // Extract service provider ID from filename _serviceProviderId = ServiceProviderGlobal::idFromFileName( filePath ); kDebug() << "No service provider ID given, using the default one for country" << country << "which is" << _serviceProviderId; } else { foreach ( const QString &extension, ServiceProviderGlobal::fileExtensions() ) { - filePath = KGlobal::dirs()->findResource( "data", + filePath = KStandardDirs::locate( "data", ServiceProviderGlobal::installationSubDirectory() + _serviceProviderId + '.' + extension ); if ( !filePath.isEmpty() ) { break; } } if ( filePath.isEmpty() ) { kDebug() << "Could not find a service provider plugin XML named" << _serviceProviderId; if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Could not find a service provider " "plugin with the ID %1", _serviceProviderId); } return 0; } // Get country code from filename QRegExp rx( "^([^_]+)" ); if ( rx.indexIn(_serviceProviderId) != -1 && KGlobal::locale()->allCountriesList().contains(rx.cap()) ) { country = rx.cap(); } } QFile file( filePath ); ServiceProviderDataReader reader; ServiceProviderData *data = reader.read( &file, providerId, filePath, country, ServiceProviderDataReader::OnlyReadCorrectFiles, 0, comments ); if ( !data && errorMessage ) { *errorMessage = i18nc("@info/plain", "Error in line %1: %2", reader.lineNumber(), reader.errorString()); } return data; } ServiceProviderData *ServiceProviderDataReader::read( QIODevice *device, const QString &fileName, ErrorAcceptance errorAcceptance, QObject *parent, QString *comments, QString *errorMessage ) { const QString serviceProvider = ServiceProviderGlobal::idFromFileName( fileName ); // Get country code from filename QString country; QRegExp rx( "^([^_]+)" ); if ( rx.indexIn(serviceProvider) != -1 && KGlobal::locale()->allCountriesList().contains(rx.cap()) ) { country = rx.cap(); } else { country = "international"; } return read( device, serviceProvider, fileName, country, errorAcceptance, parent, comments, errorMessage ); } bool ServiceProviderDataReader::handleError( const QString &errorMessage, ErrorAcceptance errorAcceptance, QString *errorMessageOutput ) { if ( errorMessageOutput ) { *errorMessageOutput = errorMessage; } if ( errorAcceptance == OnlyReadCorrectFiles ) { raiseError( errorMessage ); return false; } else { return true; } } ServiceProviderData* ServiceProviderDataReader::read( QIODevice* device, const QString &serviceProvider, const QString &fileName, const QString &country, ErrorAcceptance errorAcceptance, QObject *parent, QString *comments, QString *errorMessage ) { Q_ASSERT( device ); bool closeAfterRead; // Only close after reading if it wasn't open before if ( (closeAfterRead = !device->isOpen()) && !device->open(QIODevice::ReadOnly) ) { if ( !handleError("Couldn't read the file \"" + fileName + "\".", errorAcceptance, errorMessage) ) { return 0; } } setDevice( device ); ServiceProviderData *data = 0; while ( !atEnd() ) { readNext(); if ( isComment() ) { if ( comments ) { addComments( comments, text() ); } } else if ( isStartElement() ) { if ( name().compare("serviceProvider", Qt::CaseInsensitive) != 0 ) { if ( !handleError(QString("Wrong root element for %1, should be , " "is <%2>.").arg(serviceProvider, name().toString()), errorAcceptance, errorMessage) ) { return 0; } } else if ( attributes().value("fileVersion") != QLatin1String("1.1") ) { const QString message = QString("Service provider plugin format version '%1' " "specified by %2 is not supported. Currently only 1.1 is supported. " "Please make sure the plugin complies with that version and update the " "'fileVersion' attribute of the root tag.") .arg(attributes().value("fileVersion").toString(), serviceProvider); if ( !handleError(message, errorAcceptance, errorMessage) ) { return 0; } } data = readProviderData( serviceProvider, fileName, country, errorAcceptance, parent, comments, errorMessage ); break; } } if ( comments ) { while ( !atEnd() ) { readNext(); if ( isComment() ) { addComments( comments, text() ); } } } if ( closeAfterRead ) { device->close(); } if ( error() != NoError ) { kDebug() << "Error reading provider" << serviceProvider << errorString(); } return error() == NoError && data ? data : 0; } void ServiceProviderDataReader::readUnknownElement( QString *comments ) { Q_ASSERT( isStartElement() ); if ( comments ) { addComments( comments, readStartElementString(), false ); } while ( !atEnd() ) { TokenType type = readNext(); switch ( type ) { case EndElement: if ( comments ) { addComments( comments, QString("").arg(name().toString()), false ); } return; case Comment: case Characters: case EntityReference: if ( comments ) { addComments( comments, text(), false ); } break; case StartElement: readUnknownElement( comments ); break; default: break; } } } QString ServiceProviderDataReader::readStartElementString() const { QString elementString( '<' ); elementString.append( name() ); const QXmlStreamAttributes attr = attributes(); for ( QXmlStreamAttributes::ConstIterator it = attr.constBegin(); it != attr.constEnd(); ++it ) { elementString.append( QString(" %1=\"%2\"") .arg(it->name().toString(), it->value().toString()) ); } elementString.append( '>' ); return elementString; } void ServiceProviderDataReader::addComments( QString *comments, const QString &newComments, bool newLine ) { if ( newComments.isEmpty() ) { return; } if ( newLine && !comments->isEmpty() ) { comments->append( '\n' ); } comments->append( newComments ); } ServiceProviderData *ServiceProviderDataReader::readProviderData( const QString &serviceProviderId, const QString &fileName, const QString &country, ErrorAcceptance errorAcceptance, QObject *parent, QString *comments, QString *errorMessage ) { const QString lang = KGlobal::locale()->country(); QString langRead, url, shortUrl; QHash names, descriptions; Enums::ServiceProviderType serviceProviderType; QString serviceProviderTypeString; const QString fileVersion = attributes().value("fileVersion").toString(); if ( attributes().hasAttribute(QLatin1String("type")) ) { serviceProviderTypeString = attributes().value( QLatin1String("type") ).toString(); serviceProviderType = ServiceProviderGlobal::typeFromString( serviceProviderTypeString ); if ( serviceProviderType == Enums::InvalidProvider && errorAcceptance == OnlyReadCorrectFiles ) { if ( !handleError(QString("The service provider type %1 used for %2 is invalid. " "Currently there are two values allowed: Script or GTFS.") .arg(serviceProviderTypeString, serviceProviderId), errorAcceptance, errorMessage) ) { return 0; } return 0; } } else { // No provider type in the XML file, use a default one #ifdef BUILD_PROVIDER_TYPE_SCRIPT serviceProviderType = Enums::ScriptedProvider; serviceProviderTypeString = ServiceProviderGlobal::typeToString( serviceProviderType ); #else #ifdef BUILD_PROVIDER_TYPE_GTFS serviceProviderType = Enums::GtfsProvider; serviceProviderTypeString = ServiceProviderGlobal::typeToString( serviceProviderType ); #else kFatal() << "Internal error: No known provider type is supported, " "tried ScriptedProvider and GtfsProvider"; #endif #endif qWarning() << "No provider type in the provider plugin file, using default type" << ServiceProviderGlobal::typeName(serviceProviderType); } ServiceProviderData *serviceProviderData = new ServiceProviderData( serviceProviderType, serviceProviderId, parent ); serviceProviderData->setFileName( fileName ); serviceProviderData->setCountry( country ); serviceProviderData->setFileFormatVersion( fileVersion ); if ( attributes().hasAttribute(QLatin1String("version")) ) { serviceProviderData->setVersion( attributes().value(QLatin1String("version")).toString() ); } while ( !atEnd() ) { readNext(); if ( isEndElement() && name().compare(QLatin1String("serviceProvider"), Qt::CaseInsensitive) == 0 ) { break; } if ( isComment() ) { if ( comments ) { addComments( comments, text() ); } } else if ( isStartElement() ) { if ( name().compare(QLatin1String("name"), Qt::CaseInsensitive) == 0 ) { const QString nameRead = readLocalizedTextElement( &langRead ); names[ langRead ] = nameRead; } else if ( name().compare(QLatin1String("description"), Qt::CaseInsensitive) == 0 ) { const QString descriptionRead = readLocalizedTextElement( &langRead ); descriptions[ langRead ] = descriptionRead; } else if ( name().compare(QLatin1String("author"), Qt::CaseInsensitive) == 0 ) { QString authorName, shortName, authorEmail; readAuthor( &authorName, &shortName, &authorEmail ); serviceProviderData->setAuthor( authorName, shortName, authorEmail ); } else if ( name().compare(QLatin1String("cities"), Qt::CaseInsensitive) == 0 ) { QStringList cities; QHash cityNameReplacements; readCities( &cities, &cityNameReplacements ); serviceProviderData->setCities( cities ); serviceProviderData->setCityNameToValueReplacementHash( cityNameReplacements ); } else if ( name().compare(QLatin1String("useSeperateCityValue"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setUseSeparateCityValue( readBooleanElement() ); } else if ( name().compare(QLatin1String("onlyUseCitiesInList"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setOnlyUseCitiesInList( readBooleanElement() ); } else if ( name().compare(QLatin1String("defaultVehicleType"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setDefaultVehicleType( Global::vehicleTypeFromString(readElementText()) ); } else if ( name().compare(QLatin1String("url"), Qt::CaseInsensitive) == 0 ) { url = readElementText(); } else if ( name().compare(QLatin1String("shortUrl"), Qt::CaseInsensitive) == 0 ) { shortUrl = readElementText(); } else if ( name().compare(QLatin1String("minFetchWait"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setMinFetchWait( readElementText().toInt() ); } else if ( name().compare(QLatin1String("charsetForUrlEncoding"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setCharsetForUrlEncoding( readElementText().toAscii() ); } else if ( name().compare(QLatin1String("fallbackCharset"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setFallbackCharset( readElementText().toAscii() ); // TODO Implement as attributes in the url tags? } else if ( name().compare(QLatin1String("changelog"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setChangelog( readChangelog() ); } else if ( name().compare(QLatin1String("credit"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setCredit( readElementText() ); #ifdef BUILD_PROVIDER_TYPE_GTFS } else if ( name().compare("feedUrl", Qt::CaseInsensitive) == 0 ) { serviceProviderData->setFeedUrl( readElementText() ); } else if ( name().compare("realtimeTripUpdateUrl", Qt::CaseInsensitive) == 0 ) { serviceProviderData->setRealtimeTripUpdateUrl( readElementText() ); } else if ( name().compare("realtimeAlertsUrl", Qt::CaseInsensitive) == 0 ) { serviceProviderData->setRealtimeAlertsUrl( readElementText() ); } else if ( name().compare("timeZone", Qt::CaseInsensitive) == 0 ) { serviceProviderData->setTimeZone( readElementText() ); #endif #ifdef BUILD_PROVIDER_TYPE_SCRIPT } else if ( serviceProviderType == Enums::ScriptedProvider && name().compare(QLatin1String("script"), Qt::CaseInsensitive) == 0 ) { const QStringList extensions = attributes().value( QLatin1String("extensions") ) .toString().split( ',', QString::SkipEmptyParts ); const QString scriptFile = QFileInfo( fileName ).path() + '/' + readElementText(); if ( !QFile::exists(scriptFile) ) { if ( !handleError(QString("The script file %1 referenced by the service " "provider plugin %2 was not found") .arg(scriptFile, serviceProviderId), errorAcceptance, errorMessage) ) { delete serviceProviderData; return 0; } } serviceProviderData->setScriptFile( scriptFile, extensions ); #endif } else if ( name().compare(QLatin1String("samples"), Qt::CaseInsensitive) == 0 ) { QStringList stops; QString city; qreal longitude, latitude; readSamples( &stops, &city, &longitude, &latitude ); serviceProviderData->setSampleStops( stops ); serviceProviderData->setSampleCity( city ); serviceProviderData->setSampleCoordinates( longitude, latitude ); } else if ( name().compare(QLatin1String("notes"), Qt::CaseInsensitive) == 0 ) { serviceProviderData->setNotes( readElementText() ); } else { readUnknownElement( comments ); } } } if ( url.isEmpty() ) { qWarning() << "No tag in service provider plugin XML"; } serviceProviderData->setNames( names ); serviceProviderData->setDescriptions( descriptions ); serviceProviderData->setUrl( url, shortUrl ); serviceProviderData->finish(); return serviceProviderData; } QString ServiceProviderDataReader::readLocalizedTextElement( QString *lang ) { if ( attributes().hasAttribute(QLatin1String("lang")) ) { *lang = attributes().value(QLatin1String("lang")).toString(); } else { *lang = "en"; } return readElementText(); } bool ServiceProviderDataReader::readBooleanElement() { const QString content = readElementText().trimmed(); if ( content.compare( "true", Qt::CaseInsensitive ) == 0 || content == QLatin1String("1") ) { return true; } else { return false; } } void ServiceProviderDataReader::readAuthor( QString *fullname, QString *shortName, QString *email, QString *comments ) { while ( !atEnd() ) { readNext(); if ( isEndElement() && name().compare( "author", Qt::CaseInsensitive ) == 0 ) { break; } if ( isComment() ) { if ( comments ) { addComments( comments, text() ); } } else if ( isStartElement() ) { if ( name().compare(QLatin1String("fullName"), Qt::CaseInsensitive) == 0 ) { *fullname = readElementText().trimmed(); } else if ( name().compare(QLatin1String("short"), Qt::CaseInsensitive) == 0 ) { *shortName = readElementText().trimmed(); } else if ( name().compare(QLatin1String("email"), Qt::CaseInsensitive) == 0 ) { *email = readElementText().trimmed(); } else { readUnknownElement(); } } } } void ServiceProviderDataReader::readCities( QStringList *cities, QHash< QString, QString > *cityNameReplacements, QString *comments ) { while ( !atEnd() ) { readNext(); if ( isEndElement() && name().compare(QLatin1String("cities"), Qt::CaseInsensitive) == 0 ) { break; } if ( isComment() ) { if ( comments ) { addComments( comments, text() ); } } else if ( isStartElement() ) { if ( name().compare(QLatin1String("city"), Qt::CaseInsensitive ) == 0 ) { if ( attributes().hasAttribute(QLatin1String("replaceWith")) ) { QString replacement = attributes().value(QLatin1String("replaceWith")).toString().toLower(); QString city = readElementText(); cityNameReplacements->insert( city.toLower(), replacement ); cities->append( city ); } else { QString city = readElementText(); cities->append( city ); } } else { readUnknownElement(); } } } } void ServiceProviderDataReader::readSamples( QStringList *stops, QString *city, qreal *longitude, qreal *latitude, QString *comments ) { while ( !atEnd() ) { readNext(); if ( isEndElement() && name().compare(QLatin1String("samples"), Qt::CaseInsensitive) == 0 ) { break; } if ( isComment() ) { if ( comments ) { addComments( comments, text() ); } } else if ( isStartElement() ) { if ( name().compare(QLatin1String("stop"), Qt::CaseInsensitive ) == 0 ) { stops->append( readElementText() ); } else if ( name().compare(QLatin1String("city"), Qt::CaseInsensitive ) == 0 ) { *city = readElementText(); } else if ( name().compare(QLatin1String("longitude"), Qt::CaseInsensitive ) == 0 ) { *longitude = readElementText().toDouble(); } else if ( name().compare(QLatin1String("latitude"), Qt::CaseInsensitive ) == 0 ) { *latitude = readElementText().toDouble(); } else { readUnknownElement(); } } } } QList ServiceProviderDataReader::readChangelog( QString *comments ) { QList changelog; while ( !atEnd() ) { readNext(); if ( isEndElement() && name().compare("changelog", Qt::CaseInsensitive) == 0 ) { break; } if ( isComment() ) { if ( comments ) { addComments( comments, text() ); } } else if ( isStartElement() ) { if ( name().compare("entry", Qt::CaseInsensitive) == 0 ) { ChangelogEntry currentEntry; if ( attributes().hasAttribute(QLatin1String("version")) ) { currentEntry.version = attributes().value( QLatin1String("version") ).toString(); } else if ( attributes().hasAttribute(QLatin1String("since")) ) { // DEPRECATED currentEntry.version = attributes().value( QLatin1String("since") ).toString(); } if( attributes().hasAttribute(QLatin1String("releasedWith")) ) { currentEntry.engineVersion = attributes().value( QLatin1String("releasedWith") ).toString(); } else if ( attributes().hasAttribute(QLatin1String("engineVersion")) ) { // DEPRECATED currentEntry.engineVersion = attributes().value( QLatin1String("engineVersion") ).toString(); } if ( attributes().hasAttribute(QLatin1String("author")) ) { currentEntry.author = attributes().value( QLatin1String("author") ).toString(); } currentEntry.description = readElementText(); changelog.append( currentEntry ); } else { readUnknownElement(); } } } return changelog; } diff --git a/engine/serviceproviderglobal.cpp b/engine/serviceproviderglobal.cpp index e788dad..3aef95d 100644 --- a/engine/serviceproviderglobal.cpp +++ b/engine/serviceproviderglobal.cpp @@ -1,384 +1,385 @@ /* * 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 "serviceproviderglobal.h" // Own includes #include "config.h" // KDE includes #include #include #include #include #include #include // Qt includes #include #include +#include QList< Enums::ServiceProviderType > ServiceProviderGlobal::availableProviderTypes() { QList< Enums::ServiceProviderType > types; #ifdef BUILD_PROVIDER_TYPE_SCRIPT types << Enums::ScriptedProvider; #endif #ifdef BUILD_PROVIDER_TYPE_GTFS types << Enums::GtfsProvider; #endif return types; } bool ServiceProviderGlobal::isProviderTypeAvailable( Enums::ServiceProviderType type ) { switch ( type ) { case Enums::ScriptedProvider: #ifdef BUILD_PROVIDER_TYPE_SCRIPT return true; #else return false; #endif case Enums::GtfsProvider: #ifdef BUILD_PROVIDER_TYPE_GTFS return true; #else return false; #endif case Enums::InvalidProvider: default: return false; } } QString ServiceProviderGlobal::defaultProviderForLocation( const QString &location ) { QStringList locationProviders; const QString subDirectory = installationSubDirectory(); foreach ( const QString &pattern, filePatterns() ) { locationProviders << KGlobal::dirs()->findAllResources( "data", subDirectory + location + '_' + pattern ); } if ( locationProviders.isEmpty() ) { qWarning() << "Couldn't find any providers for location" << location; return QString(); } // Simply return first found provider as default provider return locationProviders.first(); } QString ServiceProviderGlobal::cacheFileName() { - return KGlobal::dirs()->saveLocation("data", "plasma_engine_publictransport/") + return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "plasma_engine_publictransport/") .append( QLatin1String("datacache") ); } QSharedPointer< KConfig > ServiceProviderGlobal::cache() { return QSharedPointer< KConfig >( new KConfig(cacheFileName(), KConfig::SimpleConfig) ); } void ServiceProviderGlobal::cleanupCache( const QSharedPointer< KConfig > &_cache ) { QSharedPointer< KConfig > cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; const QStringList installedProviderPaths = ServiceProviderGlobal::installedProviders(); QStringList installedProviderIDs; foreach ( const QString &installedProviderPath, installedProviderPaths ) { installedProviderIDs << idFromFileName( installedProviderPath ); } foreach ( const QString &group, cache->groupList() ) { if ( group != QLatin1String("script") && group != QLatin1String("gtfs") && !installedProviderIDs.contains(group) ) { // Found a group for a provider that is no longer installed kDebug() << "Cleanup cache data for no longer installed provider" << group; clearCache( group, cache, false ); } } cache->sync(); } void ServiceProviderGlobal::clearCache( const QString &providerId, const QSharedPointer< KConfig > &_cache, bool syncCache ) { QSharedPointer< KConfig > cache = _cache.isNull() ? ServiceProviderGlobal::cache() : _cache; if ( !cache->hasGroup(providerId) ) { // No data cached for the provider return; } // Remove all data for the provider from the cache cache->deleteGroup( providerId ); // Remove provider from "usingProviders" lists for included script files KConfigGroup globalScriptGroup = cache->group( "script" ); const QStringList globalScriptGroupNames = globalScriptGroup.groupList(); foreach ( const QString &globalScriptGroupName, globalScriptGroupNames ) { if ( !globalScriptGroupName.startsWith(QLatin1String("include_")) ) { continue; } // Check if the provider to remove from the cache is listed as using the current include KConfigGroup includeFileGroup = globalScriptGroup.group( globalScriptGroupName ); QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); if ( usingProviders.contains(providerId) ) { // Remove provider from the list of providers using the current include script file usingProviders.removeOne( providerId ); includeFileGroup.writeEntry( "usingProviders", usingProviders ); } } if ( syncCache ) { cache->sync(); } } QString ServiceProviderGlobal::idFromFileName( const QString &serviceProviderFileName ) { // Get the service provider substring from the XML filename, ie. "/path/to/xml/.pts/xml" return QFileInfo( serviceProviderFileName ).baseName(); } QString ServiceProviderGlobal::fileNameFromId( const QString &serviceProviderId ) { const QString subDirectory = installationSubDirectory(); foreach ( const QString &extension, fileExtensions() ) { - const QString fileName = KGlobal::dirs()->findResource( + const QString fileName = KStandardDirs::locate( "data", subDirectory + serviceProviderId + '.' + extension ); if ( !fileName.isEmpty() ) { return fileName; } } // File not found kDebug() << "No service provider plugin found with this ID:" << serviceProviderId; return QString(); } Enums::ServiceProviderType ServiceProviderGlobal::typeFromString( const QString &serviceProviderType ) { QString s = serviceProviderType.toLower(); if ( s == QLatin1String("script") || s == QLatin1String("html") ) // DEPRECATED { return Enums::ScriptedProvider; } else if ( s == QLatin1String("gtfs") ) { return Enums::GtfsProvider; } else { return Enums::InvalidProvider; } } QString ServiceProviderGlobal::typeToString( Enums::ServiceProviderType type ) { switch ( type ) { case Enums::ScriptedProvider: return "script"; case Enums::GtfsProvider: return "gtfs"; case Enums::InvalidProvider: default: return "invalid"; } } QString ServiceProviderGlobal::typeName( Enums::ServiceProviderType type, ProviderTypeNameOptions options ) { QString name; switch ( type ) { case Enums::ScriptedProvider: name = i18nc("@info/plain Name of a service provider plugin type", "Scripted"); break; case Enums::GtfsProvider: name = i18nc("@info/plain Name of a service provider plugin type", "GTFS"); break; case Enums::InvalidProvider: default: qWarning() << "Invalid provider type" << type; return i18nc("@info/plain Name of the invalid service provider plugin type", "Invalid"); } // Append "(unsupported)" if the engine gets build without support for the provider type if ( options == AppendHintForUnsupportedProviderTypes && !isProviderTypeAvailable(type) ) { name += ' ' + i18nc("@info/plain Gets appended to service provider plugin type names, " "if the engine gets build without support for that type", "(unsupported)"); } return name; } QStringList ServiceProviderGlobal::filePatterns() { const KMimeType::Ptr mimeType = KMimeType::mimeType("application/x-publictransport-serviceprovider"); if ( mimeType.isNull() ) { qWarning() << "The application/x-publictransport-serviceprovider mime type was not found!"; qWarning() << "No provider plugins will get loaded."; kDebug() << "Solution: Make sure 'serviceproviderplugin.xml' is installed correctly " "and run kbuildsycoca4."; return QStringList(); } else { return mimeType->patterns(); } } QStringList ServiceProviderGlobal::fileExtensions() { QStringList extensions = filePatterns(); for ( QStringList::Iterator it = extensions.begin(); it != extensions.end(); ++it ) { const int pos = it->lastIndexOf( '.' ); if ( pos == -1 || pos == it->length() - 1 ) { qWarning() << "Could not extract file extension from mime type pattern!\n" "Check the \"application/x-publictransport-serviceprovider\" mime type."; continue; } // Cut away everything but the file name extension *it = it->mid( pos + 1 ); } return extensions; } QStringList ServiceProviderGlobal::installedProviders() { QStringList providers; const QString subDirectory = installationSubDirectory(); foreach ( const QString &pattern, filePatterns() ) { - providers << KGlobal::dirs()->findAllResources( "data", subDirectory + pattern ); + providers << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, subDirectory + pattern ); } return providers; } bool ServiceProviderGlobal::isProviderInstalled( const QString &providerId ) { const QString subDirectory = installationSubDirectory(); foreach ( const QString &extension, fileExtensions() ) { - const QString providerFile = KGlobal::dirs()->findResource( + const QString providerFile = KStandardDirs::locate( "data", subDirectory + providerId + '.' + extension ); if ( !providerFile.isEmpty() ) { // Found the provider plugin source file in an installation directory return true; } } // Provider is not installed return false; } bool ServiceProviderGlobal::isSourceFileModified( const QString &providerId, const QSharedPointer &cache ) { // Check if the script file was modified since the cache was last updated const KConfigGroup group = cache->group( providerId ); const QDateTime modifiedTime = group.readEntry( "modifiedTime", QDateTime() ); const QString fileName = fileNameFromId( providerId ); return fileName.isEmpty() ? true : QFileInfo(fileName).lastModified() != modifiedTime; } QString ServiceProviderGlobal::featureName( Enums::ProviderFeature feature ) { switch ( feature ) { case Enums::ProvidesDepartures: return i18nc("@info/plain A short string indicating support for departure from a stop", "Departures"); case Enums::ProvidesArrivals: return i18nc("@info/plain A short string indicating support for arrivals to a stop.", "Arrivals"); case Enums::ProvidesJourneys: return i18nc("@info/plain A short string indicating support for journeys", "Journey search"); case Enums::ProvidesAdditionalData: return i18nc("@info/plain A short string indicating support for additional data", "Get additional data later"); case Enums::ProvidesDelays: return i18nc("@info/plain A short string indicating that delay information can be provided", "Delays"); case Enums::ProvidesNews: return i18nc("@info/plain A short string indicating that news about timetable items can be provided", "News"); case Enums::ProvidesPlatform: return i18nc("@info/plain A short string indicating that platform information can be provided", "Platform"); case Enums::ProvidesStopSuggestions: return i18nc("@info/plain A short string indicating support for stop suggestions", "Stop suggestions"); case Enums::ProvidesStopsByGeoPosition: return i18nc("@info/plain A short string indicating support for querying stops by geo position", "Stops by geolocation"); case Enums::ProvidesStopID: return i18nc("@info/plain A short string indicating that stop IDs can be provided", "Stop ID"); case Enums::ProvidesStopGeoPosition: return i18nc("@info/plain A short string indicating that stop geographical positions can be provided", "Stop geolocation"); case Enums::ProvidesPricing: return i18nc("@info/plain A short string indicating that pricing information can be provided", "Pricing"); case Enums::ProvidesRouteInformation: return i18nc("@info/plain A short string indicating that route information can be provided", "Route information"); case Enums::ProvidesMoreJourneys: return i18nc("@info/plain A short string indicating that earlier later journeys can be " "provided for existing journey data sources", "Get earlier/later journeys"); default: qWarning() << "Unexpected feature value" << feature; return QString(); } } QStringList ServiceProviderGlobal::featureNames( const QList &features ) { QStringList names; foreach ( Enums::ProviderFeature feature, features ) { names << featureName( feature ); } return names; } QStringList ServiceProviderGlobal::featureStrings( const QList &features ) { QStringList names; foreach ( Enums::ProviderFeature feature, features ) { names << Enums::toString( feature ); } return names; } QList< Enums::ProviderFeature > ServiceProviderGlobal::featuresFromFeatureStrings( const QStringList &featureNames, bool *ok ) { QList< Enums::ProviderFeature > features; if ( ok ) *ok = true; foreach ( const QString &featureName, featureNames ) { Enums::ProviderFeature feature = Enums::stringToFeature( featureName.toAscii().data() ); if ( feature == Enums::InvalidProviderFeature ) { if ( ok ) *ok = false; } else { features << feature; } } return features; } diff --git a/engine/timetablemate/src/docks/documentationdockwidget.cpp b/engine/timetablemate/src/docks/documentationdockwidget.cpp index aa01fd3..1c592b6 100644 --- a/engine/timetablemate/src/docks/documentationdockwidget.cpp +++ b/engine/timetablemate/src/docks/documentationdockwidget.cpp @@ -1,148 +1,148 @@ /* * 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 "documentationdockwidget.h" // KDE includes #include #include #include #include #include #include #include #include -#include + #include // Qt includes #include #include #include #include DocumentationDockWidget::DocumentationDockWidget( KActionMenu *showDocksAction, QWidget *parent ) : AbstractDockWidget( i18nc("@title:window Dock title", "Documentation"), showDocksAction, parent ) { setObjectName( "documentation" ); setWhatsThis( i18nc("@info:whatsthis", "Documentation" "Provides documentation for Public Transport engine scripts.") ); QWidget *container = new QWidget( this ); container->setMinimumSize( 150, 150 ); m_documentationChooser = new KComboBox( container ); KIcon classIcon( "code-class" ); m_documentationChooser->addItem( KIcon("go-home"), "Documentation Home", "index" ); m_documentationChooser->addItem( KIcon("code-variable"), "Enumerations", "enums" ); m_documentationChooser->addItem( classIcon, "Helper Object", "helper" ); m_documentationChooser->addItem( classIcon, "Result Object", "resultobject" ); m_documentationChooser->addItem( classIcon, "Network Object", "network" ); m_documentationChooser->addItem( classIcon, "NetworkRequest Objects", "networkrequest" ); m_documentationChooser->addItem( classIcon, "Storage Object", "storage" ); m_documentationChooser->addItem( classIcon, "DataStream Object", "datastreamprototype" ); m_documentationWidget = new KWebView( container ); m_documentationWidget->pageAction( QWebPage::OpenLinkInNewWindow )->setVisible( false ); m_documentationWidget->pageAction( QWebPage::OpenFrameInNewWindow )->setVisible( false ); m_documentationWidget->pageAction( QWebPage::OpenImageInNewWindow )->setVisible( false ); KToolBar *toolBar = new KToolBar( "DocumentationToolBar", container ); toolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); toolBar->addAction( m_documentationWidget->pageAction(QWebPage::Back) ); toolBar->addAction( m_documentationWidget->pageAction(QWebPage::Forward) ); toolBar->addWidget( m_documentationChooser ); connect( m_documentationWidget, SIGNAL(urlChanged(QUrl)), this, SLOT(documentationUrlChanged(QUrl)) ); QVBoxLayout *dockLayout = new QVBoxLayout( container ); dockLayout->setSpacing( 0 ); dockLayout->setContentsMargins( 0, 0, 0, 0 ); dockLayout->addWidget( toolBar ); dockLayout->addWidget( m_documentationWidget ); setWidget( container ); connect( m_documentationChooser, SIGNAL(currentIndexChanged(int)), this, SLOT(documentationChosen(int)) ); } void DocumentationDockWidget::showEvent( QShowEvent *event ) { if ( !m_documentationWidget->url().isValid() ) { // Load documentation HTML when the dock widget gets shown for the first time documentationChosen( 0 ); } QWidget::showEvent( event ); } void DocumentationDockWidget::showDocumentation( const QString &key ) { const int pos = key.indexOf( '|' ); if ( pos == -1 ) { qWarning() << "Invalid documentation key:" << key; return; } QString className = key.left( pos ).toLower(); if ( className.isEmpty() ) { className = "index"; } const QString scriptClassName = className == QLatin1String("resultobject") ? "result" : className; const QString name = key.mid( pos + 1 ).toLower(); const QString anchor = '#' + (className == QLatin1String("index") ? "script_functions_" + name : scriptClassName + '-' + name); - const QString documentationFileName = KGlobal::dirs()->findResource( + const QString documentationFileName = KStandardDirs::locate( "data", QString("timetablemate/doc/%1.html").arg(className) ); if ( documentationFileName.isEmpty() ) { qWarning() << "Documentation for" << className << "not found"; return; } m_documentationWidget->setUrl( QUrl("file://" + documentationFileName + anchor) ); } void DocumentationDockWidget::documentationChosen( int index ) { const QString page = m_documentationChooser->itemData( index ).toString(); - const QString documentationFileName = KGlobal::dirs()->findResource( + const QString documentationFileName = KStandardDirs::locate( "data", QString("timetablemate/doc/%1.html").arg(page) ); m_documentationWidget->load( QUrl("file://" + documentationFileName) ); } void DocumentationDockWidget::documentationUrlChanged( const QUrl &url ) { QRegExp regExp("/doc/(.*)\\.html(?:#.*)?$"); if ( regExp.indexIn(url.toString()) != -1 ) { const QString page = regExp.cap( 1 ); const int index = m_documentationChooser->findData( page ); if ( index != -1 ) { const bool wasBlocked = m_documentationChooser->blockSignals( true ); m_documentationChooser->setCurrentIndex( index ); m_documentationChooser->blockSignals( wasBlocked ); } else { kDebug() << "Documentation page not found:" << page; } } else { kDebug() << "Unexpected url format:" << url; } } QWidget *DocumentationDockWidget::mainWidget() const { return m_documentationWidget; } diff --git a/engine/timetablemate/src/tabs/abstracttab.cpp b/engine/timetablemate/src/tabs/abstracttab.cpp index 431e6d4..0533922 100644 --- a/engine/timetablemate/src/tabs/abstracttab.cpp +++ b/engine/timetablemate/src/tabs/abstracttab.cpp @@ -1,313 +1,314 @@ /* * 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 "abstracttab.h" // Own includes #include "project.h" #include "parserenums.h" #include "javascriptmodel.h" #include "javascriptparser.h" #include "javascriptcompletionmodel.h" #include "../debugger/debugger.h" #include // Public Transport engine include #include #include // KDE includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include +#include AbstractTab::AbstractTab( Project *project, TabType type, QWidget *parent ) : QWidget(parent), m_project(project), m_widget(0), m_modified(false) { // Cannot call typeName() here, because it uses type(), which is virtual setObjectName( Tabs::nameForType(type) ); QVBoxLayout *layout = new QVBoxLayout( this ); layout->setContentsMargins( 0, 0, 0, 0 ); connect( this, SIGNAL(tabCloseRequest()), project, SLOT(slotTabCloseRequest()) ); connect( this, SIGNAL(otherTabsCloseRequest()), project, SLOT(slotOtherTabsCloseRequest()) ); } AbstractTab::~AbstractTab() { if ( isModified() ) { qWarning() << "Destroying tab with modifications"; } } AbstractDocumentTab::AbstractDocumentTab( Project *project, TabType type, QWidget *parent ) : AbstractTab(project, type, parent), m_document(createDocument(this)) { connect( m_document, SIGNAL(modifiedChanged(KTextEditor::Document*)), this, SLOT(slotModifiedChanged(KTextEditor::Document*)) ); connect( m_document, SIGNAL(viewCreated(KTextEditor::Document*,KTextEditor::View*)), this, SLOT(viewCreated(KTextEditor::Document*,KTextEditor::View*)) ); } AbstractDocumentTab::~AbstractDocumentTab() { KTextEditor::Editor *editor = KTextEditor::EditorChooser::editor(); if ( editor ) { // Write editor configuration editor->writeConfig(); } delete m_document; } void AbstractTab::setWidget( QWidget *widget ) { if ( m_widget ) { layout()->removeWidget( m_widget ); } layout()->addWidget( widget ); m_widget = widget; connect( m_widget, SIGNAL(destroyed(QObject*)), this, SLOT(deleteLater()) ); } KTextEditor::Document *AbstractDocumentTab::createDocument( QWidget *parent ) { KService::Ptr service = KService::serviceByStorageId( "katepart" ); if ( !service ) { qWarning() << "Could not find the kate part"; return 0; } // Create text editor part return qobject_cast( service->createInstance(parent) ); } KTextEditor::View *AbstractDocumentTab::defaultView() { // Ensure a view gets created m_document->widget(); if ( m_document->views().isEmpty() ) { qWarning() << "No view created"; return m_document->createView( this ); } else { return m_document->views().first(); } } void AbstractDocumentTab::viewCreated( KTextEditor::Document *document, KTextEditor::View *view ) { Q_UNUSED( document ); // The following code is copied from KDevelop (shell/textdocument.cpp), with a little change: // It merges with the the default katepartui.rc file. The only purpose of this code here is to // remove shortcuts which are also defined in timetablemateui.rc // in KDE >= 4.4 we can use KXMLGuiClient::replaceXMLFile to provide // katepart with our own restructured UI configuration const QString uiFile = KGlobal::mainComponent().componentName() + "/katepartui.rc"; QStringList katePartUIs = KGlobal::mainComponent().dirs()->findAllResources("data", uiFile); if ( !katePartUIs.isEmpty() ) { const QString katePartUI = katePartUIs.last(); - const QString katePartLocalUI = KStandardDirs::locateLocal( "data", uiFile ); + const QString katePartLocalUI = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + uiFile ; if ( !QFile::exists(katePartLocalUI) ) { // prevent warning: No such XML file ".../.kde/share/apps/timetablemate/katepartui.rc" QFile::copy( katePartUI, katePartLocalUI ); } view->replaceXMLFile( katePartUI, katePartLocalUI, true ); // Merge with global XML file } } void AbstractDocumentTab::slotModifiedChanged( KTextEditor::Document *document ) { setModified( document->isModified() ); } void AbstractTab::setModified( bool modified ) { kDebug() << "Set Modified" << modified << m_project->projectName(); if ( modified ) { emit changed(); } if ( modified != m_modified ) { m_modified = modified; emit modifiedStatusChanged( modified ); emit titleChanged( title() ); } } QLatin1String AbstractTab::typeName() const { return Tabs::nameForType( type() ); } QString AbstractTab::fileName() const { return i18nc("@info/plain", "Unsaved Document"); } QString AbstractDocumentTab::fileName() const { const QUrl url = document()->url(); return url.isValid() ? url.path() : AbstractTab::fileName(); } QIcon AbstractTab::icon() const { if ( m_modified ) { return KIcon("document-save"); } else { switch ( type() ) { case Tabs::Dashboard: return KIcon("dashboard-show"); case Tabs::ProjectSource: return KIcon("application-x-publictransport-serviceprovider"); #ifdef BUILD_PROVIDER_TYPE_SCRIPT case Tabs::Script: return KIcon("application-javascript"); #endif #ifdef BUILD_PROVIDER_TYPE_GTFS case Tabs::GtfsDatabase: return KIcon("server-database"); #endif case Tabs::Web: return KIcon("applications-internet"); case Tabs::PlasmaPreview: return KIcon("plasma"); default: return KIcon(); } } } QString AbstractTab::title() const { switch ( type() ) { case Tabs::Dashboard: { const QString name = m_project->projectName(); return name.length() > 25 ? name.left(25) + QString::fromUtf8("…") : name; } case Tabs::ProjectSource: { const QString xmlFileName = m_project->filePath(); return !xmlFileName.isEmpty() ? QFileInfo(xmlFileName).fileName() : i18nc("@title:tab", "Project Source %1", m_project->serviceProviderId()); } #ifdef BUILD_PROVIDER_TYPE_SCRIPT case Tabs::Script: { const QString mainScriptFile = m_project->provider()->data()->scriptFileName(); const QString scriptFileName = fileName(); const bool isMainScript = mainScriptFile == scriptFileName; QString title; if ( !scriptFileName.isEmpty() ) { title = QFileInfo( scriptFileName ).fileName(); } else if ( isMainScript ) { title = i18nc("@title:tab", "Script %1", m_project->serviceProviderId()); } else { title = i18nc("@title:tab", "External Script %1", m_project->serviceProviderId()); } Debugger::Debugger *debugger = project()->debugger(); if ( debugger->isRunning() ) { const bool isDebuggerInTab = debugger->backtraceModel()->isEmpty() ? false : debugger->backtraceModel()->topFrame()->fileName() == scriptFileName; if ( isDebuggerInTab ) { if ( debugger->hasUncaughtException() ) { //m_engine->hasUncaughtException() ) { title += " - " + i18nc("@info/plain", "Exception in Line %1", debugger->uncaughtExceptionLineNumber()); } else if ( debugger->isInterrupted() ) { title += " - " + i18nc("@info/plain", "Interrupted at Line %1", debugger->lineNumber()); } else if ( debugger->isRunning() ) { title += " - " + i18nc("@info/plain", "Running"); } } } return title; } #endif // BUILD_PROVIDER_TYPE_SCRIPT #ifdef BUILD_PROVIDER_TYPE_GTFS case Tabs::GtfsDatabase: return i18nc("@title:tab", "GTFS database %1", m_project->serviceProviderId()); #endif // BUILD_PROVIDER_TYPE_GTFS case Tabs::Web: return i18nc("@title:tab", "Web %1", m_project->serviceProviderId()); case Tabs::PlasmaPreview: return i18nc("@title:tab", "Plasma Preview %1", m_project->serviceProviderId()); default: return "Unknown " + m_project->serviceProviderId(); } } void AbstractTab::showTabContextMenu( const QPoint &globalPos ) { // Show context menu for this tab QPointer contextMenu( new QMenu(this) ); contextMenu->addActions( contextMenuActions(contextMenu.data()) ); contextMenu->exec( globalPos ); delete contextMenu.data(); } QList< QAction* > AbstractTab::contextMenuActions( QWidget *parent ) const { KAction *closeTab = new KAction( KIcon("tab-close"), i18nc("@action", "Close Tab"), parent ); connect( closeTab, SIGNAL(triggered(bool)), this, SIGNAL(tabCloseRequest()) ); KAction *closeOtherTabs = new KAction( KIcon("tab-close-other"), i18nc("@action", "Close Other Tabs"), parent ); connect( closeOtherTabs, SIGNAL(triggered(bool)), this, SIGNAL(otherTabsCloseRequest()) ); KAction *separator = new KAction( parent ); separator->setSeparator( true ); return QList< QAction* >() << closeTab << closeOtherTabs << separator << project()->projectSubMenuAction(parent); } QList< QAction* > AbstractDocumentTab::contextMenuActions( QWidget *parent ) const { KAction *saveTabAction = new KAction( KIcon("document-save"), i18nc("@action", "Save Document"), parent ); saveTabAction->setEnabled( isModified() ); connect( saveTabAction, SIGNAL(triggered(bool)), this, SLOT(save()) ); return QList< QAction* >() << saveTabAction << AbstractTab::contextMenuActions( parent ); } diff --git a/engine/timetablemate/src/tabs/dashboardtab.cpp b/engine/timetablemate/src/tabs/dashboardtab.cpp index 8c429e3..604cac6 100644 --- a/engine/timetablemate/src/tabs/dashboardtab.cpp +++ b/engine/timetablemate/src/tabs/dashboardtab.cpp @@ -1,110 +1,111 @@ /* * 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 "dashboardtab.h" #include "../project.h" // PublicTransport engine includes #include #include // KDE includes #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include +#include DashboardTab::DashboardTab( Project *project, QWidget *parent ) : AbstractTab(project, type(), parent), m_qmlView(0) { // Find the QML file used for the dashboard tab - const QString fileName = KGlobal::dirs()->findResource( "data", "timetablemate/dashboard.qml" ); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "timetablemate/dashboard.qml" ); if ( fileName.isEmpty() ) { qWarning() << "dashboard.qml not found! Check installation"; return; } - const QString svgFileName = KGlobal::dirs()->findResource( "data", "timetablemate/dashboard.svg" ); + const QString svgFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "timetablemate/dashboard.svg" ); // Register classes in Qt's meta object system and for QML qRegisterMetaType< const ServiceProviderData* >( "const ServiceProviderData*" ); qRegisterMetaType< Project* >( "Project*" ); // qRegisterMetaType< TestModel* >( "TestModel*" ); qmlRegisterType< ServiceProviderData, 1 >( "TimetableMate", 1, 0, "ServiceProviderData" ); qmlRegisterUncreatableType< Project >( "TimetableMate", 1, 0, "Project", "Cannot create new projects" ); qmlRegisterUncreatableType< Tabs >( "TimetableMate", 1, 0, "Tabs", "Only for enumerables" ); qmlRegisterUncreatableType< Enums >( "TimetableMate", 1, 0, "PublicTransport", "Only for enumerables" ); // Create dashboard widget QWidget *container = new QWidget( parent ); m_qmlView = new QDeclarativeView( container ); // Install a KDeclarative instance to allow eg. QIcon("icon"), i18n("translate") KDeclarative *kdeclarative = new KDeclarative(); kdeclarative->setDeclarativeEngine( m_qmlView->engine() ); kdeclarative->initialize(); kdeclarative->setupBindings(); m_qmlView->setResizeMode( QDeclarativeView::SizeRootObjectToView ); // Expose the project itself to QML, // because the declarative engine runs in another thread, Project needs to be thread safe m_qmlView->rootContext()->setContextProperty( "project", project ); // Expose the name of the SVG to use m_qmlView->rootContext()->setContextProperty( "svgFileName", svgFileName ); // Add Plasma QML import paths const QStringList importPaths = KGlobal::dirs()->findDirs( "module", "imports" ); foreach( const QString &importPath, importPaths ) { m_qmlView->engine()->addImportPath( importPath ); } m_qmlView->setSource( fileName ); QVBoxLayout *layout = new QVBoxLayout( container ); layout->addWidget( m_qmlView ); setWidget( container ); } DashboardTab::~DashboardTab() { } DashboardTab *DashboardTab::create( Project *project, QWidget *parent ) { DashboardTab *tab = new DashboardTab( project, parent ); return tab; } void DashboardTab::contextMenuEvent( QContextMenuEvent *event ) { project()->showProjectContextMenu( event->globalPos() ); } diff --git a/engine/timetablemate/src/tabs/gtfsdatabasetab.cpp b/engine/timetablemate/src/tabs/gtfsdatabasetab.cpp index b9ad987..a92e87e 100644 --- a/engine/timetablemate/src/tabs/gtfsdatabasetab.cpp +++ b/engine/timetablemate/src/tabs/gtfsdatabasetab.cpp @@ -1,286 +1,287 @@ /* * 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 "gtfsdatabasetab.h" // Own includes #include "../project.h" // Public Transport engine includes #include #include // KDE includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include +#include GtfsDatabaseTab::GtfsDatabaseTab( Project *project, QWidget *parent ) : AbstractTab(project, type(), parent), m_model(0), m_queryModel(0), m_tabWidget(0), m_qmlView(0), m_tableChooser(0), m_tableView(0), m_queryTableView(0), m_query(0) { // Create a tab widget with tabs at the left, because it gets shown in a tab in timetablemate m_tabWidget = new KTabWidget( parent ); m_tabWidget->setTabPosition( QTabWidget::West ); m_tabWidget->setIconSize( QSize(24, 24) ); setWidget( m_tabWidget ); // Find the QML file used for the dashboard tab - const QString fileName = KGlobal::dirs()->findResource( "data", "timetablemate/gtfs_dashboard.qml" ); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "timetablemate/gtfs_dashboard.qml" ); if ( fileName.isEmpty() ) { qWarning() << "gtfs_dashboard.qml not found! Check installation"; return; } - const QString svgFileName = KGlobal::dirs()->findResource( "data", "timetablemate/dashboard.svg" ); + const QString svgFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "timetablemate/dashboard.svg" ); // Register classes in Qt's meta object system and for QML qRegisterMetaType< const ServiceProviderData* >( "const ServiceProviderData*" ); qRegisterMetaType< Project* >( "Project*" ); qmlRegisterType< ServiceProviderData, 1 >( "TimetableMate", 1, 0, "ServiceProviderData" ); qmlRegisterUncreatableType< Project >( "TimetableMate", 1, 0, "Project", "Cannot create new projects" ); qmlRegisterUncreatableType< Tabs >( "TimetableMate", 1, 0, "Tabs", "Only for enumerables" ); qmlRegisterUncreatableType< Enums >( "TimetableMate", 1, 0, "PublicTransport", "Only for enumerables" ); // Create dashboard widget QWidget *container = new QWidget( parent ); m_qmlView = new QDeclarativeView( container ); // Install a KDeclarative instance to allow eg. QIcon("icon"), i18n("translate") KDeclarative *kdeclarative = new KDeclarative(); kdeclarative->setDeclarativeEngine( m_qmlView->engine() ); kdeclarative->initialize(); kdeclarative->setupBindings(); m_qmlView->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); m_qmlView->setResizeMode( QDeclarativeView::SizeRootObjectToView ); // Expose the project itself to QML, // because the declarative engine runs in another thread, Project needs to be thread safe m_qmlView->rootContext()->setContextProperty( "project", project ); // Expose the name of the SVG to use m_qmlView->rootContext()->setContextProperty( "svgFileName", svgFileName ); // Add Plasma QML import paths const QStringList importPaths = KGlobal::dirs()->findDirs( "module", "imports" ); foreach( const QString &importPath, importPaths ) { m_qmlView->engine()->addImportPath( importPath ); } m_qmlView->setSource( fileName ); // Create tab showing database tables QWidget *tableTab = new QWidget( m_tabWidget ); m_tableView = new QTableView( tableTab ); m_tableView->setEditTriggers( QAbstractItemView::NoEditTriggers ); m_tableView->setSortingEnabled( true ); m_tableChooser = new KComboBox( tableTab ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Agency(s)"), "agency" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Stops"), "stops" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Routes (groups of trips)"), "routes" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Trips (sequences of two or more stops"), "trips" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Stop Times"), "stop_times" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Calendar (service dates with weekly schedule)"), "calendar" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Calendar Dates (exceptions for weekly schedules services)"), "calendar_dates" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Fare Attributes"), "fare_attributes" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Fare Rules"), "fare_rules" ); // m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Shapes"), "shapes" ); // Not used m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Frequencies"), "frequencies" ); m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Transfers"), "transfers" ); // m_tableChooser->addItem( KIcon("table"), i18nc("@info/plain", "Feed Info"), "feed_info" ); // Not used // These strings are taken from https://developers.google.com/transit/gtfs/reference?hl=de // (Feed Files), feed file names are replaced by the names used by the table chooser combobox m_tableChooser->setToolTip( i18nc("@info:tooltip", "Choose a table of the GTFS database" "" "Agency(s): " "One or more transit agencies that provide the data in this feed." "Stops: " "Individual locations where vehicles pick up or drop off passengers." "Routes: " "Transit routes. A route is a group of trips that are displayed to riders " "as a single service." "Trips: " "Trips for each route. A trip is a sequence of two or more stops that occurs " "at specific time." "Stop Times: " "Times that a vehicle arrives at and departs from individual stops " "for each trip." "Calendar: " "Dates for service IDs using a weekly schedule. Specify when service starts " "and ends, as well as days of the week where service is available." "Calendar Dates: " "Exceptions for the service IDs defined in the calendar.txt file. " "If Calendar Dates includes all " "dates of service, this file may be specified instead of " "Calendar." "Fare Attributes: " "Fare information for a transit organization's routes." "Fare Rules: " "Rules for applying fare information for a transit organization's routes." "Shapes (not used): " "Rules for drawing lines on a map to represent " "a transit organization's routes." "Frequencies: " "Headway (time between trips) for routes with variable frequency of service." "Transfers: " "Rules for making connections at transfer points between routes." "Feed Info (not used): " "Additional information about the feed itself, including publisher, version, " "and expiration information." "") ); connect( m_tableChooser, SIGNAL(currentIndexChanged(int)), this, SLOT(tableChosen(int)) ); QVBoxLayout *vboxLayout = new QVBoxLayout( tableTab ); vboxLayout->addWidget( m_tableChooser ); vboxLayout->addWidget( m_tableView ); // Create tab to execute database queries QWidget *queryTab = new QWidget( m_tabWidget ); m_query = new KTextEdit( queryTab ); m_query->setClickMessage( i18nc("@info/plain", "Enter an SQLite database query...") ); m_query->setFixedHeight( m_query->fontMetrics().height() * 4 ); QToolButton *runQueryButton = new QToolButton( queryTab ); runQueryButton->setIcon( KIcon("system-run") ); runQueryButton->setText( i18nc("@info/plain", "&Execute Query") ); runQueryButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); connect( runQueryButton, SIGNAL(clicked(bool)), this, SLOT(executeQuery()) ); m_queryTableView = new QTableView( queryTab ); m_queryTableView->setEditTriggers( QAbstractItemView::NoEditTriggers ); m_queryTableView->setSortingEnabled( true ); QHBoxLayout *hboxLayoutQuery = new QHBoxLayout(); hboxLayoutQuery->addWidget( m_query ); hboxLayoutQuery->addWidget( runQueryButton ); QVBoxLayout *vboxLayoutQuery = new QVBoxLayout( queryTab ); vboxLayoutQuery->addLayout( hboxLayoutQuery ); vboxLayoutQuery->addWidget( m_queryTableView ); m_tabWidget->addTab( m_qmlView, KIcon("dashboard-show"), i18nc("@title:tab", "Overview") ); m_tabWidget->addTab( tableTab, KIcon("server-database"), i18nc("@title:tab", "Database") ); m_tabWidget->addTab( queryTab, KIcon("system-run"), i18nc("@title:tab", "Query") ); // Disable database tab, until m_state changes to ImportFinished m_tabWidget->setTabEnabled( 1, false ); gtfsDatabaseStateChanged( project->gtfsDatabaseState() ); connect( project, SIGNAL(gtfsDatabaseStateChanged(Project::GtfsDatabaseState)), this, SLOT(gtfsDatabaseStateChanged(Project::GtfsDatabaseState)) ); } GtfsDatabaseTab::~GtfsDatabaseTab() { } GtfsDatabaseTab *GtfsDatabaseTab::create( Project *project, QWidget *parent ) { // Create GTFS database tab return new GtfsDatabaseTab( project, parent ); } void GtfsDatabaseTab::executeQuery() { if ( !m_queryModel ) { qWarning() << "No database connection"; return; } const QString query = m_query->toPlainText(); m_queryModel->setQuery( query, GtfsDatabase::database(project()->data()->id()) ); } void GtfsDatabaseTab::tableChosen( int index ) { if ( !m_model ) { qWarning() << "No database connection"; return; } const QString tableName = m_tableChooser->itemData( index ).toString(); m_model->setTable( tableName ); m_model->select(); } void GtfsDatabaseTab::gtfsDatabaseStateChanged( Project::GtfsDatabaseState state ) { kDebug() << "GTFS state changed to" << state; switch ( state ) { case Project::GtfsDatabaseImportFinished: m_tableView->setModel( 0 ); m_queryTableView->setModel( 0 ); delete m_model; delete m_queryModel; m_model = new QSqlTableModel( this, GtfsDatabase::database(project()->data()->id()) ); m_queryModel = new QSqlQueryModel( this ); tableChosen( m_tableChooser->currentIndex() ); m_tableView->setModel( m_model ); m_queryTableView->setModel( m_queryModel ); m_tabWidget->setTabEnabled( 1, true ); m_tabWidget->setTabEnabled( 2, true ); break; case Project::GtfsDatabaseError: case Project::GtfsDatabaseImportPending: case Project::GtfsDatabaseImportRunning: m_tabWidget->setTabEnabled( 1, false ); m_tabWidget->setTabEnabled( 2, false ); break; default: break; } } diff --git a/engine/timetablemate/src/tabs/webtab.cpp b/engine/timetablemate/src/tabs/webtab.cpp index 43bb5e9..2830279 100644 --- a/engine/timetablemate/src/tabs/webtab.cpp +++ b/engine/timetablemate/src/tabs/webtab.cpp @@ -1,183 +1,184 @@ /* * 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 "webtab.h" // Own includes #include "project.h" #include "../networkmonitormodel.h" // KDE includes #include #include #include #include #include -#include + #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include +#include WebTab::WebTab( Project *project, QWidget *parent ) : AbstractTab(project, type(), parent), m_webView(0), m_inspector(0), m_toolBar(0), m_urlBar(0), m_networkMonitor(new MonitorNetworkAccessManager(this)), m_networkMonitorModel(new NetworkMonitorModel(this)) { // Create web widget QWidget *container = new QWidget( parent ); setWidget( container ); // Create web view widget m_webView = new KWebView( container ); NetworkMemoryCache *cache = new NetworkMemoryCache( this ); m_networkMonitor->setCache( cache ); m_webView->page()->setNetworkAccessManager( m_networkMonitor ); m_webView->settings()->setAttribute( QWebSettings::DeveloperExtrasEnabled, true ); m_webView->pageAction( QWebPage::OpenLinkInNewWindow )->setVisible( false ); m_webView->pageAction( QWebPage::OpenFrameInNewWindow )->setVisible( false ); m_webView->pageAction( QWebPage::OpenImageInNewWindow )->setVisible( false ); m_webView->setMinimumHeight( 100 ); m_webView->setWhatsThis( i18nc("@info:whatsthis", "Web View" "This is the web view. You can use it to check the URLs you have defined " "in the Project Settings or to get information about the " "structure of the documents that get parsed by the script." "You can select a web element in the inspector " "using the context menu.") ); m_webView->settings()->setIconDatabasePath( - KGlobal::dirs()->saveLocation("data", "plasma_engine_publictransport") ); + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "plasma_engine_publictransport"); // Create a web inspector m_inspector = new QWebInspector( container ); m_inspector->setPage( m_webView->page() ); m_inspector->setMinimumSize( 150, 150 ); m_inspector->hide(); // Connect network monitor model connect( m_networkMonitor, SIGNAL(requestCreated(NetworkMonitorModelItem::Type,QString,QByteArray,QNetworkReply*)), m_networkMonitorModel, SLOT(requestCreated(NetworkMonitorModelItem::Type,QString,QByteArray,QNetworkReply*)) ); // Create URL bar m_urlBar = new KUrlComboBox( KUrlComboBox::Both, true, container ); m_urlBar->setFont( KGlobalSettings::generalFont() ); // Do not use toolbar font as child widget of a KToolBar connect( m_webView, SIGNAL(statusBarMessage(QString)), this, SIGNAL(statusBarMessage(QString)) ); connect( m_webView, SIGNAL(urlChanged(QUrl)), this, SLOT(urlChanged(QUrl)) ); connect( m_webView, SIGNAL(iconChanged()), this, SLOT(faviconChanged()) ); connect( m_urlBar, SIGNAL(returnPressed(QString)), this, SLOT(urlBarReturn(QString)) ); connect( m_urlBar, SIGNAL(urlActivated(QUrl)), this, SLOT(urlActivated(QUrl)) ); connect( m_webView, SIGNAL(loadStarted()), this, SLOT(slotLoadStarted()) ); connect( m_webView, SIGNAL(loadFinished(bool)), this, SLOT(slotLoadFinished()) ); // Create web toolbar m_toolBar = new KToolBar( "webToolBar", this, false ); m_toolBar->addAction( m_webView->pageAction(QWebPage::Back) ); m_toolBar->addAction( m_webView->pageAction(QWebPage::Forward) ); m_toolBar->addAction( m_webView->pageAction(QWebPage::Stop) ); m_toolBar->addAction( m_webView->pageAction(QWebPage::Reload) ); m_toolBar->addWidget( m_urlBar ); QVBoxLayout *layoutWeb = new QVBoxLayout( container ); layoutWeb->setContentsMargins( 0, 2, 0, 0 ); // 2px margin at top layoutWeb->setSpacing( 0 ); layoutWeb->addWidget( m_toolBar ); layoutWeb->addWidget( m_webView ); } WebTab::~WebTab() { delete m_webView; } void WebTab::slotLoadStarted() { emit canStopChanged( true ); } void WebTab::slotLoadFinished() { emit canStopChanged( false ); } WebTab *WebTab::create( Project *project, QWidget *parent ) { WebTab *tab = new WebTab( project, parent ); return tab; } void WebTab::faviconChanged() { const int index = m_urlBar->currentIndex(); if ( index != -1 ) { m_urlBar->changeUrl( index, m_webView->icon(), m_webView->url() ); } else { m_urlBar->addUrl( m_webView->icon(), m_webView->url() ); } } void WebTab::urlChanged( const QUrl &url ) { if ( !m_urlBar->contains(url.toString()) ) { m_urlBar->insertUrl( 0, m_webView->icon(), url ); m_urlBar->setCurrentIndex( 0 ); } else { int index = -1; for ( int i = 0; i < m_urlBar->urls().count(); ++i ) { if ( m_urlBar->urls()[i] == url.toString() ) { index = i; break; } } if ( index == -1 ) { m_urlBar->setEditUrl( url ); } else { m_urlBar->setCurrentIndex( index ); } } emit canGoBackChanged( m_webView->history()->canGoBack() ); emit canGoForwardChanged( m_webView->history()->canGoForward() ); } void WebTab::urlBarReturn( const QString &url ) { m_webView->setUrl( QUrl::fromUserInput(url) ); } void WebTab::urlActivated( const QUrl &url ) { m_webView->setUrl( url ); } diff --git a/engine/timetablemate/src/tests/DebuggerTest.cpp b/engine/timetablemate/src/tests/DebuggerTest.cpp index 91c856d..64a3038 100644 --- a/engine/timetablemate/src/tests/DebuggerTest.cpp +++ b/engine/timetablemate/src/tests/DebuggerTest.cpp @@ -1,291 +1,291 @@ /* * 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. */ #include "DebuggerTest.h" #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -#include + #include void DebuggerTest::initTestCase() { // Read provider data from XML file ServiceProviderDataReader reader; m_data = reader.read( "de_db" ); QVERIFY( m_data ); // Read script text from file const QString fileName = m_data->scriptFileName(); QVERIFY( QFile::exists(fileName) ); // Open script file QFile file( fileName ); QVERIFY( file.open(QIODevice::ReadOnly) ); // Read and close script file const QByteArray ba = file.readAll(); file.close(); m_program = ba; m_debugger = new Debugger::Debugger( this ); } void DebuggerTest::cleanupTestCase() { delete m_data; m_data = 0; } void DebuggerTest::init() {} void DebuggerTest::cleanup() {} void DebuggerTest::loadScriptTest() { QEventLoop loop; Debugger::LoadScriptJob *loadScriptJob = m_debugger->loadScript( m_program, m_data, Debugger::DefaultDebugFlags ); connect( loadScriptJob, SIGNAL(done(ThreadWeaver::Job*)), &loop, SLOT(quit()) ); if ( !loadScriptJob->isFinished() ) { loop.exec(); } } void DebuggerTest::getDeparturesTest() { QEventLoop loop; connect( m_debugger, SIGNAL(requestTimetableDataResult(QSharedPointer,bool,QString,QList,QVariant)), &loop, SLOT(quit()) ); DepartureRequest request( "TEST_DEPARTURES", "Berlin", QDateTime::currentDateTime(), 30 ); QVERIFY( m_debugger->requestTimetableData(&request, QString(), Debugger::DefaultDebugFlags) ); loop.exec(); } void DebuggerTest::multipleTestsTest() { QEventLoop loop; Debugger::Debugger *debugger2 = new Debugger::Debugger( this ); Debugger::LoadScriptJob *loadScriptJob = debugger2->loadScript( m_program, m_data, Debugger::NeverInterrupt ); connect( loadScriptJob, SIGNAL(done(ThreadWeaver::Job*)), &loop, SLOT(quit()) ); connect( m_debugger, SIGNAL(stopped(ScriptRunData)), &loop, SLOT(quit()) ); connect( debugger2, SIGNAL(stopped(ScriptRunData)), &loop, SLOT(quit()) ); DepartureRequest request( "TEST_DEPARTURES", "Berlin", QDateTime::currentDateTime(), 100 ); DepartureRequest request2( "TEST_DEPARTURES2", "München", QDateTime::currentDateTime(), 100 ); DepartureRequest request3( "TEST_DEPARTURES3", "Dresden", QDateTime::currentDateTime(), 100 ); QVERIFY( m_debugger->requestTimetableData(&request, QString(), Debugger::NeverInterrupt) ); QVERIFY( debugger2->requestTimetableData(&request, QString(), Debugger::NeverInterrupt) ); QVERIFY( m_debugger->requestTimetableData(&request2, QString(), Debugger::NeverInterrupt) ); QVERIFY( debugger2->requestTimetableData(&request2, QString(), Debugger::NeverInterrupt) ); QVERIFY( m_debugger->requestTimetableData(&request3, QString(), Debugger::NeverInterrupt) ); QVERIFY( debugger2->requestTimetableData(&request3, QString(), Debugger::NeverInterrupt) ); int done = 0; do { loop.exec(); ++done; } while ( done < 6 ); m_debugger->finish(); debugger2->finish(); // Wait until all jobs are deleted QTimer::singleShot( 0, &loop, SLOT(quit()) ); loop.exec(); } void DebuggerTest::projectTestsTest() { // Load some more projects ProjectModel *model = new ProjectModel( this ); QList< Project* > projects = QList< Project* >() << new Project(model->weaver()) << new Project(model->weaver()) << new Project(model->weaver()) << new Project(model->weaver()) << new Project(model->weaver()) << new Project(model->weaver()); int i = 0; QVERIFY( projects[i++]->loadProject(ServiceProviderGlobal::fileNameFromId("de_db")) ); QVERIFY( projects[i++]->loadProject(ServiceProviderGlobal::fileNameFromId("ch_sbb")) ); QVERIFY( projects[i++]->loadProject(ServiceProviderGlobal::fileNameFromId("de_fahrplaner")) ); QVERIFY( projects[i++]->loadProject(ServiceProviderGlobal::fileNameFromId("at_oebb")) ); QVERIFY( projects[i++]->loadProject(ServiceProviderGlobal::fileNameFromId("dk_rejseplanen")) ); QVERIFY( projects[i++]->loadProject(ServiceProviderGlobal::fileNameFromId("ie_eireann")) ); // Create signal spies to test signals (testStarted() and testFinished()) // Create an event loop that quits when a test in one of the projects // gets started of has finished QEventLoop loop; QList< QSignalSpy* > testBeginSpies; QList< QSignalSpy* > testEndSpies; foreach ( Project *project, projects ) { project->setQuestionsEnabled( false ); model->appendProject( project ); connect( project, SIGNAL(testStarted()), &loop, SLOT(quit()) ); connect( project, SIGNAL(testFinished(bool)), &loop, SLOT(quit()) ); // connect( project, SIGNAL(testFinished(bool)), project, SLOT(testProject()) ); testBeginSpies << new QSignalSpy( project, SIGNAL(testStarted()) ); testEndSpies << new QSignalSpy( project, SIGNAL(testFinished(bool)) ); } QElapsedTimer timer; timer.start(); // Call testProject() twice on each project at random times // to try to produce rare situations where the program may crash const int time = 2700; i = 0; QTimer::singleShot( 0, projects[i], SLOT(testProject()) ); QTimer::singleShot( time, projects[i++], SLOT(testProject()) ); QTimer::singleShot( 0.2 * time, projects[i], SLOT(testProject()) ); QTimer::singleShot( 1.2 * time, projects[i++], SLOT(testProject()) ); QTimer::singleShot( 0.4 * time, projects[i], SLOT(testProject()) ); QTimer::singleShot( 0.8 * time, projects[i++], SLOT(testProject()) ); QTimer::singleShot( 0.05 * time, projects[i], SLOT(testProject()) ); QTimer::singleShot( 0.8 * time, projects[i++], SLOT(testProject()) ); QTimer::singleShot( 0.05 * time, projects[i], SLOT(testProject()) ); QTimer::singleShot( time, projects[i++], SLOT(testProject()) ); QTimer::singleShot( 0.05 * time, projects[i], SLOT(testProject()) ); QTimer::singleShot( time, projects[i++], SLOT(testProject()) ); // Wait until testing starts loop.exec(); // Wait until testing ends QTimer timeout; timeout.setInterval( 5000 ); // Wait 5 seconds for a project to finish it's tests connect( &timeout, SIGNAL(timeout()), &loop, SLOT(quit()) ); forever { // Get some information from the debuggers QString infoString; foreach ( Project *project, projects ) { if ( !infoString.isEmpty() ) { infoString.append( ", " ); } infoString.append( project->data()->id() + ' ' ); if ( project->isTestRunning() ) { infoString.append( "Testing " ); } if ( project->isDebuggerRunning() ) { infoString.append( QString("and debbuging at line %1 ") .arg(project->debugger()->lineNumber()) ); } if ( !project->isTestRunning() && !project->isDebuggerRunning() ) { infoString.append( "Ready " ); } } kDebug() << "State " << infoString << timer.elapsed() << time; if ( timer.elapsed() > time ) { bool testsFinished = true; foreach ( Project *project, projects ) { if ( TestModel::isFinishedState(project->testModel()->completeState()) ) { kDebug() << "Test is finished for" << project->data()->id(); project->clearTestResults(); // disconnect( project, SIGNAL(testFinished(bool)), project, SLOT(testProject()) ); } qApp->processEvents(); if ( project->isTestRunning() ) { testsFinished = false; kDebug() << "Still running:" << project->data()->id(); break; } } if ( testsFinished ) { break; } } timeout.start(); loop.exec(); timeout.stop(); } // Wait until all jobs are deleted QTimer::singleShot( 0, &loop, SLOT(quit()) ); loop.exec(); // Ensure the testFinished() signals got emitted as often as testStarted() for ( int i = 0; i < testBeginSpies.count(); ++i ) { QCOMPARE( testBeginSpies[i]->count(), testEndSpies[i]->count() ); } qDeleteAll( testBeginSpies ); qDeleteAll( testEndSpies ); } void DebuggerTest::testAbortionTest() { // Load some more projects ProjectModel *model = new ProjectModel( this ); Project *project = new Project( model->weaver() ); QVERIFY( project->loadProject(ServiceProviderGlobal::fileNameFromId("de_db")) ); // Create signal spies to test signals (testStarted() and testFinished()) // Create an event loop that quits when a test in one of the projects // gets started of has finished QEventLoop loop; QSignalSpy *testBeginSpy, *testEndSpy; project->setQuestionsEnabled( false ); model->appendProject( project ); connect( project, SIGNAL(testStarted()), &loop, SLOT(quit()) ); connect( project, SIGNAL(testFinished(bool)), &loop, SLOT(quit()) ); testBeginSpy = new QSignalSpy( project, SIGNAL(testStarted()) ); testEndSpy = new QSignalSpy( project, SIGNAL(testFinished(bool)) ); QTimer::singleShot( 0, project, SLOT(testProject()) ); QTimer::singleShot( 200, project, SLOT(abortTests()) ); // Wait until testing starts loop.exec(); // Wait until testing ends while ( project->isTestRunning() ) { loop.exec(); } // Wait until all jobs are deleted QTimer::singleShot( 0, &loop, SLOT(quit()) ); loop.exec(); // Ensure the testFinished() signals got emitted as often as testStarted() QCOMPARE( testBeginSpy->count(), testEndSpy->count() ); delete testBeginSpy; delete testEndSpy; } QTEST_MAIN(DebuggerTest) #include "DebuggerTest.moc" diff --git a/engine/timetablemate/src/timetablemate.cpp b/engine/timetablemate/src/timetablemate.cpp index f3ff9d0..c4268cd 100644 --- a/engine/timetablemate/src/timetablemate.cpp +++ b/engine/timetablemate/src/timetablemate.cpp @@ -1,2339 +1,2340 @@ /* * 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 "timetablemate.h" // Own includes #include "project.h" #include "projectmodel.h" #include "ui_preferences.h" #include "settings.h" // Debugger includes #include "debugger/debugger.h" #include "debugger/variablemodel.h" // Tab widgets #include "tabs/abstracttab.h" #include "tabs/dashboardtab.h" #include "tabs/projectsourcetab.h" #include "tabs/webtab.h" #include "tabs/plasmapreviewtab.h" #ifdef BUILD_PROVIDER_TYPE_SCRIPT #include "tabs/scripttab.h" #endif #ifdef BUILD_PROVIDER_TYPE_GTFS #include "tabs/gtfsdatabasetab.h" #endif // Dock widgets #include "docks/docktoolbar.h" #include "docks/projectsdockwidget.h" #include "docks/consoledockwidget.h" #include "docks/documentationdockwidget.h" #include "docks/variablesdockwidget.h" #include "docks/outputdockwidget.h" #include "docks/breakpointdockwidget.h" #include "docks/backtracedockwidget.h" #include "docks/consoledockwidget.h" #include "docks/testdockwidget.h" #include "docks/webinspectordockwidget.h" #include "docks/networkmonitordockwidget.h" // PublicTransport engine includes #include #include #include // KDE includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include // For KStandardDirs::checkAccess(), W_OK, in TimetableMate::fileOpenInstalled() +#include // This function returns all actions that get connected to the currently active project // in TimetableMate::activeProjectAboutToChange(). These actions are proxy actions for the actions // inside the different projects and are added to the main TimetableMate UI (extern to the // projects). They are stored in the KActionCollection as Project::projectActionName(). const QList< Project::ProjectAction > externProjectActions() { return QList< Project::ProjectAction >() << Project::Save << Project::SaveAs << Project::Install << Project::InstallGlobally << Project::Publish << Project::ShowProjectSettings << Project::Close << Project::ShowHomepage << Project::RunAllTests << Project::AbortRunningTests << Project::ClearTestResults #ifdef BUILD_PROVIDER_TYPE_SCRIPT << Project::RunMenuAction << Project::DebugMenuAction << Project::StepInto << Project::StepOver << Project::StepOut << Project::RunToCursor << Project::Interrupt << Project::Continue << Project::AbortDebugger << Project::ToggleBreakpoint << Project::RemoveAllBreakpoints #endif ; } void moveContainer( KXMLGUIClient *client, const QString &tagname, const QString &name, const QString &to_name, bool recursive ) { QDomDocument doc = client->xmlguiBuildDocument (); if (doc.documentElement ().isNull ()) doc = client->domDocument (); // find the given elements QDomElement e = doc.documentElement (); QDomElement from_elem; QDomElement to_elem; QDomNodeList list = e.elementsByTagName (tagname); int count = list.count (); for (int i = 0; i < count; ++i) { QDomElement elem = list.item (i).toElement (); if (elem.isNull ()) continue; if (elem.attribute ("name") == name) { from_elem = elem; } else if (elem.attribute ("name") == to_name) { to_elem = elem; } } // move from_elem.parentNode ().removeChild (from_elem); to_elem.appendChild (from_elem); // set result client->setXMLGUIBuildDocument (doc); // recurse if (recursive) { QList children = client->childClients (); QList::const_iterator it; for (it = children.constBegin (); it != children.constEnd (); ++it) { moveContainer (*it, tagname, name, to_name, true); } } } TimetableMate::TimetableMate() : KParts::MainWindow( 0, Qt::WindowContextHelpButtonHint ), ui_preferences(0), m_projectModel(0), m_partManager(0), m_tabWidget(new KTabWidget(this)), m_qmlView(0), m_leftDockBar(0), m_rightDockBar(0), m_bottomDockBar(0), m_progressBar(0), m_documentationDock(0), m_projectsDock(0), m_testDock(0), m_webInspectorDock(0), m_networkMonitorDock(0), #ifdef BUILD_PROVIDER_TYPE_SCRIPT m_backtraceDock(0), m_consoleDock(0), m_outputDock(0), m_breakpointDock(0), m_variablesDock(0), #endif m_showDocksAction(0), m_toolbarAction(0), m_recentFilesAction(0), m_currentTab(0), m_messageWidgetLayout(new QVBoxLayout()), m_downloadDialog(0) { m_partManager = new KParts::PartManager( this ); m_tabWidget->setDocumentMode( true ); m_tabWidget->setAutomaticResizeTabs( true ); m_tabWidget->setMovable( true ); m_tabWidget->setTabsClosable( true ); QWidget *widget = new QWidget( this ); widget->setMinimumSize( 220, 200 ); m_mainLayout = new QVBoxLayout( widget ); m_mainLayout->setContentsMargins( 0, 0, 0, 0 ); m_messageWidgetLayout->setContentsMargins( 0, 0, 0, 0 ); m_mainLayout->addWidget( m_tabWidget ); m_mainLayout->addLayout( m_messageWidgetLayout ); setCentralWidget( widget ); // Connect signals connect( m_partManager, SIGNAL(activePartChanged(KParts::Part*)), this, SLOT(activePartChanged(KParts::Part*)) ); connect( m_tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int)) ); connect( m_tabWidget, SIGNAL(currentChanged(int)), this, SLOT(currentTabChanged(int)) ); connect( m_tabWidget, SIGNAL(contextMenu(QWidget*,QPoint)), this, SLOT(tabContextMenu(QWidget*,QPoint)) ); // Create project model m_projectModel = new ProjectModel( this ); connect( m_projectModel, SIGNAL(activeProjectAboutToChange(Project*,Project*)), this, SLOT(activeProjectAboutToChange(Project*,Project*)) ); connect( m_projectModel, SIGNAL(activeProjectChanged(Project*,Project*)), this, SLOT(activeProjectChanged(Project*,Project*)) ); connect( m_projectModel, SIGNAL(projectAdded(Project*)), this, SLOT(projectAdded(Project*)) ); connect( m_projectModel, SIGNAL(projectAboutToBeRemoved(Project*)), this, SLOT(projectAboutToBeRemoved(Project*)) ); connect( m_projectModel, SIGNAL(testProgress(int,int)), this, SLOT(updateProgress(int,int)) ); Settings::self()->readConfig(); setupActions(); setupDockWidgets(); setupGUI(); if ( !fixMenus() ) { int result = KMessageBox::warningContinueCancel( this, i18nc("@info", "Initialization Error" "There seems to be a problem with your installation. The UI will not " "be complete and there may be errors if you continue now." "Possible Solution: " "Please reinstall TimetableMate and try again."), i18nc("@title:window", "Error"), KStandardGuiItem::cont(), KStandardGuiItem::quit(), QString(), KMessageBox::Notify | KMessageBox::Dangerous ); if ( result != KMessageBox::Continue ) { QApplication::quit(); deleteLater(); return; } } populateTestMenu(); // Create fixed dock overview toolbars after setupGUI() m_leftDockBar = new DockToolBar( Qt::LeftDockWidgetArea, "leftDockBar", m_showDocksAction, this ); m_rightDockBar = new DockToolBar( Qt::RightDockWidgetArea, "rightDockBar", m_showDocksAction, this ); m_bottomDockBar = new DockToolBar( Qt::BottomDockWidgetArea, "bottomDockBar", m_showDocksAction, this ); QList dockToggleActions; dockToggleActions << action("toggle_dock_projects") << action("toggle_dock_variables") << action("toggle_dock_test") << action("toggle_dock_console") << action("toggle_dock_breakpoints") << action("toggle_dock_backtrace") << action("toggle_dock_output") << action("toggle_dock_documentation") << action("toggle_dock_webinspector") << action("toggle_dock_networkmonitor"); foreach ( QAction *action, dockToggleActions ) { DockToolButtonAction *dockAction = qobject_cast< DockToolButtonAction* >( action ); if ( !dockAction ) { continue; } Qt::DockWidgetArea area = dockWidgetArea( dockAction->dockWidget() ); switch ( area ) { case Qt::LeftDockWidgetArea: m_leftDockBar->addAction( dockAction ); break; case Qt::RightDockWidgetArea: m_rightDockBar->addAction( dockAction ); break; case Qt::BottomDockWidgetArea: m_bottomDockBar->addAction( dockAction ); break; default: qWarning() << "Top dock widget area is not supported"; break; } connect( dockAction->dockWidget(), SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), this, SLOT(dockLocationChanged(Qt::DockWidgetArea)) ); } addToolBar( Qt::LeftToolBarArea, m_leftDockBar ); addToolBar( Qt::RightToolBarArea, m_rightDockBar ); addToolBar( Qt::BottomToolBarArea, m_bottomDockBar ); // Ensure the projects dock is visible on program start (if it was created) if ( m_projectsDock && !m_projectsDock->isVisible() ) { m_projectsDock->show(); } // Set initial states stateChanged( "script_tab_is_active", StateReverse ); QTimer::singleShot( 0, this, SLOT(initialize()) ); } TimetableMate::~TimetableMate() { delete m_downloadDialog; delete ui_preferences; delete m_progressBar; } void TimetableMate::updateProgress( int finishedTests, int totalTests ) { if ( !m_progressBar ) { QWidget *widget = new QWidget( this ); // Create a progress bar, use the same height as the bottom DockToolBar QProgressBar *progressBar = new QProgressBar( widget ); progressBar->setMaximumSize( 250, KIconLoader::SizeSmall ); progressBar->setFormat( "%v of %m tests" ); // Create a fixed toolbar for the progress bar QToolBar *progressToolBar = new QToolBar( this ); progressToolBar->setObjectName( "progress" ); progressToolBar->setMovable( false ); progressToolBar->setFloatable( false ); // Add the progress bar to it's tool bar // and put a spacer before it to align it to the right QHBoxLayout *layout = new QHBoxLayout( widget ); layout->setContentsMargins( 0, 0, 0, 0 ); layout->addSpacerItem( new QSpacerItem(5, 5, QSizePolicy::Expanding) ); layout->addWidget( progressBar ); progressToolBar->addWidget( widget ); addToolBar( Qt::BottomToolBarArea, progressToolBar ); // Create a new object with data for the progress bar m_progressBar = new ProgressBarData( progressToolBar, progressBar ); } // Set values for the progress bar m_progressBar->progressBar->setMaximum( totalTests ); m_progressBar->progressBar->setValue( finishedTests ); // Stop running timers to hide the progress bar delete m_progressBar->progressBarTimer; m_progressBar->progressBarTimer = 0; if ( finishedTests == totalTests ) { // When all tests are finished start a timer to hide the progress bar again m_progressBar->progressBarTimer = new QTimer( this ); connect( m_progressBar->progressBarTimer, SIGNAL(timeout()), this, SLOT(hideProgress()) ); m_progressBar->progressBarTimer->start( 5000 ); } } TimetableMate::ProgressBarData::~ProgressBarData() { if ( progressBarTimer ) { progressBarTimer->stop(); delete progressBarTimer; progressBarTimer = 0; } if ( progressToolBar ) { delete progressToolBar; progressToolBar = 0; progressBar = 0; // Got deleted with m_progressToolBar } } void TimetableMate::hideProgress() { if ( m_progressBar ) { if ( m_progressBar->progressToolBar ) { removeToolBar( m_progressBar->progressToolBar ); } delete m_progressBar; m_progressBar = 0; } } void TimetableMate::saveProperties( KConfigGroup &config ) { m_recentFilesAction->saveEntries( config ); QStringList openedProjects; for ( int row = 0; row < m_projectModel->rowCount(); ++ row ) { Project *project = m_projectModel->projectItemFromRow( row )->project(); const QString filePath = project->filePath(); if ( !filePath.isEmpty() ) { QStringList openedTabs; const QList< TabType > allTabs = QList< TabType >() << Tabs::Dashboard << Tabs::ProjectSource << Tabs::PlasmaPreview << Tabs::Web #ifdef BUILD_PROVIDER_TYPE_SCRIPT << Tabs::Script #endif #ifdef BUILD_PROVIDER_TYPE_GTFS << Tabs::GtfsDatabase #endif ; foreach ( TabType tab, allTabs ) { if ( project->isTabOpened(tab) ) { openedTabs << QString::number(static_cast(tab)); } } QString projectString = QString("%1 ::%2%3") .arg( filePath ).arg( openedTabs.join(",") ) .arg( project->isActiveProject() ? " ::active" : QString() ); openedProjects << projectString; } } config.writeEntry( "lastOpenedProjects", openedProjects ); } void TimetableMate::readProperties( const KConfigGroup &config ) { const QStringList lastOpenedProjects = config.readEntry( "lastOpenedProjects", QStringList() ); QStringList failedToOpenProjects; foreach ( const QString &lastOpenedProject, lastOpenedProjects ) { // For each project that was opened in the last session a string is stored, // if no tabs of that project were opened, the string only contains the .pts file name. // Otherwise previously opened tabs are encoded after a " ::" const int pos = lastOpenedProject.indexOf( QLatin1String(" ::") ); QString xmlFilePath; QList< TabType > openedTabs; bool isActive = false; if ( pos == -1 ) { xmlFilePath = lastOpenedProject; } else { xmlFilePath = lastOpenedProject.left( pos ); // Get the part of the string that encodes the previously opened projects QString openedTabsCode = lastOpenedProject.mid( pos + 3 ); if ( openedTabsCode.endsWith(QLatin1String(" ::active")) ) { // The current project was the active project isActive = true; openedTabsCode.chop( 9 ); // Cut " ::active" } if ( !openedTabsCode.isEmpty() ) { // The rest of the string contains the indexes of the previously opened tab types const QStringList openedTabStrings = openedTabsCode.split(','); foreach ( const QString &openedTabString, openedTabStrings ) { openedTabs << static_cast< TabType >( openedTabString.toInt() ); } } } // Open the project Project *project = openProject( xmlFilePath ); if ( project ) { // Restore active state and open previously opened tabs of the project if ( isActive ) { project->setAsActiveProject(); } foreach ( TabType tab, openedTabs ) { project->showTab( tab ); } } else { failedToOpenProjects << xmlFilePath; } } // Show an information message box, if projects failed to open if ( !failedToOpenProjects.isEmpty() ) { KMessageBox::informationList( this, i18nc("@info", "The following projects could not be opened"), failedToOpenProjects, i18nc("@title:window", "Failed to Open"), "couldNotOpenLastProjects" ); } if ( m_projectModel->activeProject() && m_projectsDock ) { // Expand active project item m_projectsDock->projectsWidget()->expand( m_projectModel->indexFromProject(m_projectModel->activeProject()) ); } } void TimetableMate::initialize() { Settings *settings = Settings::self(); const int threadCount = settings->maximumThreadCount(); m_projectModel->weaver()->setMaximumNumberOfThreads( threadCount < 1 ? 32 : threadCount ); KConfig *config = settings->config(); if ( !config->hasGroup("Kate Document Defaults") ) { // Kate document settings not written yet, // write default values for TimetableMate to have consistent indentation // with 4 spaces and no tabulators by default, also make removing spaces the default KConfigGroup group = config->group("Kate Document Defaults"); group.writeEntry( "Indentation Width", 4 ); group.writeEntry( "ReplaceTabsDyn", true ); // Indent with (4) spaces, no tabs group.writeEntry( "Remove Spaces", true ); group.writeEntry( "RemoveTrailingDyn", true ); group.sync(); } bool restoreProjects = settings->restoreProjects(); if ( settings->lastSessionCrashed() ) { // The last session has crashed if ( restoreProjects ) { // Restoring projects is enabled, check if there are any projects to restore // before asking whether restoration should be disabled if ( settings->config()->hasGroup("last_session") ) { KConfigGroup config = settings->config()->group("last_session"); const QStringList lastOpenedProjects = config.readEntry( "lastOpenedProjects", QStringList() ); if ( !lastOpenedProjects.isEmpty() ) { // There are projects to restore, ask the user what to do int result = KMessageBox::warningContinueCancel( this, i18nc("@info", "Last Session Crashed - Sorry" "Restoring previously opened projects and tabs " "might result in another crash." "Do you want to nevertheless restore projects?"), i18nc("@title:window", "Crash Protection"), KGuiItem(i18nc("@info/plain", "&Restore Projects"), KIcon("security-medium")), KGuiItem(i18nc("@info/plain", "Do ¬ Restore Projects"), KIcon("security-high")), "crash_protection" ); if ( result != KMessageBox::Continue ) { restoreProjects = false; } } } } } else { // No last session or the last session did finish successfully. // Store true for the "lastSessionCrashed" setting while TimetableMate is running. // When it exits successfully it sets it to false again. If TimetableMate crashes before // a successful exit, true is still stored for "lastSessionCrashed". // This requires the use of KUniqueApplication to work properly. settings->setLastSessionCrashed( true ); settings->writeConfig(); } updateShownDocksAction(); if ( settings->config()->hasGroup("last_session") ) { KConfigGroup config = settings->config()->group("last_session"); m_recentFilesAction->loadEntries( config ); currentTabChanged( -1 ); if ( restoreProjects ) { readProperties( config ); } } else { currentTabChanged( -1 ); } } void TimetableMate::populateTestMenu() { // Fill test action list with menu actions for each test case m_testCaseActions.clear(); if ( m_projectModel->activeProject() ) { for ( int i = 0; i < TestModel::TestCaseCount; ++i ) { // Only add test cases that contain applicable tests const TestModel::TestCase testCase = static_cast( i ); if ( TestModel::isTestCaseApplicableTo(testCase, m_projectModel->activeProject()->data()) ) { QAction *action = Project::createProjectAction( Project::SpecificTestCaseMenuAction, QVariant::fromValue(static_cast(testCase)), this ); m_testCaseActions << action; } } } unplugActionList( "test_list" ); plugActionList( "test_list", m_testCaseActions ); } void TimetableMate::connectTestMenuWithProject( Project *project, bool doConnect ) { if ( !project ) { return; } foreach ( QAction *action, m_testCaseActions ) { const Project::ProjectActionData data = project->projectActionData( action ); project->connectProjectAction( data.actionType, action, doConnect ); } } QAction *TimetableMate::createCustomElement( QWidget *parent, int index, const QDomElement &element ) { QAction* before = 0L; if ( index > 0 && index < parent->actions().count() ) { before = parent->actions().at( index ); } // Copied from KDevelop, menubar separators need to be defined as // and to be always shown in the menubar. For those, we create special disabled actions // instead of calling QMenuBar::addSeparator() because menubar separators are ignored if ( element.tagName().toLower() == QLatin1String("separator") && element.attribute("style") == QLatin1String("visible") ) { if ( QMenuBar* bar = qobject_cast(parent) ) { QAction *separatorAction = new QAction( "|", this ); bar->insertAction( before, separatorAction ); separatorAction->setDisabled( true ); separatorAction->setObjectName( element.attribute("name") ); return separatorAction; } } return KXMLGUIBuilder::createCustomElement( parent, index, element ); } bool TimetableMate::fixMenus() { const QList< QAction* > menuBarActions = menuBar()->actions(); QHash< QString, QAction* > menus; foreach ( QAction *menuBarAction, menuBarActions ) { menus.insert( menuBarAction->objectName(), menuBarAction ); } // Show the file menu only when it is not empty QAction *fileMenu = menus["file"]; if ( fileMenu ) { fileMenu->setVisible( !fileMenu->menu()->isEmpty() ); } #ifndef BUILD_PROVIDER_TYPE_SCRIPT // Hide the run menu if the script provider type is disabled // (and the run menu is actually empty) QAction *runMenu = menus["run"]; if ( runMenu ) { runMenu->setVisible( !runMenu->menu()->isEmpty() ); } #endif // Show the separator after the part menus only when part menus are there QAction *separatorPartMenusEnd = menus["separator_part_menus_end"]; if ( separatorPartMenusEnd ) { separatorPartMenusEnd->setVisible( fileMenu->isVisible() && menus["edit"] && menus["view"] && menus["tools"] && menus["bookmarks"] ); } else { qWarning() << "Missing separator_part_menus_end, timetablemateui.rc not installed?"; } QAction *editMenu = menus["edit"]; if ( editMenu ) { const QList< QAction* > actions = editMenu->menu()->actions(); foreach ( QAction *action, actions ) { if ( action->objectName() == QLatin1String("edit_undo") || action->objectName() == QLatin1String("edit_redo") ) { action->setPriority( QAction::LowPriority ); } } } // If the "separator_part_menus_end" menu bar item cannot be found, assume that // timetablemateui.rc was not installed and return false return separatorPartMenusEnd; } void TimetableMate::testActionTriggered() { if ( m_projectModel->activeProject() ) { QAction *action = qobject_cast< QAction* >( sender() ); const TestModel::Test test = static_cast< TestModel::Test >( action->data().toInt() ); m_projectModel->activeProject()->startTest( test ); } } void TimetableMate::testCaseActionTriggered() { if ( m_projectModel->activeProject() ) { QAction *action = qobject_cast< QAction* >( sender() ); const TestModel::TestCase testCase = static_cast< TestModel::TestCase >( action->data().toInt() ); m_projectModel->activeProject()->startTestCase( testCase ); } } void TimetableMate::dockLocationChanged( Qt::DockWidgetArea area ) { QDockWidget *dockWidget = qobject_cast< QDockWidget* >( sender() ); Q_ASSERT( dockWidget ); // Find the action to toggle the dock widget in one of the three dock bars and remove it QAction *toggleAction = m_leftDockBar->actionForDockWidget( dockWidget ); if ( toggleAction ) { m_leftDockBar->removeAction( toggleAction ); } else { toggleAction = m_rightDockBar->actionForDockWidget( dockWidget ); if ( toggleAction ) { m_rightDockBar->removeAction( toggleAction ); } else { toggleAction = m_bottomDockBar->actionForDockWidget( dockWidget ); if ( toggleAction ) { m_bottomDockBar->removeAction( toggleAction ); } else { kDebug() << "Action not found for dock widget" << dockWidget; return; } } } // Add the found dock widget toggle action to the dock bar for the new area if ( area == Qt::LeftDockWidgetArea ) { m_leftDockBar->addAction( toggleAction ); } else if ( area == Qt::RightDockWidgetArea ) { m_rightDockBar->addAction( toggleAction ); } else if ( area == Qt::BottomDockWidgetArea ) { m_bottomDockBar->addAction( toggleAction ); } else { kDebug() << "Area is not allowed" << area; } updateShownDocksAction(); } void TimetableMate::updateShownDocksAction() { // Remove all actions, they will be inserted in new order below m_showDocksAction->removeAction( action("toggle_dock_projects") ); m_showDocksAction->removeAction( action("toggle_dock_variables") ); m_showDocksAction->removeAction( action("toggle_dock_documentation") ); m_showDocksAction->removeAction( action("toggle_dock_console") ); m_showDocksAction->removeAction( action("toggle_dock_breakpoints") ); m_showDocksAction->removeAction( action("toggle_dock_backtrace") ); m_showDocksAction->removeAction( action("toggle_dock_output") ); m_showDocksAction->removeAction( action("toggle_dock_test") ); m_showDocksAction->removeAction( action("toggle_dock_webinspector") ); m_showDocksAction->removeAction( action("toggle_dock_networkmonitor") ); // Delete remaining actions (titles, separators, hide actions) QList< QAction* > separators = m_showDocksAction->menu()->actions(); foreach ( QAction *action, separators ) { m_showDocksAction->removeAction( action ); delete action; } // Insert actions for the left dock area KMenu *menu = m_showDocksAction->menu(); if ( !m_leftDockBar->actions().isEmpty() ) { menu->addTitle( i18nc("@title:menu In-menu title", "Left Dock Area") ); menu->addActions( m_leftDockBar->actions() ); // Add another action to the radio group to hide the dock area QAction *hideDockAction = menu->addAction( KIcon("edit-clear"), i18nc("@action:inmenu", "&Hide Left Dock"), m_leftDockBar, SLOT(hideCurrentDock()) ); hideDockAction->setCheckable( true ); if ( !m_leftDockBar->actionGroup()->checkedAction() ) { hideDockAction->setChecked( true ); } m_leftDockBar->actionGroup()->addAction( hideDockAction ); } // Insert actions for the bottom dock area if ( !m_bottomDockBar->actions().isEmpty() ) { menu->addTitle( i18nc("@title:menu In-menu title", "Bottom Dock Area") ); menu->addActions( m_bottomDockBar->actions() ); // Add another action to the radio group to hide the dock area QAction *hideDockAction = menu->addAction( KIcon("edit-clear"), i18nc("@action:inmenu", "&Hide Bottom Dock"), m_bottomDockBar, SLOT(hideCurrentDock()) ); hideDockAction->setCheckable( true ); if ( !m_bottomDockBar->actionGroup()->checkedAction() ) { hideDockAction->setChecked( true ); } m_bottomDockBar->actionGroup()->addAction( hideDockAction ); } // Insert actions for the right dock area (after a separator) if ( !m_rightDockBar->actions().isEmpty() ) { menu->addTitle( i18nc("@title:menu In-menu title", "Right Dock Area") ); menu->addActions( m_rightDockBar->actions() ); // Add another action to the radio group to hide the dock area QAction *hideDockAction = menu->addAction( KIcon("edit-clear"), i18nc("@action:inmenu", "&Hide Right Dock"), m_rightDockBar, SLOT(hideCurrentDock()) ); hideDockAction->setCheckable( true ); if ( !m_rightDockBar->actionGroup()->checkedAction() ) { hideDockAction->setChecked( true ); } m_rightDockBar->actionGroup()->addAction( hideDockAction ); } } void TimetableMate::setupDockWidgets() { m_showDocksAction = new KActionMenu( i18nc("@action", "&Docks Shown"), this ); actionCollection()->addAction( QLatin1String("options_show_docks"), m_showDocksAction ); // Create dock widgets m_projectsDock = new ProjectsDockWidget( m_projectModel, m_showDocksAction, this ); m_testDock = new TestDockWidget( m_projectModel, m_showDocksAction, this ); m_documentationDock = new DocumentationDockWidget( m_showDocksAction, this ); m_webInspectorDock = new WebInspectorDockWidget( m_showDocksAction, this ); m_networkMonitorDock = new NetworkMonitorDockWidget( m_projectModel, m_showDocksAction, this ); #ifdef BUILD_PROVIDER_TYPE_SCRIPT m_backtraceDock = new BacktraceDockWidget( m_projectModel, m_showDocksAction, this ); m_breakpointDock = new BreakpointDockWidget( m_projectModel, m_showDocksAction, this ); m_outputDock = new OutputDockWidget( m_projectModel, m_showDocksAction, this ); m_consoleDock = new ConsoleDockWidget( m_projectModel, m_showDocksAction, this ); m_variablesDock = new VariablesDockWidget( m_projectModel, m_showDocksAction, this ); #endif const QList< AbstractDockWidget* > allDockWidgets = QList< AbstractDockWidget* >() << m_projectsDock << m_testDock << m_documentationDock << m_webInspectorDock << m_networkMonitorDock #ifdef BUILD_PROVIDER_TYPE_SCRIPT << m_backtraceDock << m_breakpointDock << m_outputDock << m_consoleDock << m_variablesDock #endif ; foreach ( AbstractDockWidget *dockWidget, allDockWidgets ) { DockToolButtonAction *toggleAction = new DockToolButtonAction( dockWidget, dockWidget->icon(), dockWidget->windowTitle(), this ); actionCollection()->addAction( "toggle_dock_" + dockWidget->objectName(), toggleAction ); // Add dock widgets to default areas (stored changes to the areas are restored later) addDockWidget( dockWidget->defaultDockArea(), dockWidget ); } } void TimetableMate::activeProjectAboutToChange( Project *project, Project *previousProject ) { // Enable "Save All" action only when at least one project is opened action("project_save_all")->setEnabled( project ); if ( previousProject ) { // Disconnect previously active project foreach ( Project::ProjectAction projectAction, externProjectActions() ) { QAction *qaction = action( Project::projectActionName(projectAction) ); previousProject->connectProjectAction( projectAction, qaction, false ); } connectTestMenuWithProject( previousProject, false ); disconnect( previousProject, SIGNAL(testStarted()), this, SLOT(testStarted()) ); disconnect( previousProject, SIGNAL(testFinished(bool)), this, SLOT(testFinished(bool)) ); #ifdef BUILD_PROVIDER_TYPE_SCRIPT Debugger::Debugger *debugger = previousProject->debugger(); disconnect( debugger, SIGNAL(aborted()), this, SLOT(updateWindowTitle()) ); disconnect( debugger, SIGNAL(interrupted(int,QString,QDateTime)), this, SLOT(updateWindowTitle()) ); disconnect( debugger, SIGNAL(continued(QDateTime,bool)), this, SLOT(updateWindowTitle()) ); disconnect( debugger, SIGNAL(started()), this, SLOT(updateWindowTitle()) ); disconnect( debugger, SIGNAL(stopped()), this, SLOT(updateWindowTitle()) ); disconnect( debugger, SIGNAL(exception(int,QString,QString)), this, SLOT(uncaughtException(int,QString,QString)) ); disconnect( debugger, SIGNAL(breakpointReached(Breakpoint)), this, SLOT(breakpointReached(Breakpoint)) ); if ( m_backtraceDock ) { disconnect( m_backtraceDock, SIGNAL(activeFrameDepthChanged(int)), debugger->variableModel(), SLOT(switchToVariableStack(int)) ); } if ( m_testDock ) { disconnect( m_testDock, SIGNAL(clickedTestErrorItem(QString,int,QString)), previousProject, SLOT(showScriptLineNumber(QString,int)) ); } #endif } if ( project ) { // Connect the new active project foreach ( Project::ProjectAction projectAction, externProjectActions() ) { QAction *qaction = action( Project::projectActionName(projectAction) ); project->connectProjectAction( projectAction, qaction ); } connectTestMenuWithProject( project ); connect( project, SIGNAL(testStarted()), this, SLOT(testStarted()) ); connect( project, SIGNAL(testFinished(bool)), this, SLOT(testFinished(bool)) ); #ifdef BUILD_PROVIDER_TYPE_SCRIPT Debugger::Debugger *debugger = project->debugger(); connect( debugger, SIGNAL(aborted()), this, SLOT(updateWindowTitle()) ); connect( debugger, SIGNAL(interrupted(int,QString,QDateTime)), this, SLOT(updateWindowTitle()) ); connect( debugger, SIGNAL(continued(QDateTime,bool)), this, SLOT(updateWindowTitle()) ); connect( debugger, SIGNAL(started()), this, SLOT(updateWindowTitle()) ); connect( debugger, SIGNAL(stopped()), this, SLOT(updateWindowTitle()) ); connect( debugger, SIGNAL(exception(int,QString,QString)), this, SLOT(uncaughtException(int,QString,QString)) ); connect( debugger, SIGNAL(breakpointReached(Breakpoint)), this, SLOT(breakpointReached(Breakpoint)) ); if ( m_backtraceDock ) { connect( m_backtraceDock, SIGNAL(activeFrameDepthChanged(int)), debugger->variableModel(), SLOT(switchToVariableStack(int)) ); } if ( m_testDock ) { connect( m_testDock, SIGNAL(clickedTestErrorItem(QString,int,QString)), project, SLOT(showScriptLineNumber(QString,int)) ); } #endif stateChanged( "project_opened" ); // Update text of the choose active project action QAction *chooseActiveProject = action("project_choose_active"); if ( chooseActiveProject ) { chooseActiveProject->setText( project->projectName() ); } } else { stateChanged( "no_project_opened" ); stateChanged( "project_opened", StateReverse ); // Update text of the choose active project action QAction *chooseActiveProject = action("project_choose_active"); if ( chooseActiveProject ) { chooseActiveProject->setText( i18nc("@action", "&Active Project") ); } } } void TimetableMate::activeProjectChanged( Project *project, Project *previousProject ) { Q_UNUSED( project ); Q_UNUSED( previousProject ); populateTestMenu(); } bool TimetableMate::queryClose() { // Save session properties into a special group in the configuration KConfigGroup config = Settings::self()->config()->group("last_session"); saveProperties( config ); // Close projects and ask to save if modified const bool close = closeAllProjects(); if ( close ) { // TimetableMate will close, no crash so far, // store that the last program exit was successful, ie. did not crash Settings *settings = Settings::self(); settings->setLastSessionCrashed( false ); settings->writeConfig(); } return close; } Project *TimetableMate::currentProject() { // Get project of the currently shown tab AbstractTab *tab = projectTabAt( m_tabWidget->currentIndex() ); return tab ? tab->project() : 0; } void TimetableMate::updateWindowTitle() { AbstractTab *tab = 0; QString caption; Project *activeProject = m_projectModel->activeProject(); // Start caption with the name of the current tab, if any if ( m_tabWidget->currentIndex() != -1 ) { tab = projectTabAt( m_tabWidget->currentIndex() ); Project *tabProject = tab->project(); const ProjectModelItem::Type type = ProjectModelItem::projectItemTypeFromTabType( tab->type() ); #ifdef BUILD_PROVIDER_TYPE_SCRIPT if ( tabProject->data()->type() == Enums::ScriptedProvider ) { if ( type != ProjectModelItem::ScriptItem || tab->fileName() == tabProject->scriptFileName() ) { // Get caption from the model by tab type, but not for external script tabs caption = m_projectModel->projectItemChildFromProject( tabProject, type )->text(); } else { // External script tab, manually construct caption QString name = tab->fileName().isEmpty() ? i18nc("@info/plain", "External Script File") : QFileInfo(tab->fileName()).fileName(); caption = KDialog::makeStandardCaption( name, 0, tab && tab->isModified() ? KDialog::ModifiedCaption : KDialog::NoCaptionFlags ); } } else { caption = m_projectModel->projectItemChildFromProject( tabProject, type )->text(); } #else caption = m_projectModel->projectItemChildFromProject( tabProject, type )->text(); #endif // Add project name caption += " - " + tabProject->projectName(); } // Add information about the test state if ( activeProject ) { if ( activeProject->isTestRunning() ) { caption += " - " + i18nc("@info/plain", "Testing"); } #ifdef BUILD_PROVIDER_TYPE_SCRIPT // Add information about the debugger state Debugger::Debugger *debugger = activeProject->debugger(); if ( debugger ) { if ( debugger->hasUncaughtException() ) { caption += " - " + i18nc("@info/plain", "Debugging (Exception in Line %1)", debugger->uncaughtExceptionLineNumber()); } else if ( debugger->isInterrupted() ) { caption += " - " + i18nc("@info/plain", "Debugger Interrupted at Line %1", debugger->lineNumber()); } else if ( debugger->isRunning() ) { caption += " - " + i18nc("@info/plain", "Debugger Running"); } } #endif } setCaption( caption, tab ? tab->isModified() : false ); } void TimetableMate::activePartChanged( KParts::Part *part ) { // Merge the GUI of the part, do not update while merging to avoid flicker setUpdatesEnabled( false ); createGUI( part ); setUpdatesEnabled( true ); if ( part ) { // Manually hide actions of the part QStringList actionsToHide; actionsToHide << "tools_mode" << "tools_highlighting" << "tools_indentation"; foreach ( QAction *action, menuBar()->actions() ) { KActionMenu *menuAction = static_cast( action ); const QList< QAction* > actions = menuAction->menu()->actions(); for ( int i = actions.count() - 1; i >= 0; --i ) { QAction *curAction = actions.at( i ); if ( curAction->parent() == actionCollection() ) continue; // Don't hide own actions if ( actionsToHide.contains(curAction->objectName()) ) { curAction->setVisible( false ); actionsToHide.removeAt( i ); if ( actionsToHide.isEmpty() ) break; } } if ( actionsToHide.isEmpty() ) { break; } } } fixMenus(); } AbstractTab *TimetableMate::projectTabAt( int index ) { return qobject_cast< AbstractTab* >( m_tabWidget->widget(index) ); } bool TimetableMate::closeProject( Project *project ) { if ( closeAllTabs(project) ) { if ( project->isModified() ) { const QString message = i18nc("@info", "The project '%1' was modified. " "Do you want to save it now?", project->projectName()); const int result = KMessageBox::warningYesNoCancel( this, message, QString(), KStandardGuiItem::save(), KStandardGuiItem::close() ); if ( result == KMessageBox::Yes ) { // Save clicked project->save( this ); m_projectModel->removeProject( project ); return !project->isModified(); } else if ( result == KMessageBox::No ) { // Close clicked m_projectModel->removeProject( project ); return true; } else { // Cancel clicked return false; } } else { m_projectModel->removeProject( project ); } return true; } else { return false; } } bool TimetableMate::closeAllProjects() { QStringList modifiedProjects; QList projects = m_projectModel->projectItems(); foreach ( const ProjectModelItem *project, projects ) { if ( project->project()->isModified() ) { modifiedProjects << project->project()->projectName(); } } if ( modifiedProjects.isEmpty() ) { // No modified projects return true; } const QString message = i18nc("@info", "The following projects were modified. " "Do you want to save them now?"); const int result = KMessageBox::warningYesNoCancelList( this, message, modifiedProjects, i18nc("@title:window", "Modified Projects"), KStandardGuiItem::save(), KStandardGuiItem::close() ); if ( result == KMessageBox::Yes ) { // Save clicked foreach ( const ProjectModelItem *projectItem, projects ) { if ( projectItem->project()->isModified() ) { projectItem->project()->save( this ); if ( projectItem->project()->isModified() ) { // Still modified, error while saving return false; } } closeAllTabs( projectItem->project(), false ); m_projectModel->removeProject( projectItem->project() ); } return true; } else if ( result == KMessageBox::No ) { // Close clicked closeAllTabs( 0, false ); foreach ( const ProjectModelItem *projectItem, projects ) { m_projectModel->removeProject( projectItem->project() ); } return true; } else { // Cancel clicked return false; } } void TimetableMate::contextMenuEvent( QContextMenuEvent *event ) { // Show "Shown Docks" action menu for context menus in empty menu bar space // and in main window splitters m_showDocksAction->menu()->exec( event->globalPos() ); } void TimetableMate::tabContextMenu( QWidget *widget, const QPoint &pos ) { AbstractTab *tab = qobject_cast< AbstractTab* >( widget ); tab->showTabContextMenu( pos ); } void TimetableMate::closeCurrentTab() { closeTab( projectTabAt(m_tabWidget->currentIndex()) ); } void TimetableMate::closeTab( AbstractTab *tab ) { if ( !tab ) { kDebug() << "Tab was already closed"; return; } if ( tab->isModified() ) { // Ask if modifications should be saved AbstractDocumentTab *documentTab = qobject_cast< AbstractDocumentTab* >( tab ); const QString message = documentTab ? i18nc("@info", "The document was modified. Do you want to save it now?") : i18nc("@info", "Tab contents were modified. Do you want to save it now?"); int result = KMessageBox::warningYesNoCancel( this, message, QString(), KStandardGuiItem::save(), documentTab ? KStandardGuiItem::closeDocument() : KStandardGuiItem::close() ); if ( result == KMessageBox::Yes ) { if ( !tab->save() ) { // Do not close the tab if modifications could not be saved return; } } else if ( result == KMessageBox::Cancel ) { // Cancel clicked, do not close the tab return; } // else: No clicked, ie. do not save, but close the tab } switch ( tab->type() ) { case Tabs::Dashboard: dashboardTabAction( qobject_cast(tab), CloseTab ); break; case Tabs::ProjectSource: projectSourceTabAction( qobject_cast(tab), CloseTab ); break; case Tabs::PlasmaPreview: plasmaPreviewTabAction( qobject_cast(tab), CloseTab ); break; case Tabs::Web: webTabAction( qobject_cast(tab), CloseTab ); break; #ifdef BUILD_PROVIDER_TYPE_SCRIPT case Tabs::Script: scriptTabAction( qobject_cast(tab), CloseTab ); break; #endif case Tabs::NoTab: default: break; } // Close the tab m_tabWidget->removeTab( m_tabWidget->indexOf(tab) ); tab->deleteLater(); } bool TimetableMate::closeAllTabsExcept( AbstractTab *tab, bool ask ) { return closeAllTabsExcept( 0, tab, ask ); } bool TimetableMate::closeAllTabs( Project *project, bool ask ) { return closeAllTabsExcept( project, 0, ask ); } bool TimetableMate::closeAllTabsExcept( Project *project, AbstractTab *except, bool ask ) { // Check for tabs with modified content documents QHash< QString, AbstractTab* > modifiedDocuments; for ( int i = 0; i < m_tabWidget->count(); ++i ) { AbstractTab *tab = projectTabAt( i ); if ( (project && tab->project() != project) || (except && except == tab) ) { // If a project is given as argument only close tabs of that project and skip others // If a tab is given do not close that close continue; } if ( tab->isModified() ) { // Tab contents are modified, get a unique name const QString baseDocumentName = tab->fileName(); QString documentName = baseDocumentName; int i = 1; if ( modifiedDocuments.contains(documentName) ) { documentName = QString("%1 (%2)").arg(baseDocumentName).arg(i); } modifiedDocuments.insert( documentName, tab ); } else { // Tab contents unchanged, just close it m_tabWidget->removeTab( i ); // Use deleteLater() because otherwise closing a tab from inside itself would cause // crash (also closing the project from inside a tab, because that closes the tab, too) tab->deleteLater(); --i; } } // Unmodified tabs are now closed, check if modified tabs were found if ( modifiedDocuments.isEmpty() ) { return true; } else if ( ask ) { // Ask the user if modified documents should be saved const QString message = i18nc("@info", "The following documents were modified. " "Do you want to save them now?"); KGuiItem saveAllItem( i18nc("@info/plain", "Save All"), KIcon("document-save-all") ); KGuiItem doNotSaveItem( i18nc("@info/plain", "Do not Save"), KIcon("user-trash") ); KGuiItem doNotCloseItem( i18nc("@info/plain", "Do not Close"), KIcon("dialog-cancel") ); KDialog dialog( this ); int result = KMessageBox::warningYesNoCancelList( this, message, modifiedDocuments.keys(), QString(), saveAllItem, doNotSaveItem, doNotCloseItem ); if ( result == KMessageBox::Cancel ) { // "Do not Close" clicked return false; } else if ( result == KMessageBox::Yes ) { // "Save All" clicked bool allTabsClosed = true; for ( QHash::ConstIterator it = modifiedDocuments.constBegin(); it != modifiedDocuments.constEnd(); ++it ) { if ( !(*it)->save() ) { // Document could not be saved (eg. cancelled by the user), // do not close the associated tab and return false at the end allTabsClosed = false; continue; } // Document successfully saved, close the tab m_tabWidget->removeTab( m_tabWidget->indexOf(*it) ); delete *it; } // Return if all tabs are now closed return allTabsClosed; } else { // "Do not Save" clicked, close all tabs without saving for ( QHash::ConstIterator it = modifiedDocuments.constBegin(); it != modifiedDocuments.constEnd(); ++it ) { // Close the tab m_tabWidget->removeTab( m_tabWidget->indexOf(*it) ); delete *it; } return true; } } else { return false; } } QAction *TimetableMate::qmlAction( const QString &name ) const { return action( name.toAscii().constData() ); } QStringList TimetableMate::recentProjects() const { QStringList projects; foreach ( const QUrl &url, m_recentFilesAction->urls() ) { // Make filenames more pretty QString prettyName; const QString fileName = url.fileName(); if ( KStandardDirs::checkAccess(url.path(), W_OK) ) { // File is writable, ie. locally installed prettyName = QFileInfo( url.path() ).baseName(); } else { // File isn't writable, ie. globally installed prettyName = i18nc("@info/plain This string is displayed instead of the full path for " "globally installed service provider plugins.", "Global: %1", QFileInfo(url.path()).baseName()); } projects << prettyName; } return projects; } QStringList TimetableMate::recentUrls() const { QStringList urls; foreach ( const QUrl &url, m_recentFilesAction->urls() ) { urls << url.path(); } return urls; } bool TimetableMate::hasOpenedProjects() const { return m_projectModel->rowCount() > 0; } void TimetableMate::showDock( const QString &dockName, bool show ) { AbstractDockWidget *dock; if ( dockName == m_documentationDock->objectName() ) { dock = m_documentationDock; } else if ( dockName == m_projectsDock->objectName() ) { dock = m_projectsDock; } else if ( dockName == m_testDock->objectName() ) { dock = m_testDock; } else if ( dockName == m_webInspectorDock->objectName() ) { dock = m_webInspectorDock; } else if ( dockName == m_networkMonitorDock->objectName() ) { dock = m_networkMonitorDock; #ifdef BUILD_PROVIDER_TYPE_SCRIPT } else if ( dockName == m_backtraceDock->objectName() ) { dock = m_backtraceDock; } else if ( dockName == m_consoleDock->objectName() ) { dock = m_consoleDock; } else if ( dockName == m_outputDock->objectName() ) { dock = m_outputDock; } else if ( dockName == m_breakpointDock->objectName() ) { dock = m_breakpointDock; } else if ( dockName == m_variablesDock->objectName() ) { dock = m_variablesDock; #endif } else { qWarning() << "Unknown dock" << dockName; return; } dock->setVisible( show ); if ( show ) { dock->mainWidget()->setFocus(); } } void TimetableMate::showDocumentation( const QString &key ) { m_documentationDock->showDocumentation( key ); m_documentationDock->show(); m_documentationDock->mainWidget()->setFocus(); } void TimetableMate::currentTabChanged( int index ) { AbstractTab *tab = index == -1 ? 0 : qobject_cast(m_tabWidget->widget(index)); if ( tab ) { // Go to project source or script tab if ( tab->isProjectSourceTab() || tab->isScriptTab() ) { AbstractDocumentTab *documentTab = qobject_cast< AbstractDocumentTab* >( tab ); Q_ASSERT( documentTab ); if ( documentTab ) { if ( !m_partManager->parts().contains(documentTab->document()) ) { m_partManager->addPart( documentTab->document() ); } documentTab->document()->activeView()->setFocus(); } } // Remove QML view if ( m_qmlView ) { m_mainLayout->removeWidget( m_qmlView ); m_mainLayout->removeItem( m_messageWidgetLayout ); m_mainLayout->addWidget( m_tabWidget ); m_mainLayout->addLayout( m_messageWidgetLayout ); m_qmlView->deleteLater(); m_qmlView = 0; } } else { m_partManager->setActivePart( 0 ); if ( index == -1 && !m_qmlView ) { m_qmlView = new QDeclarativeView( this ); m_qmlView->setResizeMode( QDeclarativeView::SizeRootObjectToView ); // Install a KDeclarative instance to allow eg. QIcon("icon"), i18n("translate") KDeclarative *kdeclarative = new KDeclarative(); kdeclarative->setDeclarativeEngine( m_qmlView->engine() ); kdeclarative->initialize(); kdeclarative->setupBindings(); m_qmlView->rootContext()->setContextProperty( "timetableMate", this ); // Expose the name of the SVG to use - const QString svgFileName = KGlobal::dirs()->findResource( "data", "timetablemate/dashboard.svg" ); + const QString svgFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "timetablemate/dashboard.svg" ); m_qmlView->rootContext()->setContextProperty( "svgFileName", svgFileName ); // Add Plasma QML import paths const QStringList importPaths = KGlobal::dirs()->findDirs( "module", "imports" ); foreach( const QString &importPath, importPaths ) { m_qmlView->engine()->addImportPath( importPath ); } // Find the QML file used for the timetablemate dashboard - const QString fileName = KGlobal::dirs()->findResource( "data", "timetablemate/timetablemate.qml" ); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "timetablemate/timetablemate.qml" ); if ( fileName.isEmpty() ) { qWarning() << "timetablemate.qml not found! Check installation"; } else { m_qmlView->setSource( fileName ); } m_mainLayout->removeWidget( m_tabWidget ); m_mainLayout->removeItem( m_messageWidgetLayout ); m_mainLayout->addWidget( m_qmlView ); m_mainLayout->addLayout( m_messageWidgetLayout ); } } // Adjust if a dashboard tab was left or newly shown const bool leftDashboardTab = m_currentTab && m_currentTab->isDashboardTab(); const bool movedToDashboardTab = tab && tab->isDashboardTab(); if ( leftDashboardTab && !movedToDashboardTab ) { dashboardTabAction( qobject_cast(m_currentTab), LeaveTab ); } else if ( movedToDashboardTab && !leftDashboardTab ) { dashboardTabAction( qobject_cast(tab), MoveToTab ); } // Adjust if a project source tab was left or newly shown const bool leftProjectSourceTab = m_currentTab && m_currentTab->isProjectSourceTab(); const bool movedToProjectSourceTab = tab && tab->isProjectSourceTab(); if ( leftProjectSourceTab && !movedToProjectSourceTab ) { projectSourceTabAction( qobject_cast(m_currentTab), LeaveTab ); } else if ( movedToProjectSourceTab && !leftProjectSourceTab ) { projectSourceTabAction( qobject_cast(tab), MoveToTab ); } // Adjust if a plasma preview tab was left or newly shown const bool leftPlasmaPreviewTab = m_currentTab && m_currentTab->isPlasmaPreviewTab(); const bool movedToPlasmaPreviewTab = tab && tab->isPlasmaPreviewTab(); if ( leftPlasmaPreviewTab && !movedToPlasmaPreviewTab ) { plasmaPreviewTabAction( qobject_cast(m_currentTab), LeaveTab ); } else if ( movedToPlasmaPreviewTab && !leftPlasmaPreviewTab ) { plasmaPreviewTabAction( qobject_cast(tab), MoveToTab ); } #ifdef BUILD_PROVIDER_TYPE_SCRIPT // Adjust if a script tab was left or newly shown const bool leftScriptTab = m_currentTab && m_currentTab->isScriptTab(); const bool movedToScriptTab = tab && tab->isScriptTab(); if ( leftScriptTab && !movedToScriptTab ) { scriptTabAction( qobject_cast(m_currentTab), LeaveTab ); } else if ( movedToScriptTab && !leftScriptTab ) { scriptTabAction( qobject_cast(tab), MoveToTab ); } #endif #ifdef BUILD_PROVIDER_TYPE_GTFS // Adjust if a GTFS database tab was left or newly shown const bool leftGtfsDatabaseTab = m_currentTab && m_currentTab->isGtfsDatabaseTab(); const bool movedToGtfsDatabaseTab = tab && tab->isGtfsDatabaseTab(); if ( leftGtfsDatabaseTab && !movedToGtfsDatabaseTab ) { gtfsDatabaseTabAction( qobject_cast(m_currentTab), LeaveTab ); } else if ( movedToGtfsDatabaseTab && !leftGtfsDatabaseTab ) { gtfsDatabaseTabAction( qobject_cast(tab), MoveToTab ); } #endif // Adjust if a web tab was left or newly shown const bool leftWebTab = m_currentTab && m_currentTab->isWebTab(); const bool movedToWebTab = tab && tab->isWebTab(); if ( leftWebTab && !movedToWebTab ) { webTabAction( qobject_cast(m_currentTab), LeaveTab ); } else if ( movedToWebTab && !leftWebTab ) { webTabAction( qobject_cast(tab), MoveToTab ); } if ( movedToWebTab ) { WebTab *webTab = qobject_cast< WebTab* >( tab ); if ( m_webInspectorDock ) { m_webInspectorDock->setWebTab( webTab ); } } else if ( leftWebTab ) { WebTab *webTab = qobject_cast< WebTab* >( m_currentTab ); if ( m_webInspectorDock && m_webInspectorDock->webInspector()->page() == webTab->webView()->page() ) { // The web tab that was closed was connected to the web inspector dock widget m_webInspectorDock->setWebTab( 0 ); } } // Store new tab and update window title m_currentTab = tab; updateWindowTitle(); } void TimetableMate::dashboardTabAction( DashboardTab *dashboardTab, TimetableMate::TabAction tabAction ) { Q_UNUSED( dashboardTab ); Q_UNUSED( tabAction ); } void TimetableMate::projectSourceTabAction( ProjectSourceTab *projectSourceTab, TimetableMate::TabAction tabAction ) { if ( tabAction == CloseTab ) { m_partManager->removePart( projectSourceTab->document() ); } } #ifdef BUILD_PROVIDER_TYPE_SCRIPT void TimetableMate::scriptTabAction( ScriptTab *scriptTab, TimetableMate::TabAction tabAction ) { if ( tabAction == MoveToTab ) { stateChanged( "script_tab_is_active" ); connect( scriptTab, SIGNAL(canGoToPreviousFunctionChanged(bool)), action("script_previous_function"), SLOT(setEnabled(bool)) ); connect( scriptTab, SIGNAL(canGoToNextFunctionChanged(bool)), action("script_next_function"), SLOT(setEnabled(bool)) ); } else if ( tabAction == LeaveTab ) { stateChanged( "script_tab_is_active", StateReverse ); disconnect( scriptTab, SIGNAL(canGoToPreviousFunctionChanged(bool)), action("script_previous_function"), SLOT(setEnabled(bool)) ); disconnect( scriptTab, SIGNAL(canGoToNextFunctionChanged(bool)), action("script_next_function"), SLOT(setEnabled(bool)) ); } else if ( tabAction == CloseTab ) { m_partManager->removePart( scriptTab->document() ); } } #endif // BUILD_PROVIDER_TYPE_SCRIPT #ifdef BUILD_PROVIDER_TYPE_GTFS void TimetableMate::gtfsDatabaseTabAction( GtfsDatabaseTab *gtfsDatabaseTab, TimetableMate::TabAction tabAction ) { Q_UNUSED( tabAction ); Q_ASSERT( gtfsDatabaseTab ); } #endif // BUILD_PROVIDER_TYPE_GTFS void TimetableMate::plasmaPreviewTabAction( PlasmaPreviewTab *plasmaPreviewTab, TimetableMate::TabAction tabAction ) { Q_UNUSED( tabAction ); Q_ASSERT( plasmaPreviewTab ); } void TimetableMate::webTabAction( WebTab *webTab, TimetableMate::TabAction tabAction ) { if ( m_webInspectorDock ) { if ( tabAction == MoveToTab ) { m_webInspectorDock->setWebTab( webTab ); webTab->urlBar()->setFocus(); } else if ( tabAction == CloseTab ) { if ( m_webInspectorDock->webInspector()->page() == webTab->webView()->page() ) { // The web tab that was closed was connected to the web inspector dock widget m_webInspectorDock->setWebTab( 0 ); } } } } void TimetableMate::setupActions() { KAction *newProject = new KAction( KIcon("project-development-new-template"), i18nc("@action", "New Project"), this ); newProject->setPriority( QAction::LowPriority ); KAction *openProject = new KAction( KIcon("project-open"), i18nc("@action", "Open Project"), this ); openProject->setPriority( QAction::LowPriority ); actionCollection()->addAction( QLatin1String("project_new"), newProject ); actionCollection()->addAction( QLatin1String("project_open"), openProject ); connect( newProject, SIGNAL(triggered(bool)), this, SLOT(fileNew()) ); connect( openProject, SIGNAL(triggered(bool)), this, SLOT(fileOpen()) ); KAction *saveAllProjects = new KAction( KIcon("document-save-all"), i18nc("@action", "Save All"), this ); actionCollection()->addAction( QLatin1String("project_save_all"), saveAllProjects ); connect( saveAllProjects, SIGNAL(triggered(bool)), this, SLOT(fileSaveAll()) ); KStandardAction::quit( qApp, SLOT(closeAllWindows()), actionCollection() ); KStandardAction::preferences( this, SLOT(optionsPreferences()), actionCollection() ); m_recentFilesAction = KStandardAction::openRecent( this, SLOT(open(QUrl)), actionCollection() ); actionCollection()->addAction( QLatin1String("project_open_recent"), m_recentFilesAction ); KAction *openInstalled = new KAction( KIcon("document-open"), i18nc("@action", "Open I&nstalled..."), this ); actionCollection()->addAction( QLatin1String("project_open_installed"), openInstalled ); connect( openInstalled, SIGNAL(triggered(bool)), this, SLOT(fileOpenInstalled()) ); KAction *fetch = new KAction( KIcon("svn-update"), i18nc("@action", "&Fetch Project..."), this ); actionCollection()->addAction( QLatin1String("project_fetch"), fetch ); connect( fetch, SIGNAL(triggered(bool)), this, SLOT(fetchProject()) ); KSelectAction *chooseActiveProject = new KSelectAction( KIcon("edit-select"), i18nc("@action", "&Active Project"), this ); actionCollection()->addAction( QLatin1String("project_choose_active"), chooseActiveProject ); QToolButton *activeProjectButton = new QToolButton( this ); activeProjectButton->setDefaultAction( chooseActiveProject ); activeProjectButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); activeProjectButton->setPopupMode( QToolButton::InstantPopup ); menuBar()->setCornerWidget( activeProjectButton ); #ifdef BUILD_PROVIDER_TYPE_SCRIPT // TODO Move to Project? => Project::ProjectAction KAction *scriptNextFunction = ScriptTab::createNextFunctionAction( this ); actionCollection()->addAction( QLatin1String("script_next_function"), scriptNextFunction ); scriptNextFunction->setVisible( false ); connect( scriptNextFunction, SIGNAL(triggered(bool)), this, SLOT(scriptNextFunction()) ); KAction *scriptPreviousFunction = ScriptTab::createPreviousFunctionAction( this ); actionCollection()->addAction( QLatin1String("script_previous_function"), scriptPreviousFunction ); scriptPreviousFunction->setVisible( false ); connect( scriptPreviousFunction, SIGNAL(triggered(bool)), this, SLOT(scriptPreviousFunction()) ); #endif // Add project actions, they get connected to the currently active project // in activeProjectAboutToChange() foreach ( Project::ProjectAction projectAction, externProjectActions() ) { actionCollection()->addAction( Project::projectActionName(projectAction), Project::createProjectAction(projectAction, this) ); } KAction *tabNext = new KAction( KIcon("go-next"), i18nc("@action", "Go to &Next Tab"), this ); tabNext->setShortcut( KStandardShortcut::tabNext() ); connect( tabNext, SIGNAL(triggered()), this, SLOT(tabNextActionTriggered()) ); actionCollection()->addAction( QLatin1String("tab_next"), tabNext ); KAction *tabPrevious = new KAction( KIcon("go-previous"), i18nc("@action", "Go to &Previous Tab"), this ); tabPrevious->setShortcut( KStandardShortcut::tabPrev() ); connect( tabPrevious, SIGNAL(triggered()), this, SLOT(tabPreviousActionTriggered()) ); actionCollection()->addAction( QLatin1String("tab_previous"), tabPrevious ); KAction *tabClose = new KAction( KIcon("tab-close"), i18nc("@action", "&Close Tab"), this ); tabClose->setShortcut( KStandardShortcut::close() ); connect( tabClose, SIGNAL(triggered()), this, SLOT(closeCurrentTab()) ); actionCollection()->addAction( QLatin1String("tab_close"), tabClose ); KAction *runAllTestsAction = new KAction( KIcon("task-complete"), i18nc("@action", "Test all &Projects"), this ); connect( m_projectModel, SIGNAL(idleChanged(bool)), runAllTestsAction, SLOT(setEnabled(bool)) ); connect( runAllTestsAction, SIGNAL(triggered()), m_projectModel, SLOT(testAllProjects()) ); actionCollection()->addAction( QLatin1String("test_all_projects"), runAllTestsAction ); } void TimetableMate::tabNextActionTriggered() { if ( m_tabWidget->currentIndex() + 1 < m_tabWidget->count() ) { m_tabWidget->setCurrentIndex( m_tabWidget->currentIndex() + 1 ); } else if ( m_tabWidget->count() > 1 ) { // Was at last tab, go to the first tab m_tabWidget->setCurrentIndex( 0 ); } } void TimetableMate::tabPreviousActionTriggered() { if ( m_tabWidget->currentIndex() - 1 >= 0 ) { m_tabWidget->setCurrentIndex( m_tabWidget->currentIndex() - 1 ); } else if ( m_tabWidget->count() > 1 ) { // Was at first tab, go to the last tab m_tabWidget->setCurrentIndex( m_tabWidget->count() - 1 ); } } #ifdef BUILD_PROVIDER_TYPE_SCRIPT void TimetableMate::toggleBreakpoint() { ScriptTab *scriptTab = qobject_cast< ScriptTab* >( m_currentTab ); if ( scriptTab ) { scriptTab->toggleBreakpoint(); } } void TimetableMate::scriptPreviousFunction() { ScriptTab *scriptTab = qobject_cast< ScriptTab* >( m_currentTab ); if ( scriptTab ) { scriptTab->goToPreviousFunction(); } } void TimetableMate::scriptNextFunction() { ScriptTab *scriptTab = qobject_cast< ScriptTab* >( m_currentTab ); if ( scriptTab ) { scriptTab->goToNextFunction(); } } void TimetableMate::breakpointReached( const Breakpoint &breakpoint ) { infoMessage( i18nc("@info/plain", "Reached breakpoint at %1", breakpoint.lineNumber()), KMessageWidget::Information ); } void TimetableMate::uncaughtException( int lineNumber, const QString &errorMessage, const QString &fileName ) { // Do not show messages while testing, results are shown in the test dock if ( m_projectModel->activeProject()->suppressMessages() ) { return; } infoMessage( i18nc("@info", "Uncaught exception in %1 at %2: " "%3", QFileInfo(fileName).fileName(), lineNumber, errorMessage), KMessageWidget::Error, -1 ); } #endif // BUILD_PROVIDER_TYPE_SCRIPT void TimetableMate::testStarted() { if ( m_testDock ) { m_testDock->show(); } updateWindowTitle(); } void TimetableMate::testFinished( bool success ) { Q_UNUSED( success ); updateWindowTitle(); } void TimetableMate::fileNew() { const Enums::ServiceProviderType providerType = chooseProviderPluginType(); if ( providerType == Enums::InvalidProvider ) { return; // Canceled } // Create the new project Project *newProject = new Project( providerType, m_projectModel->weaver(), this ); newProject->loadProject(); m_projectModel->appendProject( newProject ); connect( newProject, SIGNAL(showDocumentationRequest(QString)), this, SLOT(showDocumentation(QString)) ); // Make the new project the currently active one newProject->setAsActiveProject(); // Show settings of the project newProject->showSettingsDialog( this ); // Show the dashboard of the new project newProject->showDashboardTab(); // Expand new project item in the projects dock m_projectsDock->projectsWidget()->expand( m_projectModel->indexFromProject(newProject) ); } Enums::ServiceProviderType TimetableMate::chooseProviderPluginType() { if ( ServiceProviderGlobal::availableProviderTypes().count() == 1 ) { // Only one provider plugin type is available, no need to ask the user return ServiceProviderGlobal::availableProviderTypes().first(); } // Create a dialog with a combo box to choose a provider plugin type QPointer< KDialog > dialog = new KDialog( this ); QWidget *container = new QWidget( dialog ); KComboBox *pluginType = new KComboBox( container ); foreach ( Enums::ServiceProviderType type, ServiceProviderGlobal::availableProviderTypes() ) { pluginType->addItem( ServiceProviderGlobal::typeName(type), static_cast(type) ); } QLabel *infoLabel = new QLabel( container ); infoLabel->setWordWrap( true ); infoLabel->setText( i18nc("@info:whatsthis", "Create New Provider Plugin" "To create a new provider plugin first choose the type of the plugin. " "Currently Scripted and GTFS " "(since version 0.11) types are available. Your version of TimetableMate may be " "compiled without support for one of these types." "The Scripted type can be used for a variety of providers " "publishing their timetable data in web sites. Helper functions are provided to easily " "parse HTML sites for timetable data in the script (QtScript, ie. JavaScript, by using " "the Kross script extension Python and Ruby are also available). The script extension " "\"qt.xml\" helps parsing XML data sources. For providers using the HAFAS API there " "is a base script available, use include( \"base_hafas.js\" );" "If a GTFS feed is available a provider plugin can be " "created much easier, ie. without writing a script. Only the URL to the GTFS feed is " "needed. GTFS-realtime TripUpdates and Alerts URLs can also be " "specified.") ); QFormLayout *layout = new QFormLayout( container ); layout->setContentsMargins( 4, 4, 4, 48 ); layout->addRow( infoLabel ); layout->addItem( new QSpacerItem(12, 12, QSizePolicy::Preferred, QSizePolicy::Preferred) ); layout->addRow( pluginType ); layout->addItem( new QSpacerItem(12, 12, QSizePolicy::Preferred, QSizePolicy::Preferred) ); dialog->setMainWidget( container ); dialog->setWindowTitle( i18nc("@info/plain", "Choose Provider Plugin Type") ); dialog->setButtonText( KDialog::Ok, i18nc("@info/plain", "Create Provider Plugin") ); // Show the dialog const int result = dialog->exec(); // Get selected provider type const Enums::ServiceProviderType type = static_cast< Enums::ServiceProviderType >( pluginType->itemData(pluginType->currentIndex()).toInt() ); // Cleanup delete dialog.data(); return result == KDialog::Accepted ? type : Enums::InvalidProvider; } void TimetableMate::projectAdded( Project *project ) { // Connect new project connect( project, SIGNAL(tabTitleChanged(QWidget*,QString,QIcon)), this, SLOT(tabTitleChanged(QWidget*,QString,QIcon)) ); connect( project, SIGNAL(testStarted()), this, SLOT(removeAllMessageWidgets()) ); connect( project, SIGNAL(informationMessage(QString,KMessageWidget::MessageType,int,QString,QString,QList)), this, SLOT(infoMessage(QString,KMessageWidget::MessageType,int,QString,QString,QList)) ); connect( project, SIGNAL(closeRequest()), this, SLOT(projectCloseRequest()) ); connect( project, SIGNAL(tabCloseRequest(AbstractTab*)), this, SLOT(closeTab(AbstractTab*)) ); connect( project, SIGNAL(otherTabsCloseRequest(AbstractTab*)), this, SLOT(closeAllTabsExcept(AbstractTab*)) ); connect( project, SIGNAL(tabOpenRequest(AbstractTab*)), this, SLOT(tabOpenRequest(AbstractTab*)) ); connect( project, SIGNAL(tabGoToRequest(AbstractTab*)), this, SLOT(tabGoToRequest(AbstractTab*)) ); connect( project, SIGNAL(saveLocationChanged(QString,QString)), this, SLOT(projectSaveLocationChanged(QString,QString)) ); KSelectAction *chooseActiveProject = qobject_cast< KSelectAction* >( action("project_choose_active") ); if ( chooseActiveProject ) { // Create "Set as Active Project" action and use the project name/icon for it // instead of the default, which would mean that the chooseActiveProject action would // contain multiple actions with the same text/icon QAction *action = project->createProjectAction( Project::SetAsActiveProject, chooseActiveProject ); action->setText( project->projectName() ); action->setIcon( project->projectIcon() ); // Store a pointer to the project in the action, // to be able to find the action for a specific project in the select action action->setData( QVariant::fromValue(static_cast(project)) ); // Connect action with the project project->connectProjectAction( Project::SetAsActiveProject, action ); // Add action to make the project active to the select action chooseActiveProject->addAction( action ); } emit hasOpenedProjectsChanged( true ); } void TimetableMate::projectAboutToBeRemoved( Project *project ) { KSelectAction *chooseActiveProject = qobject_cast< KSelectAction* >( action("project_choose_active") ); if ( chooseActiveProject ) { // Search for the action associated with the given project foreach ( QAction *action, chooseActiveProject->actions() ) { // Read pointer to the associated project from the action's data Project *associatedProject = static_cast< Project* >( action->data().value() ); if ( associatedProject == project ) { // Found the action associated with the given project, remove it chooseActiveProject->removeAction( action ); break; } } } if ( m_projectModel->rowCount() == 0 ) { emit hasOpenedProjectsChanged( false ); } } void TimetableMate::projectSaveLocationChanged( const QString &newXmlFilePath, const QString &oldXmlFilePath ) { Q_UNUSED( oldXmlFilePath ); if ( newXmlFilePath.isEmpty() ) { m_recentFilesAction->addUrl( newXmlFilePath ); } } void TimetableMate::removeAllMessageWidgets() { // Hide the widget and then delete it (give 1 second for the hide animation) while ( !m_messageWidgets.isEmpty() ) { QPointer< KMessageWidget > messageWidget = m_messageWidgets.dequeue(); if ( messageWidget.data() ) { messageWidget->animatedHide(); QTimer::singleShot( 1000, messageWidget, SLOT(deleteLater()) ); } } } void TimetableMate::infoMessage( const QString &message, KMessageWidget::MessageType type, int timeout, const QString &messageGroup, const QString &resolveMessage, QList actions ) { // Do not show messages from inactive projects, except for error messages Project *project = qobject_cast< Project* >( sender() ); if ( project && !project->isActiveProject() && type != KMessageWidget::Error ) { return; } if ( !m_messageWidgets.isEmpty() ) { QPointer< KMessageWidget > messageWidget = m_messageWidgets.last(); while ( !messageWidget.data() ) { m_messageWidgets.removeOne( messageWidget ); if ( m_messageWidgets.isEmpty() ) { messageWidget = 0; break; } messageWidget = m_messageWidgets.last(); } if ( messageWidget && messageWidget->messageType() == type && messageWidget->text() == message ) { // The same message was just added return; } } if ( !replaceMessage(messageGroup, message, resolveMessage) ) { // Create a new KMessageWidget QPointer< KMessageWidget > messageWidget( new KMessageWidget(message, this) ); messageWidget->setObjectName( messageGroup ); messageWidget->hide(); messageWidget->setCloseButtonVisible( true ); messageWidget->setMessageType( type ); messageWidget->addActions( actions ); if ( message.length() > 60 ) { messageWidget->setWordWrap( true ); } // Install event filter to delete the message widget when it gets hidden messageWidget->installEventFilter( this ); // Add new message widget m_messageWidgetLayout->addWidget( messageWidget ); m_messageWidgets.enqueue( messageWidget ); messageWidget->animatedShow(); // Add a timer to remove the message widget again if ( timeout > 0 ) { hideMessageWidgetLater( messageWidget, timeout ); } // Clear up the message widget queue, if there are too many messages shown const int maxMessageWidgetCount = 3; while ( m_messageWidgets.length() > maxMessageWidgetCount ) { QPointer< KMessageWidget > messageWidget = m_messageWidgets.dequeue(); if ( messageWidget.data() ) { messageWidget.data()->deleteLater(); } } } } uint qHash( const QPointer< KMessageWidget > &messageWidget ) { return qHash( messageWidget.data() ); } void TimetableMate::hideMessageWidgetLater( const QPointer< KMessageWidget > &messageWidget, int timeout ) { QTimer *timer = timerFromMessageWidget( messageWidget ); if ( !timer ) { timer = new QTimer( this ); connect( timer, SIGNAL(timeout()), this, SLOT(removeMessageWidget()) ); timer->setSingleShot( true ); m_autoRemoveMessageWidgets.insert( messageWidget, timer ); } timer->start( timeout ); } QTimer *TimetableMate::timerFromMessageWidget( const QPointer &messageWidget ) { return m_autoRemoveMessageWidgets.value( messageWidget, 0 ); } QPointer TimetableMate::messageWidgetFromTimer( QTimer *timer ) { return m_autoRemoveMessageWidgets.key( timer ); } bool TimetableMate::hasMessagesOfGroup( const QString &messageGroup ) const { QQueue< QPointer >::ConstIterator it = m_messageWidgets.constBegin(); while ( it != m_messageWidgets.constEnd() ) { if ( (*it)->objectName() == messageGroup ) { return true; } ++it; } return false; } void TimetableMate::removeMessages( const QString &messageGroup, const QString &resolveMessage ) { if ( messageGroup.isEmpty() ) { return; } for ( int i = 0; i < m_messageWidgets.count(); ++i ) { const QPointer &messageWidget = m_messageWidgets[i]; if ( messageWidget->objectName() == messageGroup ) { messageWidget->animatedHide(); m_messageWidgets.removeAt( i ); if ( !resolveMessage.isEmpty() ) { infoMessage( resolveMessage, KMessageWidget::Positive, 1000, messageGroup ); } break; // Only one message widget per group } } } bool TimetableMate::replaceMessage( const QString &messageGroup, const QString &newMessage, const QString &resolveMessage ) { if ( newMessage.isEmpty() ) { // No new message text, remove old messages of the group removeMessages( messageGroup, resolveMessage ); return true; } // Update the text shown in the message group QQueue< QPointer >::ConstIterator it = m_messageWidgets.constBegin(); while ( it != m_messageWidgets.constEnd() ) { if ( (*it)->objectName() == messageGroup ) { (*it)->setText( newMessage ); // Restart the timer to hide the message widget hideMessageWidgetLater( *it ); return true; } ++it; } return false; } void TimetableMate::removeMessageWidget() { if ( m_autoRemoveMessageWidgets.isEmpty() ) { return; } // Hide the widget and then delete it (give 1 second for the hide animation) QTimer *timer = qobject_cast< QTimer* >( sender() ); QPointer messageWidget = messageWidgetFromTimer( timer ); if ( !messageWidget ) { qWarning() << "Cannot find message widget associated to timer" << timer; return; } m_messageWidgets.removeOne( messageWidget ); if ( messageWidget.data() ) { messageWidget->animatedHide(); } } bool TimetableMate::eventFilter( QObject *object, QEvent *event ) { KMessageWidget *messageWidget = qobject_cast< KMessageWidget*> ( object ); if ( messageWidget && event->type() == QEvent::Hide ) { // Delete message widgets after they are hidden messageWidget->deleteLater(); } return QObject::eventFilter( object, event ); } void TimetableMate::projectCloseRequest() { Project *project = qobject_cast< Project* >( sender() ); if ( !project ) { qWarning() << "Slot projectCloseRequest() called from wrong sender, " "only class Project is allowed"; return; } closeProject( project ); } void TimetableMate::tabTitleChanged( QWidget *tabWidget, const QString &title, const QIcon &icon ) { const int index = m_tabWidget->indexOf( tabWidget ); if ( index != -1 ) { // Tab widget was already inserted into the main tab bar m_tabWidget->setTabText( index, title ); m_tabWidget->setTabIcon( index, icon ); } } void TimetableMate::closeProject() { closeProject( m_projectModel->activeProject() ); } AbstractTab *TimetableMate::showProjectTab( bool addTab, AbstractTab *tab ) { if ( !tab ) { kDebug() << "No tab object"; return 0; } if ( addTab ) { // Add the tab m_tabWidget->addTab( tab, tab->icon(), tab->title() ); } // Switch to the tab m_tabWidget->setCurrentWidget( tab ); return tab; } void TimetableMate::open( const QUrl &url ) { kDebug() << "OPEN" << url; Project *project = openProject( url.path() ); if ( project ) { project->showDashboardTab( this ); } } Project *TimetableMate::openProject( const QString &filePath ) { Project *openedProject = m_projectModel->projectFromFilePath( filePath ); if ( openedProject ) { return openedProject; } // Create project and try to load it, // before loading it connect to the info message signal for error messages while reading Project *project = new Project( m_projectModel->weaver(), this ); connect( project, SIGNAL(informationMessage(QString,KMessageWidget::MessageType,int,QString,QString,QList)), this, SLOT(infoMessage(QString,KMessageWidget::MessageType,int,QString,QString,QList)) ); project->loadProject( filePath ); disconnect( project, SIGNAL(informationMessage(QString,KMessageWidget::MessageType,int,QString,QString,QList)), this, SLOT(infoMessage(QString,KMessageWidget::MessageType,int,QString,QString,QList)) ); if ( project->state() == Project::ProjectSuccessfullyLoaded ) { if ( !project->filePath().isEmpty() ) { m_recentFilesAction->addUrl( project->filePath() ); } m_projectModel->appendProject( project ); connect( project, SIGNAL(showDocumentationRequest(QString)), this, SLOT(showDocumentation(QString)) ); return project; } else if ( project->state() == Project::ProjectError ) { // The error message was emitted from the constructor of Project infoMessage( project->lastError(), KMessageWidget::Error ); delete project; return 0; } return project; } void TimetableMate::fileOpen() { const QStringList fileNames = KFileDialog::getOpenFileNames( QUrl("kfiledialog:///serviceprovider"), "application/x-publictransport-serviceprovider application/xml", this, i18nc("@title:window", "Open Service Provider Plugin") ); if ( fileNames.isEmpty() ) return; // Cancel clicked foreach ( const QString &fileName, fileNames ) { open( QUrl(fileName) ); // Expand manually opened project item m_projectsDock->projectsWidget()->expand( m_projectModel->index(m_projectModel->rowCount() - 1, 0) ); } } void TimetableMate::fileOpenInstalled() { // Get a list of all service provider plugin files in the directory of the XML file QStringList pluginFiles = ServiceProviderGlobal::installedProviders(); if ( pluginFiles.isEmpty() ) { KMessageBox::information( this, i18nc("@info/plain", "There are no installed " "service provider plugins. You need to install the PublicTransport data engine.") ); return; } qSort( pluginFiles ); // Make filenames more pretty and create a hash to map from the pretty names to the full paths QHash< QString, QString > map; for ( QStringList::iterator it = pluginFiles.begin(); it != pluginFiles.end(); ++it ) { QString prettyName; if ( KStandardDirs::checkAccess(*it, W_OK) ) { // File is writable, ie. locally installed prettyName = QUrl( *it ).fileName(); } else { // File isn't writable, ie. globally installed prettyName = i18nc("@info/plain This string is displayed instead of the full path for " "globally installed service provider plugins.", "Global: %1", QUrl(*it).fileName()); } map.insert( prettyName, *it ); *it = prettyName; } bool ok; QStringList selectedPrettyNames = KInputDialog::getItemList( i18nc("@title:window", "Open Installed Service Provider Plugin"), i18nc("@info", "Installed service provider plugin"), pluginFiles, QStringList(), true, &ok, this ); if ( ok ) { foreach ( const QString &selectedPrettyName, selectedPrettyNames ) { QString selectedFilePath = map[ selectedPrettyName ]; Project *project = openProject( selectedFilePath ); if ( project ) { project->showDashboardTab( this ); } } } } void TimetableMate::fetchProject() { // Show the GHNS download dialog, downloads to .../timetablemate/serviceProviders if ( !m_downloadDialog ) { m_downloadDialog = new KNS3::DownloadDialog( "timetablemate.knsrc", this ); connect( m_downloadDialog, SIGNAL(accepted()), this, SLOT(fetchProjectFinished()) ); } m_downloadDialog->show(); } void TimetableMate::fetchProjectFinished() { if ( !m_downloadDialog ) { qWarning() << "Download dialog already destroyed"; return; } // Dialog is done if ( !m_downloadDialog->installedEntries().isEmpty() ) { // At least one project has been newly installed // Get all provider plugin .pts files that have been installed (not eg. the script files) QStringList installedProviderNames; QStringList installedProviderFiles; foreach ( const KNS3::Entry &installedEntry, m_downloadDialog->installedEntries() ) { // Find the provider plugin .pts file const QStringList installedFiles = installedEntry.installedFiles(); QString providerFile; foreach ( const QString &installedFile, installedFiles ) { if ( installedFile.endsWith(QLatin1String(".pts")) || installedFile.endsWith(QLatin1String(".xml")) ) { providerFile = installedFile; break; } } if ( providerFile.isEmpty() ) { qWarning() << "No provider plugin file found in installed entry" << installedEntry.name(); continue; } installedProviderNames << installedEntry.name(); installedProviderFiles << providerFile; } // Ask if the just installed provider plugins should get opened now const int result = KMessageBox::questionYesNoList( this, i18nc("@info", "The following provider plugins have been fetched. " "Do you want to open them now?"), installedProviderNames ); if ( result == KMessageBox::Yes ) { // Open clicked, open the installed provider plugins foreach ( const QString &providerFile, installedProviderFiles ) { openProject( providerFile ); } } // Inform about fetched providers and show the target directory const QString saveDirectory = - KGlobal::dirs()->saveLocation( "data", "timetablemate/serviceProviders"); + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "timetablemate/serviceProviders"); infoMessage( i18ncp("@info", "%1 provider plugin has been fetched to %2", "%1 provider plugins have been fetched to %2", installedProviderFiles.count(), saveDirectory) ); } delete m_downloadDialog; m_downloadDialog = 0; } void TimetableMate::fileSaveAll() { for ( int row = 0; row < m_projectModel->rowCount(); ++row ) { Project *project = m_projectModel->projectItemFromRow( row )->project(); project->save( this ); } } void TimetableMate::optionsPreferences() { // Avoid to have two dialogs shown if ( !KConfigDialog::showDialog("settings") ) { // Create a new preferences dialog and show it KConfigDialog *dialog = new KConfigDialog( this, "settings", Settings::self() ); QWidget *generalSettings = new QWidget( dialog ); ui_preferences = new Ui::preferences; ui_preferences->setupUi( generalSettings ); dialog->addPage( generalSettings, i18n("General"), "package_settings" ); dialog->setAttribute( Qt::WA_DeleteOnClose ); connect( dialog, SIGNAL(finished()), this, SLOT(preferencesDialogFinished()) ); dialog->show(); } } void TimetableMate::preferencesDialogFinished() { delete ui_preferences; ui_preferences = 0; const int threadCount = Settings::self()->maximumThreadCount(); m_projectModel->weaver()->setMaximumNumberOfThreads( threadCount < 1 ? 32 : threadCount ); } bool TimetableMate::hasHomePageURL( const ServiceProviderData *data ) { if ( data->url().isEmpty() ) { KMessageBox::information( this, i18nc("@info", "The Home Page URL is empty." "Please set it in the project settings dialog first.") ); return false; } else return true; } #include "timetablemate.moc"