diff --git a/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/1.json b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/1.json new file mode 100644 --- /dev/null +++ b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/1.json @@ -0,0 +1,464 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "5c633aea20ff416df784e00a939d7ae5", + "entities": [ + { + "tableName": "gtfs_calendar_dates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `date` TEXT NOT NULL, `exception_type` INTEGER NOT NULL, PRIMARY KEY(`service_id`, `date`), FOREIGN KEY(`service_id`) REFERENCES `gtfs_calendar`(`service_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exceptionType", + "columnName": "exception_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id", + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "gtfs_calendar", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "service_id" + ], + "referencedColumns": [ + "service_id" + ] + } + ] + }, + { + "tableName": "stops_gtfs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stop_id` INTEGER NOT NULL, `stop_code` TEXT NOT NULL, `stop_name` TEXT NOT NULL, `stop_desc` TEXT NOT NULL, `stop_lat` REAL NOT NULL, `stop_lon` REAL NOT NULL, `wheelchair_boarding` TEXT NOT NULL, PRIMARY KEY(`stop_id`))", + "fields": [ + { + "fieldPath": "internalID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gttStopID", + "columnName": "stop_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopName", + "columnName": "stop_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gttPlaceName", + "columnName": "stop_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "stop_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "stop_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wheelchair", + "columnName": "wheelchair_boarding", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stop_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `monday` INTEGER NOT NULL, `tuesday` INTEGER NOT NULL, `wednesday` INTEGER NOT NULL, `thursday` INTEGER NOT NULL, `friday` INTEGER NOT NULL, `saturday` INTEGER NOT NULL, `sunday` INTEGER NOT NULL, `start_date` TEXT NOT NULL, `end_date` TEXT NOT NULL, PRIMARY KEY(`service_id`))", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "onMonday", + "columnName": "monday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onTuesday", + "columnName": "tuesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onWednesday", + "columnName": "wednesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onThursday", + "columnName": "thursday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onFriday", + "columnName": "friday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSaturday", + "columnName": "saturday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSunday", + "columnName": "sunday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "end_date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "routes_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_type` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, `route_sort_order` INTEGER NOT NULL, PRIMARY KEY(`route_id`))", + "fields": [ + { + "fieldPath": "ID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agencyID", + "columnName": "agency_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "route_short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "route_long_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "route_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "route_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "route_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textColor", + "columnName": "route_text_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "route_sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "route_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_stop_times", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`trip_id` TEXT NOT NULL, `arrival_time` TEXT NOT NULL, `departure_time` TEXT NOT NULL, `stop_id` INTEGER NOT NULL, `stop_sequence` INTEGER NOT NULL, PRIMARY KEY(`trip_id`, `stop_id`), FOREIGN KEY(`stop_id`) REFERENCES `stops_gtfs`(`stop_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`trip_id`) REFERENCES `gtfs_trips`(`trip_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrival_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departure_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stopSequence", + "columnName": "stop_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id", + "stop_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_stop_times_stop_id", + "unique": false, + "columnNames": [ + "stop_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_stop_times_stop_id` ON `${TABLE_NAME}` (`stop_id`)" + } + ], + "foreignKeys": [ + { + "table": "stops_gtfs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stop_id" + ], + "referencedColumns": [ + "stop_id" + ] + }, + { + "table": "gtfs_trips", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "trip_id" + ], + "referencedColumns": [ + "trip_id" + ] + } + ] + }, + { + "tableName": "gtfs_trips", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `service_id` TEXT NOT NULL, `trip_id` TEXT NOT NULL, `trip_headsign` TEXT NOT NULL, `direction_id` INTEGER NOT NULL, `block_id` TEXT NOT NULL, `shape_id` TEXT NOT NULL, `wheelchair_accessible` INTEGER NOT NULL, `limited_route` INTEGER NOT NULL, PRIMARY KEY(`trip_id`), FOREIGN KEY(`route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "routeID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripHeadsign", + "columnName": "trip_headsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionID", + "columnName": "direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockID", + "columnName": "block_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isWheelchairAccess", + "columnName": "wheelchair_accessible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLimitedRoute", + "columnName": "limited_route", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_trips_route_id", + "unique": false, + "columnNames": [ + "route_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_route_id` ON `${TABLE_NAME}` (`route_id`)" + } + ], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "gtfs_shapes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shape_id` TEXT NOT NULL, `shape_pt_lat` REAL NOT NULL, `shape_pt_lon` REAL NOT NULL, `shape_pt_sequence` INTEGER NOT NULL, PRIMARY KEY(`shape_id`, `shape_pt_sequence`))", + "fields": [ + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointLat", + "columnName": "shape_pt_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointLon", + "columnName": "shape_pt_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointSequence", + "columnName": "shape_pt_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "shape_id", + "shape_pt_sequence" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5c633aea20ff416df784e00a939d7ae5')" + ] + } +} \ No newline at end of file diff --git a/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/2.json b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/2.json new file mode 100644 --- /dev/null +++ b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/2.json @@ -0,0 +1,648 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6d2aa826894d1e6b1429678e13b65433", + "entities": [ + { + "tableName": "gtfs_feeds", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feed_id` TEXT NOT NULL, PRIMARY KEY(`feed_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "feed_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_agencies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gtfs_id` TEXT NOT NULL, `ag_name` TEXT NOT NULL, `ag_url` TEXT NOT NULL, `fare_url` TEXT, `phone` TEXT, `feed_id` TEXT, PRIMARY KEY(`gtfs_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "ag_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "ag_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fareUrl", + "columnName": "fare_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feed.gtfsId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "gtfs_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_calendar_dates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `date` TEXT NOT NULL, `exception_type` INTEGER NOT NULL, PRIMARY KEY(`service_id`, `date`), FOREIGN KEY(`service_id`) REFERENCES `gtfs_calendar`(`service_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exceptionType", + "columnName": "exception_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id", + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "gtfs_calendar", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "service_id" + ], + "referencedColumns": [ + "service_id" + ] + } + ] + }, + { + "tableName": "stops_gtfs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stop_id` INTEGER NOT NULL, `stop_code` TEXT NOT NULL, `stop_name` TEXT NOT NULL, `stop_desc` TEXT NOT NULL, `stop_lat` REAL NOT NULL, `stop_lon` REAL NOT NULL, `wheelchair_boarding` TEXT NOT NULL, PRIMARY KEY(`stop_id`))", + "fields": [ + { + "fieldPath": "internalID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gttStopID", + "columnName": "stop_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopName", + "columnName": "stop_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gttPlaceName", + "columnName": "stop_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "stop_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "stop_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wheelchair", + "columnName": "wheelchair_boarding", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stop_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `monday` INTEGER NOT NULL, `tuesday` INTEGER NOT NULL, `wednesday` INTEGER NOT NULL, `thursday` INTEGER NOT NULL, `friday` INTEGER NOT NULL, `saturday` INTEGER NOT NULL, `sunday` INTEGER NOT NULL, `start_date` TEXT NOT NULL, `end_date` TEXT NOT NULL, PRIMARY KEY(`service_id`))", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "onMonday", + "columnName": "monday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onTuesday", + "columnName": "tuesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onWednesday", + "columnName": "wednesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onThursday", + "columnName": "thursday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onFriday", + "columnName": "friday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSaturday", + "columnName": "saturday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSunday", + "columnName": "sunday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "end_date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "routes_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_mode` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, PRIMARY KEY(`route_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agencyID", + "columnName": "agency_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "route_short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "route_long_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "route_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "route_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "route_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textColor", + "columnName": "route_text_color", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "route_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_stop_times", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`trip_id` TEXT NOT NULL, `arrival_time` TEXT NOT NULL, `departure_time` TEXT NOT NULL, `stop_id` INTEGER NOT NULL, `stop_sequence` INTEGER NOT NULL, PRIMARY KEY(`trip_id`, `stop_id`), FOREIGN KEY(`stop_id`) REFERENCES `stops_gtfs`(`stop_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`trip_id`) REFERENCES `gtfs_trips`(`trip_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrival_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departure_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stopSequence", + "columnName": "stop_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id", + "stop_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_stop_times_stop_id", + "unique": false, + "columnNames": [ + "stop_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_stop_times_stop_id` ON `${TABLE_NAME}` (`stop_id`)" + } + ], + "foreignKeys": [ + { + "table": "stops_gtfs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stop_id" + ], + "referencedColumns": [ + "stop_id" + ] + }, + { + "table": "gtfs_trips", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "trip_id" + ], + "referencedColumns": [ + "trip_id" + ] + } + ] + }, + { + "tableName": "gtfs_trips", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `service_id` TEXT NOT NULL, `trip_id` TEXT NOT NULL, `trip_headsign` TEXT NOT NULL, `direction_id` INTEGER NOT NULL, `block_id` TEXT NOT NULL, `shape_id` TEXT NOT NULL, `wheelchair_accessible` INTEGER NOT NULL, `limited_route` INTEGER NOT NULL, PRIMARY KEY(`trip_id`), FOREIGN KEY(`route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "routeID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripHeadsign", + "columnName": "trip_headsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionID", + "columnName": "direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockID", + "columnName": "block_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isWheelchairAccess", + "columnName": "wheelchair_accessible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLimitedRoute", + "columnName": "limited_route", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_trips_route_id", + "unique": false, + "columnNames": [ + "route_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_route_id` ON `${TABLE_NAME}` (`route_id`)" + } + ], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "gtfs_shapes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shape_id` TEXT NOT NULL, `shape_pt_lat` REAL NOT NULL, `shape_pt_lon` REAL NOT NULL, `shape_pt_sequence` INTEGER NOT NULL, PRIMARY KEY(`shape_id`, `shape_pt_sequence`))", + "fields": [ + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointLat", + "columnName": "shape_pt_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointLon", + "columnName": "shape_pt_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointSequence", + "columnName": "shape_pt_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "shape_id", + "shape_pt_sequence" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mato_patterns", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_name` TEXT NOT NULL, `pattern_code` TEXT NOT NULL, `pattern_hash` TEXT NOT NULL, `pattern_direction_id` INTEGER NOT NULL, `pattern_route_id` TEXT NOT NULL, `pattern_headsign` TEXT, `pattern_polyline` TEXT NOT NULL, `pattern_polylength` INTEGER NOT NULL, PRIMARY KEY(`pattern_code`), FOREIGN KEY(`pattern_route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "pattern_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "pattern_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "semanticHash", + "columnName": "pattern_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionId", + "columnName": "pattern_direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "routeGtfsId", + "columnName": "pattern_route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headsign", + "columnName": "pattern_headsign", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patternGeometryPoly", + "columnName": "pattern_polyline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patternGeometryLength", + "columnName": "pattern_polylength", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "pattern_code" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pattern_route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "patterns_stops", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_gtfs_id` TEXT NOT NULL, `stop_gtfs_id` TEXT NOT NULL, `stop_order` INTEGER NOT NULL, PRIMARY KEY(`pattern_gtfs_id`, `stop_gtfs_id`, `stop_order`), FOREIGN KEY(`pattern_gtfs_id`) REFERENCES `mato_patterns`(`pattern_code`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "patternId", + "columnName": "pattern_gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopGtfsId", + "columnName": "stop_gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "stop_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "pattern_gtfs_id", + "stop_gtfs_id", + "stop_order" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "mato_patterns", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pattern_gtfs_id" + ], + "referencedColumns": [ + "pattern_code" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d2aa826894d1e6b1429678e13b65433')" + ] + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,8 @@ } ext { + androidXTestVersion = "1.4.0" + //multidex multidex_version = "2.0.1" //libraries versions @@ -57,6 +59,12 @@ versionName "1.16.3" vectorDrawables.useSupportLibrary = true multiDexEnabled true + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] + } + } + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { @@ -65,6 +73,8 @@ } sourceSets { + androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) + main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] @@ -93,7 +103,7 @@ dependencies { //new libraries - implementation "androidx.fragment:fragment:$fragment_version" + implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.3.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" @@ -121,25 +131,45 @@ // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData - implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) - implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" + // Legacy + implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" - androidTestImplementation "androidx.room:room-testing:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' + testImplementation 'junit:junit:4.12' + implementation 'junit:junit:4.12' + + implementation "androidx.test.ext:junit:1.1.3" + implementation "androidx.test:core:$androidXTestVersion" + implementation "androidx.test:runner:$androidXTestVersion" + implementation "androidx.room:room-testing:$room_version" + + androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test:core:$androidXTestVersion" + androidTestImplementation "androidx.test:runner:$androidXTestVersion" + androidTestImplementation "androidx.test:rules:$androidXTestVersion" + androidTestImplementation "androidx.room:room-testing:$room_version" + + + + } } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + } diff --git a/res/drawable/ic_moving.xml b/res/drawable/ic_moving.xml new file mode 100644 --- /dev/null +++ b/res/drawable/ic_moving.xml @@ -0,0 +1,9 @@ + + + diff --git a/res/drawable/ic_moving_emph.xml b/res/drawable/ic_moving_emph.xml new file mode 100644 --- /dev/null +++ b/res/drawable/ic_moving_emph.xml @@ -0,0 +1,9 @@ + + + diff --git a/res/layout/arrivals_nearby_card.xml b/res/layout/arrivals_nearby_card.xml --- a/res/layout/arrivals_nearby_card.xml +++ b/res/layout/arrivals_nearby_card.xml @@ -27,16 +27,23 @@ android:layout_margin="10dp" android:textStyle="normal" android:layout_marginRight="20dp" android:layout_marginEnd="20dp" android:layout_marginBottom="20dp"/> - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/entry_bus_stop.xml b/res/layout/entry_bus_stop.xml --- a/res/layout/entry_bus_stop.xml +++ b/res/layout/entry_bus_stop.xml @@ -6,68 +6,69 @@ android:paddingBottom="8dip" > + android:id="@+id/busStopName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="16sp" + android:layout_marginStart="16sp" + android:layout_marginRight="16sp" + android:layout_marginEnd="16sp" + android:layout_marginTop="2sp" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_toLeftOf="@+id/busStopID" + android:layout_toStartOf="@+id/busStopID" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="@color/blue_500"/> + android:id="@+id/busStopLocality" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="16sp" + android:layout_marginStart="16sp" + android:layout_marginRight="16sp" + android:layout_marginEnd="16sp" + android:layout_below="@id/busStopName" + android:layout_toLeftOf="@+id/busStopID" + android:layout_toStartOf="@+id/busStopID" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:textColor="@color/grey_600" + android:textAppearance="?android:attr/textAppearanceMedium"/> + android:id="@+id/routesThatStopHere" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/busStopLocality" + android:layout_marginLeft="16sp" + android:layout_marginStart="16sp" + android:layout_marginRight="16sp" + android:layout_marginEnd="16sp" + android:gravity="center_vertical" + android:layout_toLeftOf="@id/busStopID" + android:layout_toStartOf="@id/busStopID" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@color/grey_600"/> + android:id="@+id/busStopID" + android:layout_width="70dip" + android:layout_height="70dip" + android:layout_marginRight="16sp" + android:layout_marginEnd="16sp" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:layout_alignTop="@id/busStopName" + android:layout_centerVertical="true" + android:background="@drawable/bus_stop_background" + android:gravity="center" + android:textColor="@color/grey_100" + android:textSize="21sp"/> \ No newline at end of file diff --git a/res/layout/fragment_lines.xml b/res/layout/fragment_lines.xml new file mode 100644 --- /dev/null +++ b/res/layout/fragment_lines.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/drawer_main.xml b/res/menu/drawer_main.xml --- a/res/menu/drawer_main.xml +++ b/res/menu/drawer_main.xml @@ -14,6 +14,10 @@ android:id="@+id/nav_favorites_item" android:icon="@drawable/ic_star_filled_white" android:title="@string/nav_favorites_text" /> + + + + + \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -14,7 +14,11 @@ Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s - Linee: %1$s + Linea + Linee + + Linea: %1$s + Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code @@ -95,7 +99,10 @@ Ricerca della posizione in corso… Nessuna fermata nei dintorni Preferenze - Aggiornamento del database… + Aggiornamento del database… + Aggiornamento del database + Aggiornamento database forzato + Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero @@ -140,6 +147,10 @@ Canale unico delle notifiche + Database + Informazioni sul database (aggiornamento) + + Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio @@ -160,6 +171,10 @@ Versione app Orari di arrivo + + Richiesto aggiornamento del database + + Mostra direzioni in maiuscolo @@ -167,4 +182,7 @@ Tutto in maiuscolo Solo la prima lettera maiuscola + + + Tocca a lungo per le opzioni diff --git a/res/values/keys.xml b/res/values/keys.xml --- a/res/values/keys.xml +++ b/res/values/keys.xml @@ -2,4 +2,5 @@ layout_pref + pref_update_db_now \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -22,7 +22,10 @@ Arrivals at: %1$s Choose the bus stop… - Lines: %1$s + Line + Lines + Lines: %1$s + Line: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website @@ -126,7 +129,10 @@ Allow access to position to show it on the map Please enable GPS - Database update in progress… + Database update in progress… + Updating the database + Force database update + Touch to update the app database now is arriving at at the stop %1$s - %2$s @@ -156,6 +162,9 @@ --> Default Default channel for notifications + Database + Notifications on the update of the database + Asked for %1$s permission too many times Cannot use the map with the storage permission! storage @@ -176,6 +185,7 @@ Buy us a coffee Map Search by stop + Launching database update @@ -191,4 +201,7 @@ CAPITALIZE_FIRST + + + Long press for options diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -58,4 +58,12 @@ android:key="@string/pref_arrival_times_capit" /> + + + + diff --git a/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java b/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java new file mode 100644 --- /dev/null +++ b/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java @@ -0,0 +1,53 @@ +package it.reyboz.bustorino.data.gtfs; + +import androidx.room.Room; +import androidx.room.migration.Migration; +import androidx.room.testing.MigrationTestHelper; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +import it.reyboz.bustorino.data.gtfs.GtfsDatabase; + +//@RunWith(AndroidJUnit4.class) +public class GtfsDBMigrationsTest { + private static final String TEST_DB = "migration-test"; + + @Rule + public MigrationTestHelper helper; + + public GtfsDBMigrationsTest() { + helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), + GtfsDatabase.class.getCanonicalName(), + new FrameworkSQLiteOpenHelperFactory()); + } + + @Test + public void migrateAll() throws IOException { + // Create earliest version of the database. + SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); + db.close(); + + // Open latest version of the database. Room will validate the schema + // once all migrations execute. + GtfsDatabase appDb = Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().getTargetContext(), + GtfsDatabase.class, + TEST_DB) + .addMigrations(ALL_MIGRATIONS).build(); + appDb.getOpenHelper().getWritableDatabase(); + appDb.close(); + } + + // Array of all migrations + private static final Migration[] ALL_MIGRATIONS = new Migration[]{ + GtfsDatabase.Companion.getMIGRATION_1_2()}; +} + diff --git a/src/it/reyboz/bustorino/ActivityExperiments.java b/src/it/reyboz/bustorino/ActivityExperiments.java --- a/src/it/reyboz/bustorino/ActivityExperiments.java +++ b/src/it/reyboz/bustorino/ActivityExperiments.java @@ -18,21 +18,16 @@ package it.reyboz.bustorino; import android.content.Context; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.Toast; -import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.gtfs.GtfsDataParser; import it.reyboz.bustorino.backend.networkTools; -import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; -import it.reyboz.bustorino.data.gtfs.StaticGtfsDao; +import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.middleware.GeneralActivity; import java.io.*; @@ -44,7 +39,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; public class ActivityExperiments extends GeneralActivity { @@ -91,7 +85,7 @@ "ExperimentGTFS", "Last update date is " + updateDate//utils.joinList(files, "\n") ); //Toast.makeText(v.getContext(), "Gtfs data already downloaded", Toast.LENGTH_SHORT).show(); - StaticGtfsDao dao = GtfsDatabase.Companion.getGtfsDatabase(appContext).gtfsDao(); + GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(appContext).gtfsDao(); Log.d(DEBUG_TAG, String.valueOf(dao)); dao.deleteAllStopTimes(); @@ -106,10 +100,12 @@ // now iterate through each item in the stream. The get next // entry call will return a ZipEntry for each file in the // stream + /* Enumeration entries = zipFile.entries(); ZipEntry entry; String line; //final BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + HashSet readLater = new HashSet<>(); while(entries.hasMoreElements()){ entry = entries.nextElement(); @@ -123,30 +119,7 @@ for(ZipEntry laterEntry: readLater){ GtfsDataParser.readGtfsZipEntry(laterEntry, zipFile, v.getContext().getApplicationContext()); } - //Toast.makeText(appContext, "D", Toast.LENGTH_SHORT).show(); - /* - while ((entry = stream.getNextEntry()) != null) { - String s = String.format(Locale.ENGLISH, "Entry: %s len %d added", - entry.getName(), - entry.getSize() - ); - if(entry.getName().contains("stop_times.")){ - //skip and do later - - } - //Toast.makeText(v.getContext(), "File: " + entry.getName(), Toast.LENGTH_SHORT).show(); - Log.d(DEBUG_TAG, s); - //read data in table - final String tableName = entry.getName().split("\\.")[0].trim(); - - - // Once we get the entry from the stream, the stream is - // positioned read to read the raw data, and we keep - // reading until read returns 0 or less. - //result.add(entry.getName()); - } - stream.close(); */ } catch (IOException e) { e.printStackTrace(); @@ -165,32 +138,18 @@ } }; - Toast.makeText(this, "Launching, no result will show", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, "Test disabled", Toast.LENGTH_SHORT).show(); //Looper looper = new Looper(true); //Handler handler = new Handler(); //handler.post(run); - executorService.execute(run); + //executorService.execute(run); } public void deleteDatabase(View v){ - final Context con = getApplicationContext().getApplicationContext(); - Toast.makeText(this, "Deleting GTFS DB contents, wait a few seconds", Toast.LENGTH_SHORT).show(); - Runnable deleteDB = new Runnable() { - @Override - public void run() { - StaticGtfsDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); - Log.d(DEBUG_TAG, String.valueOf(dao)); - dao.deleteAllStopTimes(); - dao.deleteAllTrips(); - dao.deleteAllRoutes(); - dao.deleteAllStops(); - dao.deleteAllServices(); - Log.d(DEBUG_TAG, "Deleted stuff"); - } - }; + //final Context con = getApplicationContext().getApplicationContext(); + Toast.makeText(this, "Deleting GTFS DB contents isn't allowed anymore", Toast.LENGTH_SHORT).show(); - executorService.execute(deleteDB); } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -41,11 +41,6 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; -import androidx.work.BackoffPolicy; -import androidx.work.Constraints; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.PeriodicWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; @@ -53,7 +48,6 @@ import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; -import java.util.concurrent.TimeUnit; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateWorker; @@ -179,7 +173,7 @@ requestArrivalsForStopID(busStopID); } //Try (hopefully) database update - DatabaseUpdate.requestDBUpdateWithWork(this, false); + DatabaseUpdate.requestDBUpdateWithWork(this, false, false); /* Watch for database update */ @@ -239,8 +233,8 @@ //get Fragment FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); FavoritesFragment fragment = FavoritesFragment.newInstance(); - ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES); - ft.addToBackStack(null); + ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES) + .addToBackStack("main"); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); return true; @@ -266,6 +260,22 @@ Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); } return true; + } else if (menuItem.getItemId() == R.id.nav_lines_item) { + closeDrawerIfOpen(); + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + Fragment f = getSupportFragmentManager().findFragmentByTag(LinesFragment.FRAGMENT_TAG); + if(f!=null){ + ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG); + }else{ + //use new method + ft.replace(R.id.mainActContentFrame,LinesFragment.class,null,LinesFragment.FRAGMENT_TAG); + } + + ft.setReorderingAllowed(true) + .addToBackStack("lines") + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .commit(); + return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); @@ -389,7 +399,7 @@ } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); - snackbar = Snackbar.make(baseView, R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); + snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } @@ -500,6 +510,10 @@ titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; + case LINES: + titleResId=R.string.lines; + mNavView.setCheckedItem(R.id.nav_lines_item); + break; default: titleResId = 0; } diff --git a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java --- a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -18,10 +18,12 @@ package it.reyboz.bustorino.adapters; import android.content.Context; +import android.content.SharedPreferences; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -37,15 +39,18 @@ import java.util.*; -public class ArrivalsStopAdapter extends RecyclerView.Adapter { +public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.arrivals_nearby_card; //private List stops; private @Nullable Location userPosition; private FragmentListenerMain listener; - private List< Pair > routesPairList = new ArrayList<>(); + private List< Pair > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable + private String KEY_CAPITALIZE; + private NameCapitalize capit; + public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @Nullable Location pos) { listener = fragmentListener; @@ -55,10 +60,16 @@ resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); + KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); + SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); + defSharPref.registerOnSharedPreferenceChangeListener(this); + String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); + this.capit = NameCapitalize.getCapitalize(capitalizeKey); } + @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); @@ -85,7 +96,7 @@ //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { holder.lineNameTextView.setText(r.getNameForDisplay()); - holder.lineDirectionTextView.setText(r.destinazione); + holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); @@ -117,6 +128,17 @@ return routesPairList.size(); } + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(KEY_CAPITALIZE)){ + String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); + capit = NameCapitalize.getCapitalize(k); + + notifyDataSetChanged(); + + } + } + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; diff --git a/src/it/reyboz/bustorino/adapters/NameCapitalize.java b/src/it/reyboz/bustorino/adapters/NameCapitalize.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/adapters/NameCapitalize.java @@ -0,0 +1,44 @@ +package it.reyboz.bustorino.adapters; + +import java.util.Locale; + +import it.reyboz.bustorino.backend.utils; + +public enum NameCapitalize { + DO_NOTHING, ALL, FIRST; + public static NameCapitalize getCapitalize(String capitalize){ + + switch (capitalize.trim()){ + case "KEEP": + return NameCapitalize.DO_NOTHING; + case "CAPITALIZE_ALL": + return NameCapitalize.ALL; + + case "CAPITALIZE_FIRST": + return NameCapitalize.FIRST; + } + return NameCapitalize.DO_NOTHING; + } + + /** + * Parse the output + * @param input the input string + * @param capitalize the capitalize value + * @return parsed string + */ + public static String capitalizePass(String input, NameCapitalize capitalize){ + String dest = input; + switch (capitalize){ + case ALL: + dest = input.toUpperCase(Locale.ROOT); + break; + case FIRST: + dest = utils.toTitleCase(input, true); + break; + case DO_NOTHING: + default: + + } + return dest; + } +} diff --git a/src/it/reyboz/bustorino/adapters/StopAdapter.java b/src/it/reyboz/bustorino/adapters/StopAdapter.java --- a/src/it/reyboz/bustorino/adapters/StopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopAdapter.java @@ -35,7 +35,7 @@ */ public class StopAdapter extends ArrayAdapter { private LayoutInflater li; - private static int row_layout = R.layout.entry_bus_stop; + private static final int row_layout = R.layout.entry_bus_stop; private static final int busIcon = R.drawable.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; diff --git a/src/it/reyboz/bustorino/adapters/AdapterListener.java b/src/it/reyboz/bustorino/adapters/StopAdapterListener.java rename from src/it/reyboz/bustorino/adapters/AdapterListener.java rename to src/it/reyboz/bustorino/adapters/StopAdapterListener.java --- a/src/it/reyboz/bustorino/adapters/AdapterListener.java +++ b/src/it/reyboz/bustorino/adapters/StopAdapterListener.java @@ -19,6 +19,8 @@ import it.reyboz.bustorino.backend.Stop; -public interface AdapterListener { +public interface StopAdapterListener { void onTappedStop(Stop stop); + + boolean onLongPressOnStop(Stop stop); } diff --git a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java --- a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java @@ -35,13 +35,17 @@ public class StopRecyclerAdapter extends RecyclerView.Adapter { private List stops; - private static final int row_layout = R.layout.entry_bus_stop; + private static final int ITEM_LAYOUT_FAVORITES = R.layout.entry_bus_stop; + private static final int ITEM_LAYOUT_LINES = R.layout.bus_stop_line_elmt; private static final int busIcon = R.drawable.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; private static final int cityIcon = R.drawable.city; - private AdapterListener listener; + private NameCapitalize capitalizeLocation = NameCapitalize.DO_NOTHING; + private final Use usedFor; + + private final StopAdapterListener listener; private int position; @@ -52,14 +56,29 @@ //TextView busLineVehicleIcon; TextView busStopLinesTextView; TextView busStopLocaLityTextView; + + View topStub, bottomStub; Stop mStop; - public ViewHolder(@NonNull View itemView, AdapterListener listener) { + int menuResID=R.menu.menu_favourites_entry; + + public ViewHolder(@NonNull View itemView, StopAdapterListener listener, Use usedFor) { super(itemView); - busStopIDTextView = (TextView) itemView.findViewById(R.id.busStopID); - busStopNameTextView = (TextView) itemView.findViewById(R.id.busStopName); - busStopLinesTextView = (TextView) itemView.findViewById(R.id.routesThatStopHere); - busStopLocaLityTextView = (TextView) itemView.findViewById(R.id.busStopLocality); + busStopIDTextView = itemView.findViewById(R.id.busStopID); + busStopNameTextView = itemView.findViewById(R.id.busStopName); + busStopLinesTextView = itemView.findViewById(R.id.routesThatStopHere); + busStopLocaLityTextView = itemView.findViewById(R.id.busStopLocality); + switch (usedFor){ + case LINES: + topStub = itemView.findViewById(R.id.topStub); + bottomStub = itemView.findViewById(R.id.bottomStub); + menuResID = R.menu.menu_line_item; + break; + case FAVORITES: + default: + topStub = null; + bottomStub = null; + } mStop = new Stop(""); @@ -71,13 +90,29 @@ @Override public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { MenuInflater inflater = new MenuInflater(view.getContext()); - inflater.inflate(R.menu.menu_favourites_entry, contextMenu); + inflater.inflate(menuResID, contextMenu); } } - public StopRecyclerAdapter(List stops,AdapterListener listener) { + public StopRecyclerAdapter(List stops, StopAdapterListener listener, Use usedFor) { this.stops = stops; this.listener = listener; + this.usedFor = usedFor; + } + public StopRecyclerAdapter(List stops, StopAdapterListener listener, Use usedFor, NameCapitalize locationCapit) { + this.stops = stops; + this.listener = listener; + this.usedFor = usedFor; + this.capitalizeLocation = locationCapit; + } + + public NameCapitalize getCapitalizeLocation() { + return capitalizeLocation; + } + + public void setCapitalizeLocation(NameCapitalize capitalizeLocation) { + this.capitalizeLocation = capitalizeLocation; + notifyDataSetChanged(); } public void setStops(List stops){ @@ -100,10 +135,20 @@ @NonNull @Override public StopRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final int layoutID; + switch (usedFor){ + case LINES: + layoutID = ITEM_LAYOUT_LINES; + break; + case FAVORITES: + default: + layoutID = ITEM_LAYOUT_FAVORITES; + + } View view = LayoutInflater.from(parent.getContext()) - .inflate(row_layout, parent, false); + .inflate(layoutID, parent, false); - return new StopRecyclerAdapter.ViewHolder(view, listener); + return new StopRecyclerAdapter.ViewHolder(view, listener, this.usedFor); } @Override @@ -114,11 +159,11 @@ @Override public void onBindViewHolder(@NonNull StopRecyclerAdapter.ViewHolder vh, int position) { - Log.d("StopRecyclerAdapter", "Called for position "+position); + //Log.d("StopRecyclerAdapter", "Called for position "+position); Stop stop = stops.get(position); vh.busStopIDTextView.setText(stop.ID); vh.mStop = stop; - Log.d("StopRecyclerAdapter", "Stop: "+stop.ID); + //Log.d("StopRecyclerAdapter", "Stop: "+stop.ID); // NOTE: intentionally ignoring stop username in search results: if it's in the favorites, why are you searching for it? vh.busStopNameTextView.setText(stop.getStopDisplayName()); @@ -154,21 +199,34 @@ if (stop.location == null) { vh.busStopLocaLityTextView.setVisibility(View.GONE); } else { - vh.busStopLocaLityTextView.setText(stop.location); + vh.busStopLocaLityTextView.setText(NameCapitalize.capitalizePass(stop.location, capitalizeLocation)); vh.busStopLocaLityTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } //trick to set the position - vh.itemView.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View view) { - setPosition(vh.getAdapterPosition()); - return false; - } + vh.itemView.setOnLongClickListener(view -> { + setPosition(vh.getAdapterPosition()); + return false; }); + if(this.usedFor == Use.LINES){ + + //vh.menuResID; + vh.bottomStub.setVisibility(View.VISIBLE); + vh.topStub.setVisibility(View.VISIBLE); + if(position == 0) { + vh.topStub.setVisibility(View.GONE); + } + else if (position == stops.size()-1) { + vh.bottomStub.setVisibility(View.GONE); + } + } } @Override public int getItemCount() { return stops.size(); } + + public enum Use{ + FAVORITES, LINES + } } diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -291,7 +291,8 @@ Stop s = new Stop(stopID, stopName, null,location,t,Arrays.asList(lines), Double.parseDouble(currentStop.getString("lat")), - Double.parseDouble(currentStop.getString("lng"))); + Double.parseDouble(currentStop.getString("lng")), + null); if(placeName!=null) s.setAbsurdGTTPlaceName(placeName); stopslist.add(s); diff --git a/src/it/reyboz/bustorino/backend/Notifications.java b/src/it/reyboz/bustorino/backend/Notifications.java --- a/src/it/reyboz/bustorino/backend/Notifications.java +++ b/src/it/reyboz/bustorino/backend/Notifications.java @@ -8,6 +8,7 @@ public class Notifications { public static final String DEFAULT_CHANNEL_ID ="Default"; + public static final String DB_UPDATE_CHANNELS_ID ="Database Update"; public static void createDefaultNotificationChannel(Context context) { // Create the NotificationChannel, but only on API 26+ because diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -48,13 +48,13 @@ public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, - s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude()); + s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), null); } public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, - @Nullable Double lat, @Nullable Double lon) { - super(ID, name, userName, location, null, null, lat, lon); + @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { + super(ID, name, userName, location, null, null, lat, lon, gtfsID); } public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere) { diff --git a/src/it/reyboz/bustorino/backend/Result.java b/src/it/reyboz/bustorino/backend/Result.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/Result.java @@ -0,0 +1,38 @@ +package it.reyboz.bustorino.backend; + +import androidx.annotation.Nullable; + + +public class Result { + @Nullable + public final T result; + + @Nullable + public final Exception exception; + public boolean isSuccess() { + return exception == null; + } + + + public static Result success(@Nullable T result) { + return new Result<>(result); + } + + /** + * Returns a failed response + */ + public static Result failure(Exception error) { + return new Result<>(error); + } + + private Result(@Nullable T result) { + this.result = result; + this.exception = null; + } + + + private Result(Exception error) { + this.result = null; + this.exception = error; + } +} diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -80,7 +80,9 @@ /** * Constructor that sets EVERYTHING. */ - public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, @Nullable Double lat, @Nullable Double lon) { + public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, + @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, + @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { this.ID = ID; this.name = name; this.username = userName; @@ -89,6 +91,7 @@ this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; + this.gtfsID = gtfsID; } diff --git a/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java @@ -0,0 +1,48 @@ +package it.reyboz.bustorino.backend.gtfs; + +import org.osmdroid.util.GeoPoint; + +import java.util.ArrayList; + +public final class PolylineParser { + /** + * Decode a Google polyline + * Thanks to https://stackoverflow.com/questions/9341020/how-to-decode-googles-polyline-algorithm + * @param encodedPolyline the encoded polyline in a string + * @param initial_capacity for the list + * @return the list of points correspoding to the polyline + */ + public static ArrayList decodePolyline(String encodedPolyline, int initial_capacity) { + ArrayList points = new ArrayList<>(initial_capacity); + int truck = 0; + int carriage_q = 0; + int longit=0, latit=0; + boolean is_lat=true; + for (int x = 0, xx = encodedPolyline.length(); x < xx; ++x) { + int i = encodedPolyline.charAt(x); + i -= 63; + int _5_bits = i << (32 - 5) >>> (32 - 5); + truck |= _5_bits << carriage_q; + carriage_q += 5; + boolean is_last = (i & (1 << 5)) == 0; + if (is_last) { + boolean is_negative = (truck & 1) == 1; + truck >>>= 1; + if (is_negative) { + truck = ~truck; + } + if (is_lat){ + latit += truck; + is_lat = false; + } else{ + longit += truck; + points.add(new GeoPoint((double)latit/1e5,(double)longit/1e5)); + is_lat=true; + } + carriage_q = 0; + truck = 0; + } + } + return points; + } +} diff --git a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java --- a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -31,10 +31,7 @@ import org.json.JSONException; import org.json.JSONObject; -import java.nio.charset.StandardCharsets; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import it.reyboz.bustorino.backend.Fetcher; @@ -54,7 +51,7 @@ AtomicReference res, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { - super(MatoAPIFetcher.QueryType.ARRIVALS, listener, errorListener); + super(MatoQueries.QueryType.ARRIVALS, listener, errorListener); this.stopName = stopName; this.startingTime = startingTime; this.timeRange = timeRange; diff --git a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java --- a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java @@ -12,9 +12,9 @@ private static final String API_URL="https://mapi.5t.torino.it/routing/v1/routers/mat/index/graphql"; protected final Response.Listener listener; - private final MatoAPIFetcher.QueryType type; + protected final MatoQueries.QueryType type; public MapiVolleyRequest( - MatoAPIFetcher.QueryType type, + MatoQueries.QueryType type, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { super(Method.POST, API_URL, errorListener); diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt --- a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -19,15 +19,23 @@ import android.content.Context import android.util.Log +import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* +import it.reyboz.bustorino.data.gtfs.GtfsAgency +import it.reyboz.bustorino.data.gtfs.GtfsFeed +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.data.gtfs.MatoPattern +import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference +import kotlin.collections.ArrayList open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher { @@ -58,7 +66,7 @@ return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue - request.setTag(getVolleyReqTag(QueryType.ARRIVALS)) + request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) try { @@ -105,10 +113,15 @@ "DNT" to "1", "Host" to "mapi.5t.torino.it") - fun getVolleyReqTag(type: QueryType): String{ + private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) + + fun getVolleyReqTag(type: MatoQueries.QueryType): String{ return when (type){ - QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" - QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" + MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" + MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" + MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds" + MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes" + MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute" } } @@ -120,14 +133,15 @@ val future = RequestFuture.newFuture>() val request = VolleyAllStopsRequest(future, future) - request.tag = getVolleyReqTag(QueryType.ALL_STOPS) + request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS) + request.retryPolicy = longRetryPolicy requestQueue.add(request) var palinaList:List = mutableListOf() try { - palinaList = future.get(60, TimeUnit.SECONDS) + palinaList = future.get(120, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { @@ -172,10 +186,9 @@ val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), - null, null, latitude, longitude + null, null, latitude, longitude, + jsonStop.getString("gtfsId") ) - palina.gtfsID = jsonStop.getString("gtfsId") - val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf() // get all the possible routes @@ -259,10 +272,159 @@ return data } - } - enum class QueryType { - ARRIVALS, ALL_STOPS + fun getFeedsAndAgencies(context: Context, res: AtomicReference?): + Pair, ArrayList> { + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + val future = RequestFuture.newFuture() + + val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future) + request.setRetryPolicy(longRetryPolicy) + request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS) + + requestQueue.add(request) + + val feeds = ArrayList() + val agencies = ArrayList() + var outObj = "" + try { + val resObj = future.get(120,TimeUnit.SECONDS) + outObj = resObj.toString(1) + val feedsJSON = resObj.getJSONArray("feeds") + for (i in 0 until feedsJSON.length()){ + val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i)) + feeds.add(resTup.first) + + agencies.addAll(resTup.second) + } + + + } catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } catch (e: JSONException){ + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + } + return Pair(feeds,agencies) + + } + fun getRoutes(context: Context, res: AtomicReference?): + ArrayList{ + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + val future = RequestFuture.newFuture() + + val params = JSONObject() + params.put("feeds","gtt") + + val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future) + request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES) + request.retryPolicy = longRetryPolicy + + requestQueue.add(request) + + val routes = ArrayList() + var outObj = "" + try { + val resObj = future.get(120,TimeUnit.SECONDS) + outObj = resObj.toString(1) + val routesJSON = resObj.getJSONArray("routes") + for (i in 0 until routesJSON.length()){ + val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i)) + routes.add(route) + } + + + } catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } catch (e: JSONException){ + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + } + return routes + + } + fun getPatternsWithStops(context: Context, routesGTFSIds: ArrayList, res: AtomicReference?): ArrayList{ + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + + val future = RequestFuture.newFuture() + + val params = JSONObject() + for (r in routesGTFSIds){ + if(r.isEmpty()) routesGTFSIds.remove(r) + } + val routes = JSONArray(routesGTFSIds) + + params.put("routes",routes) + + val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future) + request.retryPolicy = longRetryPolicy + request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES) + + requestQueue.add(request) + + val patterns = ArrayList() + //var outObj = "" + try { + val resObj = future.get(60,TimeUnit.SECONDS) + //outObj = resObj.toString(1) + val routesJSON = resObj.getJSONArray("routes") + for (i in 0 until routesJSON.length()){ + val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i)) + patterns.addAll(patternList) + } + + + } catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } catch (e: JSONException){ + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + //Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + } + /* + var numRequests = 0 + for(routeName in routesGTFSIds){ + if (!routeName.isEmpty()) numRequests++ + } + val countDownForRequests = CountDownLatch(numRequests) + val lockSave = ReentrantLock() + //val countDownFor + for (routeName in routesGTFSIds){ + val pars = JSONObject() + pars.put("") + + } + val goodResponseListener = Response.Listener { } + val errorResponseListener = Response.ErrorListener { } + */ + + return patterns + } + + } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt --- a/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt @@ -86,5 +86,81 @@ } } """ + + const val ALL_FEEDS=""" + query AllFeeds{ + feeds{ + feedId + agencies{ + gtfsId + name + url + fareUrl + phone + } + } + } + """ + + const val ROUTES_BY_FEED=""" + query AllRoutes(${'$'}feeds: [String]){ + routes(feeds: ${'$'}feeds) { + agency{ + gtfsId + } + gtfsId + shortName + longName + type + desc + color + textColor + } + } + """ + + const val ROUTES_WITH_PATTERNS=""" + query RoutesWithPatterns(${'$'}routes: [String]) { + routes(ids: ${'$'}routes) { + gtfsId + shortName + longName + type + + patterns{ + name + code + semanticHash + directionId + headsign + stops{ + gtfsId + lat + lon + } + patternGeometry{ + length + points + } + + } + } + } + """ + + fun getNameAndRequest(type: QueryType): Pair{ + return when (type){ + QueryType.FEEDS -> Pair("AllFeeds", ALL_FEEDS) + QueryType.ALL_STOPS -> Pair("AllStops", ALL_STOPS_BY_FEEDS) + QueryType.ARRIVALS -> Pair("AllStopsDirect", QUERY_ARRIVALS) + QueryType.ROUTES -> Pair("AllRoutes", ROUTES_BY_FEED) + QueryType.PATTERNS_FOR_ROUTES -> Pair("RoutesWithPatterns", ROUTES_WITH_PATTERNS) + } + } } + + enum class QueryType { + ARRIVALS, ALL_STOPS, FEEDS, ROUTES, PATTERNS_FOR_ROUTES + } + } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt b/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt @@ -0,0 +1,48 @@ +package it.reyboz.bustorino.backend.mato + +import android.util.Log +import com.android.volley.NetworkResponse +import com.android.volley.Response +import com.android.volley.VolleyError +import com.android.volley.toolbox.HttpHeaderParser +import org.json.JSONException +import org.json.JSONObject + +class MatoVolleyJSONRequest(type: MatoQueries.QueryType, + val variables: JSONObject, + listener: Response.Listener, + errorListener: Response.ErrorListener?) + : MapiVolleyRequest(type, listener, errorListener) { + protected val requestName:String + protected val requestQuery:String + init { + val dd = MatoQueries.getNameAndRequest(type) + requestName = dd.first + requestQuery = dd.second + } + + override fun getBody(): ByteArray { + + val data = MatoAPIFetcher.makeRequestParameters(requestName, variables, requestQuery) + + return data.toString().toByteArray() + } + + override fun parseNetworkResponse(response: NetworkResponse?): Response { + if (response==null) + return Response.error(VolleyError("Null response")) + else if(response.statusCode != 200) + return Response.error(VolleyError("Response not ready, status "+response.statusCode)) + val obj:JSONObject + try { + obj = JSONObject(String(response.data)).getJSONObject("data") + }catch (ex: JSONException){ + Log.e("BusTO-VolleyJSON","Cannot parse response as JSON") + ex.printStackTrace() + return Response.error(VolleyError("Error parsing JSON")) + } + + return Response.success(obj, HttpHeaderParser.parseCacheHeaders(response)) + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt b/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt @@ -0,0 +1,120 @@ +/* + BusTO - Backend components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.backend.mato + +import it.reyboz.bustorino.data.gtfs.* +import org.json.JSONObject + +abstract class ResponseParsing{ + + companion object{ + fun parseAgencyJSON(jsonObject: JSONObject): GtfsAgency { + return GtfsAgency( + jsonObject.getString("gtfsId"), + jsonObject.getString("name"), + jsonObject.getString("url"), + jsonObject.getString("fareUrl"), + jsonObject.getString("phone"), + null + ) + } + + /** + * Parse a feed request json, containing the GTFS agencies it is served by + */ + fun parseFeedJSON(jsonObject: JSONObject): Pair> { + + val agencies = ArrayList() + val feed = GtfsFeed(jsonObject.getString("feedId")) + val oo = jsonObject.getJSONArray("agencies") + agencies.ensureCapacity(oo.length()) + for (i in 0 until oo.length()){ + val agObj = oo.getJSONObject(i) + + agencies.add( + GtfsAgency( + agObj.getString("gtfsId"), + agObj.getString("name"), + agObj.getString("url"), + agObj.getString("fareUrl"), + agObj.getString("phone"), + feed + ) + ) + } + return Pair(feed, agencies) + } + + fun parseRouteJSON(jsonObject: JSONObject): GtfsRoute { + + val agencyJSON = jsonObject.getJSONObject("agency") + val agencyId = agencyJSON.getString("gtfsId") + + + return GtfsRoute( + jsonObject.getString("gtfsId"), + agencyId, + jsonObject.getString("shortName"), + jsonObject.getString("longName"), + jsonObject.getString("desc"), + GtfsMode.getByValue(jsonObject.getInt("type"))!!, + jsonObject.getString("color"), + jsonObject.getString("textColor") + + ) + } + + /** + * Parse a route pattern from the JSON response of the MaTO server + */ + fun parseRoutePatternsStopsJSON(jsonObject: JSONObject) : ArrayList{ + val routeGtfsId = jsonObject.getString("gtfsId") + + val patternsJSON = jsonObject.getJSONArray("patterns") + val patternsOut = ArrayList(patternsJSON.length()) + var mPatternJSON: JSONObject + for(i in 0 until patternsJSON.length()){ + mPatternJSON = patternsJSON.getJSONObject(i) + + val stopsJSON = mPatternJSON.getJSONArray("stops") + + val stopsCodes = ArrayList(stopsJSON.length()) + for(k in 0 until stopsJSON.length()){ + stopsCodes.add( + stopsJSON.getJSONObject(k).getString("gtfsId") + ) + } + + val geometry = mPatternJSON.getJSONObject("patternGeometry") + val numGeo = geometry.getInt("length") + val polyline = geometry.getString("points") + + patternsOut.add( + MatoPattern( + mPatternJSON.getString("name"), mPatternJSON.getString("code"), + mPatternJSON.getString("semanticHash"), mPatternJSON.getInt("directionId"), + routeGtfsId,mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes + ) + ) + } + return patternsOut + } + + + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt --- a/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt +++ b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt @@ -31,7 +31,7 @@ listener: Response.Listener>, errorListener: Response.ErrorListener, ) : MapiVolleyRequest>( - MatoAPIFetcher.QueryType.ALL_STOPS,listener, errorListener) { + MatoQueries.QueryType.ALL_STOPS,listener, errorListener) { private val FEEDS = JSONArray() init { @@ -73,7 +73,7 @@ return Response.success(palinas, HttpHeaderParser.parseCacheHeaders(response)) } companion object{ - val FEEDS_STR = arrayOf("gtt") + //val FEEDS_STR = arrayOf("gtt") } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -15,11 +15,15 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; public abstract class utils { private static final double EarthRadius = 6371e3; + + public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); @@ -107,21 +111,58 @@ } return busStopID; } + final static Pattern ROMAN_PATTERN = Pattern.compile( + "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); + private static boolean isRomanNumber(String str){ + if(str.isEmpty()) return false; + final Matcher matcher = ROMAN_PATTERN.matcher(str); + return matcher.find(); + } public static String toTitleCase(String givenString, boolean lowercaseRest) { - String[] arr = givenString.split(" "); + String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); - for (int i = 0; i < arr.length; i++) { - if (arr[i].length() > 1) { - sb.append(Character.toUpperCase(arr[i].charAt(0))); + for (String s : arr) { + if (s.length() > 0) { + String[] allsubs = s.split("\\."); + + boolean addPoint = s.contains("."); + /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli + { + DOESN'T ALWAYS WORK + addPoint = false; + allsubs = new String[]{s}; + }*/ + boolean first = true; + for (String subs : allsubs) { + if(first) first=false; + else { + if (addPoint) sb.append("."); + sb.append(" "); + } + if(isRomanNumber(subs)){ + //add and skip the rest + sb.append(subs); + continue; + } + sb.append(Character.toUpperCase(subs.charAt(0))); + if (lowercaseRest) + sb.append(subs.substring(1).toLowerCase(Locale.ROOT)); + else + sb.append(subs.substring(1)); + + } + sb.append(" "); + /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); - } - else sb.append(arr[i]); + + */ + } else sb.append(s); } return sb.toString().trim(); } diff --git a/src/it/reyboz/bustorino/data/AppDataProvider.java b/src/it/reyboz/bustorino/data/AppDataProvider.java --- a/src/it/reyboz/bustorino/data/AppDataProvider.java +++ b/src/it/reyboz/bustorino/data/AppDataProvider.java @@ -153,7 +153,7 @@ Log.d("InsBranchWithProvider","line: "+c.getString(c.getColumnIndex(LinesTable.COLUMN_NAME))+"\n" +c.getString(c.getColumnIndex(LinesTable.COLUMN_DESCRIPTION))); }*/ - lineid = c.getInt(c.getColumnIndex(NextGenDB.Contract.LinesTable._ID)); + lineid = c.getInt(c.getColumnIndexOrThrow(NextGenDB.Contract.LinesTable._ID)); c.close(); } values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME); @@ -191,7 +191,7 @@ @Override public boolean onCreate() { con = getContext(); - appDBHelper = new NextGenDB(getContext()); + appDBHelper = NextGenDB.getInstance(getContext()); userDBHelper = new UserDB(getContext()); if(con!=null) { preferences = new DBStatusManager(con,null); diff --git a/src/it/reyboz/bustorino/data/AppRepository.java b/src/it/reyboz/bustorino/data/AppRepository.java deleted file mode 100644 --- a/src/it/reyboz/bustorino/data/AppRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - BusTO - Data components - Copyright (C) 2021 Fabio Mazza - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ -package it.reyboz.bustorino.data; - -public class AppRepository { - - -} diff --git a/src/it/reyboz/bustorino/data/DBUpdateWorker.java b/src/it/reyboz/bustorino/data/DBUpdateWorker.java --- a/src/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/src/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -47,13 +47,13 @@ public static final int SUCCESS_NO_ACTION_NEEDED = 9; public static final int SUCCESS_UPDATE_DONE = 1; - private final int notifi_ID=62341; + private final static int NOTIFIC_ID =32198; public static final String FORCED_UPDATE = "FORCED-UPDATE"; public static final String DEBUG_TAG = "Busto-UpdateWorker"; - private static final long UPDATE_MIN_DELAY= 3*7*24*3600; //3 weeks + private static final long UPDATE_MIN_DELAY= 9*24*3600; //9 days public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { @@ -66,7 +66,14 @@ public Result doWork() { //register Notification channel final Context con = getApplicationContext(); - Notifications.createDefaultNotificationChannel(con); + //Notifications.createDefaultNotificationChannel(con); + //Use the new notification channels + Notifications.createNotificationChannel(con,con.getString(R.string.database_notification_channel), + con.getString(R.string.database_notification_channel_desc), NotificationManagerCompat.IMPORTANCE_LOW, + Notifications.DB_UPDATE_CHANNELS_ID + ); + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); + final int notification_ID = 32198; final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); @@ -77,7 +84,17 @@ final long lastDBUpdateTime = shPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, 0); long currentTime = System.currentTimeMillis()/1000; - final int notificationID = showNotification(); + //showNotification(notificationManager, notification_ID); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(con, + Notifications.DB_UPDATE_CHANNELS_ID) + .setContentTitle(con.getString(R.string.database_update_msg_notif)) + .setProgress(0,0,true) + .setPriority(NotificationCompat.PRIORITY_LOW); + builder.setSmallIcon(R.drawable.ic_bus_orange); + + + notificationManager.notify(notification_ID,builder.build()); + Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); /* @@ -95,7 +112,7 @@ if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) && !isUpdateCompulsory) { //don't need to update - cancelNotification(notificationID); + cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } @@ -106,8 +123,7 @@ DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); if (resultUpdate != DatabaseUpdate.Result.DONE){ - Fetcher.Result result = resultAtomicReference.get(); - + //Fetcher.Result result = resultAtomicReference.get(); final Data.Builder dataBuilder = new Data.Builder(); switch (resultUpdate){ case ERROR_STOPS_DOWNLOAD: @@ -117,7 +133,7 @@ dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); break; } - cancelNotification(notificationID); + cancelNotification(notification_ID); return ListenableWorker.Result.failure(dataBuilder.build()); } Log.d(DEBUG_TAG, "Update finished successfully!"); @@ -127,7 +143,7 @@ currentTime = System.currentTimeMillis()/1000; editor.putLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, currentTime); editor.apply(); - cancelNotification(notificationID); + cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); } @@ -144,19 +160,21 @@ .build(); } - - private int showNotification(){ - final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), Notifications.DEFAULT_CHANNEL_ID) + /* + private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, + final String channel_ID){ + final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); - final NotificationManagerCompat notifcManager = NotificationManagerCompat.from(getApplicationContext()); - final int notification_ID = 32198; - notifcManager.notify(notification_ID,builder.build()); + + + notificManager.notify(notification_ID,builder.build()); return notification_ID; } + */ private void cancelNotification(int notificationID){ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); diff --git a/src/it/reyboz/bustorino/data/DatabaseUpdate.java b/src/it/reyboz/bustorino/data/DatabaseUpdate.java --- a/src/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/src/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -22,22 +22,31 @@ import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; -import androidx.core.content.ContextCompat; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; -import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; +import it.reyboz.bustorino.data.gtfs.GtfsAgency; +import it.reyboz.bustorino.data.gtfs.GtfsDatabase; +import it.reyboz.bustorino.data.gtfs.GtfsDBDao; +import it.reyboz.bustorino.data.gtfs.GtfsFeed; +import it.reyboz.bustorino.data.gtfs.GtfsRoute; +import it.reyboz.bustorino.data.gtfs.MatoPattern; +import it.reyboz.bustorino.data.gtfs.PatternStop; +import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -59,131 +68,189 @@ DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD } - /** - * Request the server the version of the database - * @return the version of the DB, or an error code - */ - public static int getNewVersion(){ - AtomicReference gres = new AtomicReference<>(); - String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); - if(networkRequest == null){ - return VERSION_UNAVAILABLE; - } + /** + * Request the server the version of the database + * @return the version of the DB, or an error code + */ + public static int getNewVersion(){ + AtomicReference gres = new AtomicReference<>(); + String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); + if(networkRequest == null){ + return VERSION_UNAVAILABLE; + } - try { - JSONObject resp = new JSONObject(networkRequest); - return resp.getInt("id"); - } catch (JSONException e) { - e.printStackTrace(); - Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); - return JSON_PARSING_ERROR; - } + try { + JSONObject resp = new JSONObject(networkRequest); + return resp.getInt("id"); + } catch (JSONException e) { + e.printStackTrace(); + Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); + return JSON_PARSING_ERROR; } - /** - * Run the DB Update - * @param con a context - * @param gres a result reference - * @return result of the update - */ - public static Result performDBUpdate(Context con, AtomicReference gres) { + } + private static boolean updateGTFSAgencies(Context con, AtomicReference res){ - final FiveTAPIFetcher f = new FiveTAPIFetcher(); - /* - final ArrayList stops = f.getAllStopsFromGTT(gres); - //final ArrayList cpOp = new ArrayList<>(); + final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); - if (gres.get() != Fetcher.Result.OK) { - Log.w(DEBUG_TAG, "Something went wrong downloading"); - return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( + con, res + ); - } + dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); - */ - final NextGenDB dbHelp = new NextGenDB(con.getApplicationContext()); - final SQLiteDatabase db = dbHelp.getWritableDatabase(); + return true; + } + private static boolean updateGTFSRoutes(Context con, AtomicReference res){ - final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); - if (gres.get() != Fetcher.Result.OK) { - Log.w(DEBUG_TAG, "Something went wrong downloading"); - return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); - } - //TODO: Get the type of stop from the lines - //Empty the needed tables - db.beginTransaction(); - //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); - //db.delete(LinesTable.TABLE_NAME,null,null); - - //put new data - long startTime = System.currentTimeMillis(); - - Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); - for (final Palina p : palinasMatoAPI) { - final ContentValues cv = new ContentValues(); - - cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); - cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); - if (p.location != null) - cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); - cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); - cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); - if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); - cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, p.routesThatStopHereToString()); - if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); - if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); - //Log.d(DEBUG_TAG,cv.toString()); - //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); - //valuesArr[i] = cv; - db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); + final List routes= MatoAPIFetcher.Companion.getRoutes( + con, res + ); + dao.insertRoutes(routes); + if(res.get()!= Fetcher.Result.OK){ + return false; + } + final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); + for(GtfsRoute r: routes){ + gtfsRoutesIDs.add(r.getGtfsId()); + } + long t0 = System.currentTimeMillis(); + final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); + long tend = System.currentTimeMillis() - t0; + Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); + if(res.get()!=Fetcher.Result.OK){ + Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); + return false; + } + final ArrayList patternStops = new ArrayList<>(patterns.size()); + for(MatoPattern p: patterns){ + final ArrayList stopsIDs = p.getStopsGtfsIDs(); + for (int i=0; i routes = f.getAllLinesFromGTT(gres); + return true; + } - if (routes == null) { - Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); - dbHelp.close(); - return DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD; - } + /** + * Run the DB Update + * @param con a context + * @param gres a result reference + * @return result of the update + */ + public static Result performDBUpdate(Context con, AtomicReference gres) { - db.beginTransaction(); - startTime = System.currentTimeMillis(); - for (Route r : routes) { - final ContentValues cv = new ContentValues(); - cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName()); - switch (r.type) { - case BUS: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO"); - break; - case RAILWAY: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA"); - break; - case LONG_DISTANCE_BUS: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA"); - break; - } - cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description); - - //db.insert(LinesTable.TABLE_NAME,null,cv); - int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); - if (rows < 1) { //we haven't changed anything - db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - endTime = System.currentTimeMillis(); - Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); + final FiveTAPIFetcher f = new FiveTAPIFetcher(); + + final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); + final SQLiteDatabase db = dbHelp.getWritableDatabase(); + + final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); + if (gres.get() != Fetcher.Result.OK) { + Log.w(DEBUG_TAG, "Something went wrong downloading"); + return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + + } + //TODO: Get the type of stop from the lines + //Empty the needed tables + db.beginTransaction(); + //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); + //db.delete(LinesTable.TABLE_NAME,null,null); + + //put new data + long startTime = System.currentTimeMillis(); + + Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); + for (final Palina p : palinasMatoAPI) { + final ContentValues cv = new ContentValues(); + + cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); + cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); + if (p.location != null) + cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); + cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); + cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); + if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); + cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, p.routesThatStopHereToString()); + if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); + if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); + //Log.d(DEBUG_TAG,cv.toString()); + //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); + //valuesArr[i] = cv; + db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); + + } + db.setTransactionSuccessful(); + db.endTransaction(); + long endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); + + // GTFS data fetching + AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); + updateGTFSAgencies(con, gtfsRes); + if (gtfsRes.get()!= Fetcher.Result.OK){ + Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); + } else{ + Log.d(DEBUG_TAG, "Done downloading agencies"); + } + gtfsRes.set(Fetcher.Result.OK); + updateGTFSRoutes(con,gtfsRes); + if (gtfsRes.get()!= Fetcher.Result.OK){ + Log.w(DEBUG_TAG, "Could not insert the routes into DB"); + } else{ + Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); + } + /* + final ArrayList routes = f.getAllLinesFromGTT(gres); + + if (routes == null) { + Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); dbHelp.close(); + return DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD; - return DatabaseUpdate.Result.DONE; } + db.beginTransaction(); + startTime = System.currentTimeMillis(); + for (Route r : routes) { + final ContentValues cv = new ContentValues(); + cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName()); + switch (r.type) { + case BUS: + cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO"); + break; + case RAILWAY: + cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA"); + break; + case LONG_DISTANCE_BUS: + cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA"); + break; + } + cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description); + + //db.insert(LinesTable.TABLE_NAME,null,cv); + int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); + if (rows < 1) { //we haven't changed anything + db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); + + */ + dbHelp.close(); + + return DatabaseUpdate.Result.DONE; + } + public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); @@ -199,17 +266,21 @@ * @param con the context to use * @param forced if you want to force the request to go now */ - public static void requestDBUpdateWithWork(Context con, boolean forced){ + public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); + final Data reqData = new Data.Builder() + .putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build(); + PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) - .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) + .setInputData(reqData) .build(); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); final long lastDBUpdateTime = theShPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, -10); - if ((version >= 0 || lastDBUpdateTime >=0) && !forced) + if ((version >= 0 || lastDBUpdateTime >=0) && !restart) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, @@ -221,4 +292,12 @@ TODO } */ + + public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner, + @NonNull Observer> observer) { + WorkManager workManager = WorkManager.getInstance(context); + workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe( + lifecycleOwner, observer + ); + } } diff --git a/src/it/reyboz/bustorino/data/GtfsRepository.kt b/src/it/reyboz/bustorino/data/GtfsRepository.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/GtfsRepository.kt @@ -0,0 +1,36 @@ +package it.reyboz.bustorino.data + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import it.reyboz.bustorino.data.gtfs.GtfsDBDao +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.data.gtfs.MatoPattern +import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops + +class GtfsRepository( + val gtfsDao: GtfsDBDao +) { + + + fun getLinesLiveDataForFeed(feed: String): LiveData>{ + //return withContext(Dispatchers.IO){ + return gtfsDao.getRoutesForFeed(feed) + //} + } + fun getPatternsForRouteID(routeID: String): LiveData>{ + return if(routeID.isNotEmpty()) + gtfsDao.getPatternsByRouteID(routeID) + else + MutableLiveData(listOf()) + } + + /** + * Get the patterns with the stops lists (gtfsIDs only) + */ + fun getPatternsWithStopsForRouteID(routeID: String): LiveData>{ + return if(routeID.isNotEmpty()) + gtfsDao.getPatternsWithStopsByRouteID(routeID) + else + MutableLiveData(listOf()) + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/NextGenDB.java b/src/it/reyboz/bustorino/data/NextGenDB.java --- a/src/it/reyboz/bustorino/data/NextGenDB.java +++ b/src/it/reyboz/bustorino/data/NextGenDB.java @@ -85,15 +85,24 @@ StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG + " >= ? AND "+ StopsTable.COL_LONG + " <= ?"; + public static final String QUERY_FROM_GTFS_ID_IN_TO_COMPLETE= StopsTable.COL_GTFS_ID +" IN "; + public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?"; private final Context appContext; + private static NextGenDB INSTANCE; - public NextGenDB(Context context) { + private NextGenDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); appContext = context.getApplicationContext(); } + public static NextGenDB getInstance(Context context) { + if (INSTANCE == null){ + INSTANCE = new NextGenDB(context); + } + return INSTANCE; + } @Override @@ -124,7 +133,7 @@ db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); - DatabaseUpdate.requestDBUpdateWithWork(appContext, true); + DatabaseUpdate.requestDBUpdateWithWork(appContext, true, true); } if(oldVersion < 3 && newVersion == 3){ Log.d("BusTO-Database", "Running upgrades for version 3"); @@ -194,6 +203,53 @@ return stops; } + /** + * Query stops in the database having these IDs + * REMEMBER TO CLOSE THE DB CONNECTION AFTERWARDS + * @param bustoDB readable database instance + * @param gtfsIDs gtfs IDs to query + * @return list of stops + */ + public static synchronized ArrayList queryAllStopsWithGtfsIDs(SQLiteDatabase bustoDB, List gtfsIDs){ + final ArrayList stops = new ArrayList<>(); + + if(bustoDB == null){ + Log.e(DEBUG_TAG, "Asked query for IDs but database is null"); + return stops; + } else if (gtfsIDs == null || gtfsIDs.isEmpty()) { + return stops; + } + + final StringBuilder builder = new StringBuilder(QUERY_FROM_GTFS_ID_IN_TO_COMPLETE); + boolean first = true; + builder.append(" ( "); + for(int i=0; i< gtfsIDs.size(); i++){ + if(first){ + first = false; + } else{ + builder.append(", "); + } + builder.append("?");//.append("\"").append(id).append("\""); + } + builder.append(") "); + final String whereClause = builder.toString(); + + final String[] idsQuery = gtfsIDs.toArray(new String[0]); + + try { + final Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause, + idsQuery, + null, null, null); + stops.addAll(getStopsFromCursorAllFields(result)); + result.close(); + } catch(SQLiteException e) { + Log.e(DEBUG_TAG, "SQLiteException occurred"); + e.printStackTrace(); + + } + return stops; + } + /** * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all} * @param result cursor from query @@ -205,6 +261,7 @@ final int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION); final int colType = result.getColumnIndex(StopsTable.COL_TYPE); final int colLat = result.getColumnIndex(StopsTable.COL_LAT); + final int colGtfsID = result.getColumnIndex(StopsTable.COL_GTFS_ID); final int colLon = result.getColumnIndex(StopsTable.COL_LONG); final int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING); @@ -215,9 +272,15 @@ while(result.moveToNext()) { final String stopID = result.getString(colID).trim(); - final Route.Type type; - if(result.getString(colType) == null) type = Route.Type.BUS; - else type = Route.getTypeFromSymbol(result.getString(colType)); + Route.Type type; + //if(result.getString(colType) == null) type = Route.Type.BUS; + //else type = Route.getTypeFromSymbol(result.getString(colType)); + //if(result.getInt(colType) == null) type = Route.Type.BUS; + try{ + type = Route.Type.fromCode(result.getInt(colType)); + } catch (Exception e){ + type = Route.Type.BUS; + } String lines = result.getString(colLines).trim(); String locationSometimesEmpty = result.getString(colLocation); @@ -227,11 +290,39 @@ stops.add(new Stop(stopID, result.getString(colName), null, locationSometimesEmpty, type, splitLinesString(lines), - result.getDouble(colLat), result.getDouble(colLon)) + result.getDouble(colLat), result.getDouble(colLon), + result.getString(colGtfsID)) ); } return stops; } + /* + static ArrayList createStopListFromCursor(Cursor data){ + ArrayList stopList = new ArrayList<>(); + final int col_id = data.getColumnIndex(StopsTable.COL_ID); + final int latInd = data.getColumnIndex(StopsTable.COL_LAT); + final int lonInd = data.getColumnIndex(StopsTable.COL_LONG); + final int nameindex = data.getColumnIndex(StopsTable.COL_NAME); + final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE); + final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); + + data.moveToFirst(); + for(int i=0; i. + */ +package it.reyboz.bustorino.data; + +import android.database.sqlite.SQLiteDatabase; +import it.reyboz.bustorino.backend.Result; +import it.reyboz.bustorino.backend.Stop; + +import java.util.List; +import java.util.concurrent.Executor; + +public class OldDataRepository { + + private final Executor executor; + private final NextGenDB nextGenDB; + + public OldDataRepository(Executor executor, final NextGenDB nextGenDB) { + this.executor = executor; + this.nextGenDB = nextGenDB; + } + + public void requestStopsWithGtfsIDs(final List gtfsIDs, + final Callback> callback){ + executor.execute(() -> { + + try { + //final NextGenDB dbHelper = new NextGenDB(context); + final SQLiteDatabase db = nextGenDB.getReadableDatabase(); + + final List stops = NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs); + //Result> result = Result.success; + + callback.onComplete(Result.success(stops)); + } catch (Exception e){ + callback.onComplete(Result.failure(e)); + } + }); + } + + public interface Callback{ + void onComplete(Result result); + } +} diff --git a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt --- a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt +++ b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt @@ -19,13 +19,12 @@ import android.content.Context import android.util.Log -import java.util.ArrayList class CsvTableInserter( val tableName: String, context: Context ) { private val database: GtfsDatabase = GtfsDatabase.getGtfsDatabase(context) - private val dao: StaticGtfsDao = database.gtfsDao() + private val databaseDao: GtfsDBDao = database.gtfsDao() private val elementsList: MutableList< in GtfsTable> = mutableListOf() @@ -35,12 +34,12 @@ private var countInsert = 0 init { if(tableName == "stop_times") { - stopsIDsPresent = dao.getAllStopsIDs().toHashSet() - tripsIDsPresent = dao.getAllTripsIDs().toHashSet() + stopsIDsPresent = databaseDao.getAllStopsIDs().toHashSet() + tripsIDsPresent = databaseDao.getAllTripsIDs().toHashSet() Log.d(DEBUG_TAG, "num stop IDs present: "+ stopsIDsPresent!!.size) Log.d(DEBUG_TAG, "num trips IDs present: "+ tripsIDsPresent!!.size) } else if(tableName == "routes"){ - dao.deleteAllRoutes() + databaseDao.deleteAllRoutes() } } @@ -77,7 +76,7 @@ //have to insert if (tableName == "routes") - dao.insertRoutes(elementsList.filterIsInstance()) + databaseDao.insertRoutes(elementsList.filterIsInstance()) else insertDataInDatabase() @@ -90,21 +89,21 @@ countInsert += elementsList.size when(tableName){ "stops" -> { - dao.insertStops(elementsList.filterIsInstance()) + databaseDao.insertStops(elementsList.filterIsInstance()) } - "routes" -> dao.insertRoutes(elementsList.filterIsInstance()) - "calendar" -> dao.insertServices(elementsList.filterIsInstance()) - "calendar_dates" -> dao.insertDates(elementsList.filterIsInstance()) - "trips" -> dao.insertTrips(elementsList.filterIsInstance()) - "stop_times"-> dao.insertStopTimes(elementsList.filterIsInstance()) - "shapes" -> dao.insertShapes(elementsList.filterIsInstance()) + "routes" -> databaseDao.insertRoutes(elementsList.filterIsInstance()) + "calendar" -> databaseDao.insertServices(elementsList.filterIsInstance()) + "calendar_dates" -> databaseDao.insertDates(elementsList.filterIsInstance()) + "trips" -> databaseDao.insertTrips(elementsList.filterIsInstance()) + "stop_times"-> databaseDao.insertStopTimes(elementsList.filterIsInstance()) + "shapes" -> databaseDao.insertShapes(elementsList.filterIsInstance()) } ///if(elementsList.size < MAX_ELEMENTS) } fun finishInsert(){ insertDataInDatabase() - Log.d(DEBUG_TAG, "Inserted "+countInsert+" elements from "+tableName); + Log.d(DEBUG_TAG, "Inserted $countInsert elements from $tableName") } companion object{ diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt @@ -0,0 +1,55 @@ +package it.reyboz.bustorino.data.gtfs + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = GtfsAgency.TABLE_NAME) +data class GtfsAgency( + @PrimaryKey + @ColumnInfo(name = COL_GTFS_ID) + val gtfsId: String, + @ColumnInfo(name = COL_NAME) + val name: String, + @ColumnInfo(name = COL_URL) + val url: String, + @ColumnInfo(name = COL_FAREURL) + val fareUrl: String?, + @ColumnInfo(name = COL_PHONE) + val phone: String?, + @Embedded var feed: GtfsFeed? +): GtfsTable{ + constructor(valuesByColumn: Map) : this( + valuesByColumn[COL_GTFS_ID]!!, + valuesByColumn[COL_NAME]!!, + valuesByColumn[COL_URL]!!, + valuesByColumn[COL_FAREURL], + valuesByColumn[COL_PHONE], + null + ) + + companion object{ + const val TABLE_NAME="gtfs_agencies" + + const val COL_GTFS_ID="gtfs_id" + const val COL_NAME="ag_name" + const val COL_URL="ag_url" + const val COL_FAREURL = "fare_url" + const val COL_PHONE = "phone" + + val COLUMNS = arrayOf( + COL_GTFS_ID, + COL_NAME, + COL_URL, + COL_FAREURL, + COL_PHONE + ) + const val CREATE_SQL = + "CREATE TABLE $TABLE_NAME ( $COL_GTFS_ID )" + } + + override fun getColumns(): Array { + return COLUMNS + } +} diff --git a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt rename from src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt rename to src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt --- a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt @@ -21,8 +21,8 @@ import androidx.room.* @Dao -interface StaticGtfsDao { - @Query("SELECT * FROM "+GtfsRoute.DB_TABLE+" ORDER BY "+GtfsRoute.COL_SORT_ORDER) +interface GtfsDBDao { + @Query("SELECT * FROM "+GtfsRoute.DB_TABLE) fun getAllRoutes() : LiveData> @Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE) @@ -40,14 +40,32 @@ ) fun getShapeByID(shapeID: String) : LiveData> + + @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_AGENCY_ID} LIKE :agencyID") + fun getRoutesByAgency(agencyID:String) : LiveData> + + @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID") + fun getPatternsByRouteID(routeID: String): LiveData> + + @Query("SELECT * FROM ${PatternStop.TABLE_NAME} WHERE ${PatternStop.COL_PATTERN_ID} LIKE :patternGtfsID") + fun getStopsByPatternID(patternGtfsID: String): LiveData> + + @Transaction + @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID") + fun getPatternsWithStopsByRouteID(routeID: String): LiveData> + + fun getRoutesForFeed(feed:String): LiveData>{ + val agencyID = "${feed}:%" + return getRoutesByAgency(agencyID) + } @Transaction fun clearAndInsertRoutes(routes: List){ deleteAllRoutes() insertRoutes(routes) } - + @Transaction @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertRoutes(users: List) + fun insertRoutes(routes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStops(stops: List) @Insert(onConflict = OnConflictStrategy.REPLACE) @@ -87,4 +105,24 @@ @Query("DELETE FROM "+GtfsService.DB_TABLE) fun deleteAllServices() + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertFeeds(feeds: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAgencies(agencies: List) + + @Transaction + fun insertAgenciesWithFeeds(feeds: List, agencies: List){ + insertFeeds(feeds) + insertAgencies(agencies) + } + //patterns + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertPatterns(patterns: List) + + @Query("SELECT * FROM "+MatoPattern.TABLE_NAME+ + " WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeGtfsId") + fun getPatternsForRouteID(routeGtfsId: String) : List + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertPatternStops(patternStops: List) } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt --- a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt @@ -18,24 +18,30 @@ package it.reyboz.bustorino.data.gtfs import android.content.Context +import android.util.Log import androidx.room.* +import androidx.room.migration.Migration @Database( entities = [ + GtfsFeed::class, + GtfsAgency::class, GtfsServiceDate::class, GtfsStop::class, GtfsService::class, GtfsRoute::class, GtfsStopTime::class, GtfsTrip::class, - GtfsShape::class], + GtfsShape::class, + MatoPattern::class, + PatternStop::class + ], version = GtfsDatabase.VERSION, - exportSchema = false, ) @TypeConverters(Converters::class) -public abstract class GtfsDatabase : RoomDatabase() { +abstract class GtfsDatabase : RoomDatabase() { - abstract fun gtfsDao() : StaticGtfsDao + abstract fun gtfsDao() : GtfsDBDao companion object{ @Volatile @@ -44,14 +50,36 @@ fun getGtfsDatabase(context: Context): GtfsDatabase{ return INSTANCE ?: synchronized(this){ val instance = Room.databaseBuilder(context.applicationContext, - GtfsDatabase::class.java, - "gtfs_database").build() + GtfsDatabase::class.java, + "gtfs_database") + .addMigrations(MIGRATION_1_2) + .build() INSTANCE = instance instance } } - const val VERSION = 1 + const val VERSION = 2 const val FOREIGNKEY_ONDELETE = ForeignKey.CASCADE + + val MIGRATION_1_2 = Migration(1,2) { + Log.d("BusTO-Database", "Upgrading from version 1 to version 2 the Room Database") + //create table for feeds + it.execSQL("CREATE TABLE IF NOT EXISTS `gtfs_feeds` (`feed_id` TEXT NOT NULL, PRIMARY KEY(`feed_id`))") + //create table gtfs_agencies + it.execSQL("CREATE TABLE IF NOT EXISTS `gtfs_agencies` (`gtfs_id` TEXT NOT NULL, `ag_name` TEXT NOT NULL, `ag_url` TEXT NOT NULL, `fare_url` TEXT, `phone` TEXT, `feed_id` TEXT, PRIMARY KEY(`gtfs_id`))") + + //recreate routes + it.execSQL("DROP TABLE IF EXISTS `routes_table`") + it.execSQL("CREATE TABLE IF NOT EXISTS `routes_table` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_mode` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, PRIMARY KEY(`route_id`))") + + //create patterns and stops + it.execSQL("CREATE TABLE IF NOT EXISTS `mato_patterns` (`pattern_name` TEXT NOT NULL, `pattern_code` TEXT NOT NULL, `pattern_hash` TEXT NOT NULL, `pattern_direction_id` INTEGER NOT NULL, `pattern_route_id` TEXT NOT NULL, `pattern_headsign` TEXT, `pattern_polyline` TEXT NOT NULL, `pattern_polylength` INTEGER NOT NULL, PRIMARY KEY(`pattern_code`), FOREIGN KEY(`pattern_route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )") + it.execSQL("CREATE TABLE IF NOT EXISTS `patterns_stops` (`pattern_gtfs_id` TEXT NOT NULL, `stop_gtfs_id` TEXT NOT NULL, `stop_order` INTEGER NOT NULL, PRIMARY KEY(`pattern_gtfs_id`, `stop_gtfs_id`, `stop_order`), FOREIGN KEY(`pattern_gtfs_id`) REFERENCES `mato_patterns`(`pattern_code`) ON UPDATE NO ACTION ON DELETE CASCADE )") + + + } + + } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt @@ -0,0 +1,50 @@ +/* + BusTO - Data components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.data.gtfs + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = GtfsFeed.TABLE_NAME) +data class GtfsFeed( + @PrimaryKey + @ColumnInfo(name = COL_GTFS_ID) + val gtfsId: String, +): GtfsTable{ + constructor(valuesByColumn: Map) : this( + valuesByColumn[COL_GTFS_ID]!!, + ) + + companion object{ + const val TABLE_NAME="gtfs_feeds" + + const val COL_GTFS_ID="feed_id" + + + val COLUMNS = arrayOf( + COL_GTFS_ID, + ) + const val CREATE_SQL = + "CREATE TABLE $TABLE_NAME ( $COL_GTFS_ID )" + } + + override fun getColumns(): Array { + return COLUMNS + } +} diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt @@ -0,0 +1,19 @@ +package it.reyboz.bustorino.data.gtfs + +enum class GtfsMode(val intType: Int) { + TRAM(0), + SUBWAY(1), + RAIL(2), + BUS(3), + FERRY(4), + CABLE_TRAM(5), + GONDOLA(6), + FUNICULAR(7), + TROLLEYBUS(11), + MONORAIL(12); + + companion object { + private val VALUES = values() + fun getByValue(value: Int) = VALUES.firstOrNull { it.intType == value } + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt --- a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt @@ -23,26 +23,25 @@ @Entity(tableName=GtfsRoute.DB_TABLE) data class GtfsRoute( - @PrimaryKey @ColumnInfo(name = COL_ROUTE_ID) - val ID: String, - @ColumnInfo(name = "agency_id") + @PrimaryKey @ColumnInfo(name = COL_ROUTE_ID) + val gtfsId: String, + @ColumnInfo(name = COL_AGENCY_ID) val agencyID: String, - @ColumnInfo(name = "route_short_name") + @ColumnInfo(name = "route_short_name") val shortName: String, - @ColumnInfo(name = "route_long_name") + @ColumnInfo(name = "route_long_name") val longName: String, - @ColumnInfo(name = "route_desc") + @ColumnInfo(name = "route_desc") val description: String, - @ColumnInfo(name ="route_type") - val type: String, + @ColumnInfo(name = COL_MODE) + val mode: GtfsMode, //@ColumnInfo(name ="route_url") //val url: String, - @ColumnInfo(name ="route_color") + @ColumnInfo(name = COL_COLOR) val color: String, - @ColumnInfo(name ="route_text_color") + @ColumnInfo(name = COL_TEXT_COLOR) val textColor: String, - @ColumnInfo(name = COL_SORT_ORDER) - val sortOrder: Int + ): GtfsTable { constructor(valuesByColumn: Map) : this( @@ -51,18 +50,21 @@ valuesByColumn["route_short_name"]!!, valuesByColumn["route_long_name"]!!, valuesByColumn["route_desc"]!!, - valuesByColumn["route_type"]!!, - valuesByColumn["route_color"]!!, - valuesByColumn["route_text_color"]!!, - valuesByColumn[COL_SORT_ORDER]?.toInt()!! + valuesByColumn["route_type"]?.toInt()?.let { GtfsMode.getByValue(it) }!!, + valuesByColumn[COL_COLOR]!!, + valuesByColumn[COL_TEXT_COLOR]!!, ) companion object { const val DB_TABLE: String="routes_table" const val COL_SORT_ORDER: String="route_sort_order" + const val COL_AGENCY_ID = "agency_id" const val COL_ROUTE_ID = "route_id" + const val COL_MODE ="route_mode" + const val COL_COLOR="route_color" + const val COL_TEXT_COLOR="route_text_color" val COLUMNS = arrayOf(COL_ROUTE_ID, - "agency_id", + COL_AGENCY_ID, "route_short_name", "route_long_name", "route_desc", @@ -71,6 +73,8 @@ "route_text_color", COL_SORT_ORDER ) + + //const val CREATE_SQL = "" } override fun getColumns(): Array { diff --git a/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt @@ -0,0 +1,140 @@ +/* + BusTO - Data components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.data.gtfs + +import androidx.room.* +import it.reyboz.bustorino.backend.Stop + +@Entity(tableName = MatoPattern.TABLE_NAME, + foreignKeys = [ + ForeignKey(entity = GtfsRoute::class, + parentColumns = [GtfsRoute.COL_ROUTE_ID], + childColumns = [MatoPattern.COL_ROUTE_ID], + onDelete = ForeignKey.CASCADE, + ) + ] +) +data class MatoPattern( + @ColumnInfo(name= COL_NAME) + val name: String, + @ColumnInfo(name= COL_CODE) + @PrimaryKey + val code: String, + @ColumnInfo(name= COL_SEMANTIC_HASH) + val semanticHash: String, + @ColumnInfo(name= COL_DIRECTION_ID) + val directionId: Int, + @ColumnInfo(name= COL_ROUTE_ID) + val routeGtfsId: String, + @ColumnInfo(name= COL_HEADSIGN) + var headsign: String?, + @ColumnInfo(name= COL_GEOMETRY_POLY) + val patternGeometryPoly: String, + @ColumnInfo(name= COL_GEOMETRY_LENGTH) + val patternGeometryLength: Int, + @Ignore + val stopsGtfsIDs: ArrayList + +):GtfsTable{ + + @Ignore + val servingStops= ArrayList(4) + constructor( + name: String, code:String, + semanticHash: String, directionId: Int, + routeGtfsId: String, headsign: String?, + patternGeometryPoly: String, patternGeometryLength: Int + ): this(name, code, semanticHash, directionId, routeGtfsId, headsign, patternGeometryPoly, patternGeometryLength, ArrayList(4)) + + companion object{ + const val TABLE_NAME="mato_patterns" + + const val COL_NAME="pattern_name" + const val COL_CODE="pattern_code" + const val COL_ROUTE_ID="pattern_route_id" + const val COL_SEMANTIC_HASH="pattern_hash" + const val COL_DIRECTION_ID="pattern_direction_id" + const val COL_HEADSIGN="pattern_headsign" + const val COL_GEOMETRY_POLY="pattern_polyline" + const val COL_GEOMETRY_LENGTH="pattern_polylength" + + val COLUMNS = arrayOf( + COL_NAME, + COL_CODE, + COL_ROUTE_ID, + COL_SEMANTIC_HASH, + COL_DIRECTION_ID, + COL_HEADSIGN, + COL_GEOMETRY_POLY, + COL_GEOMETRY_LENGTH + ) + } + override fun getColumns(): Array { + return COLUMNS + } +} + +//DO NOT USE EMBEDDED!!! -> copies all data + +@Entity(tableName=PatternStop.TABLE_NAME, + primaryKeys = [ + PatternStop.COL_PATTERN_ID, + PatternStop.COL_STOP_GTFS, + PatternStop.COL_ORDER + ], + foreignKeys = [ + ForeignKey(entity = MatoPattern::class, + parentColumns = [MatoPattern.COL_CODE], + childColumns = [PatternStop.COL_PATTERN_ID], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class PatternStop( + @ColumnInfo(name= COL_PATTERN_ID) + val patternId: String, + @ColumnInfo(name=COL_STOP_GTFS) + val stopGtfsId: String, + @ColumnInfo(name=COL_ORDER) + val order: Int, +){ + companion object{ + const val TABLE_NAME="patterns_stops" + + const val COL_PATTERN_ID="pattern_gtfs_id" + const val COL_STOP_GTFS="stop_gtfs_id" + const val COL_ORDER="stop_order" + } +} + +data class MatoPatternWithStops( + @Embedded val pattern: MatoPattern, + @Relation( + parentColumn = MatoPattern.COL_CODE, + entityColumn = PatternStop.COL_PATTERN_ID, + + ) + var stopsIndices: List) + { + + + init { + stopsIndices = stopsIndices.sortedBy { p-> p.order } + + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java --- a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -19,7 +19,6 @@ import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.ContextMenu; @@ -44,9 +43,8 @@ import java.util.ArrayList; import java.util.List; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.*; -import it.reyboz.bustorino.adapters.AdapterListener; +import it.reyboz.bustorino.adapters.StopAdapterListener; import it.reyboz.bustorino.adapters.StopRecyclerAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.FavoritesViewModel; @@ -64,11 +62,17 @@ public static final String FRAGMENT_TAG = "BusTOFavFragment"; - private final AdapterListener adapterListener = new AdapterListener() { + private final StopAdapterListener adapterListener = new StopAdapterListener() { @Override public void onTappedStop(Stop stop) { mListener.requestArrivalsForStopID(stop.ID); } + + @Override + public boolean onLongPressOnStop(Stop stop) { + Log.d("BusTO-FavoritesFrag", "LongPressOnStop"); + return true; + } }; @@ -123,6 +127,7 @@ angeryBusImageView = root.findViewById(R.id.angeryBusImageView); favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView); + //register for the context menu registerForContextMenu(favoriteRecyclerView); FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class); @@ -150,12 +155,13 @@ } /* This method is apparently NOT CALLED ANYMORE + Called on Android 6 */ @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - Log.d("Favorites Fragment", "Creating context menu on "+v); + Log.d("Favorites Fragment", "Creating context menu "); if (v.getId() == R.id.favoritesRecyclerView) { // if we aren't attached to activity, return null if (getActivity()==null) return; @@ -244,7 +250,7 @@ * redrwaing everything. */ // Show results - favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener)); + favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener, StopRecyclerAdapter.Use.FAVORITES)); } public void showBusStopUsernameInputDialog(final Stop busStop) { diff --git a/src/it/reyboz/bustorino/fragments/FragmentKind.java b/src/it/reyboz/bustorino/fragments/FragmentKind.java --- a/src/it/reyboz/bustorino/fragments/FragmentKind.java +++ b/src/it/reyboz/bustorino/fragments/FragmentKind.java @@ -18,5 +18,6 @@ package it.reyboz.bustorino.fragments; public enum FragmentKind { - STOPS,ARRIVALS,FAVORITES,NEARBY_STOPS,NEARBY_ARRIVALS, MAP, MAIN_SCREEN_FRAGMENT + STOPS,ARRIVALS,FAVORITES,NEARBY_STOPS,NEARBY_ARRIVALS, MAP, MAIN_SCREEN_FRAGMENT, + LINES } diff --git a/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java b/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java --- a/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java +++ b/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java @@ -17,6 +17,9 @@ */ package it.reyboz.bustorino.fragments; +/** + * This interface is for the subfragments + */ public interface FragmentListenerMain extends CommonFragmentListener { void toggleSpinner(boolean state); diff --git a/src/it/reyboz/bustorino/fragments/LinesFragment.kt b/src/it/reyboz/bustorino/fragments/LinesFragment.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/LinesFragment.kt @@ -0,0 +1,289 @@ +/* + BusTO - Fragments components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.fragments + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.* +import android.widget.* +import android.widget.AdapterView.INVALID_POSITION +import android.widget.AdapterView.OnItemSelectedListener +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import it.reyboz.bustorino.R +import it.reyboz.bustorino.adapters.NameCapitalize +import it.reyboz.bustorino.adapters.StopAdapterListener +import it.reyboz.bustorino.adapters.StopRecyclerAdapter +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops +import it.reyboz.bustorino.data.gtfs.PatternStop +import it.reyboz.bustorino.util.LinesNameSorter + +class LinesFragment : ScreenBaseFragment() { + + companion object { + fun newInstance(){ + val fragment = LinesFragment() + } + const val DEBUG_TAG="BusTO-LinesFragment" + const val FRAGMENT_TAG="LinesFragment" + } + + + private lateinit var viewModel: LinesViewModel + + private lateinit var linesSpinner: Spinner + private lateinit var patternsSpinner: Spinner + + private lateinit var currentRoutes: List + private lateinit var currentPatterns: List + private lateinit var currentPatternStops: List + + private lateinit var routeDescriptionTextView: TextView + private lateinit var stopsRecyclerView: RecyclerView + + private var linesAdapter: ArrayAdapter? = null + private var patternsAdapter: ArrayAdapter? = null + private var mListener: CommonFragmentListener? = null + + private val linesNameSorter = LinesNameSorter() + private val linesComparator = Comparator { a,b -> + return@Comparator linesNameSorter.compare(a.shortName, b.shortName) + } + + private val adapterListener = object : StopAdapterListener { + override fun onTappedStop(stop: Stop?) { + //var r = "" + //stop?.let { r= it.stopDisplayName.toString() } + Toast.makeText(context,R.string.long_press_for_options,Toast.LENGTH_SHORT).show() + } + + override fun onLongPressOnStop(stop: Stop?): Boolean { + Log.d("BusTO-LinesFrag", "LongPressOnStop") + return true + } + } + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val rootView = inflater.inflate(R.layout.fragment_lines, container, false) + + linesSpinner = rootView.findViewById(R.id.linesSpinner) + patternsSpinner = rootView.findViewById(R.id.patternsSpinner) + + routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView) + stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) + + /* + Stop busStop = (Stop) parent.getItemAtPosition(position); + + if(mListener!=null){ + mListener.requestArrivalsForStopID(busStop.ID); + } + + }); + + */ + val llManager = LinearLayoutManager(context) + llManager.orientation = LinearLayoutManager.VERTICAL + + stopsRecyclerView.layoutManager = llManager + //allow the context menu to be opened + registerForContextMenu(stopsRecyclerView) + + if(context!=null) { + patternsAdapter = ArrayAdapter(context!!, android.R.layout.simple_spinner_dropdown_item, ArrayList()) + patternsSpinner.adapter = patternsAdapter + linesAdapter = ArrayAdapter(context!!, android.R.layout.simple_spinner_dropdown_item, ArrayList()) + linesSpinner.adapter = linesAdapter + + + linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{ + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) { + val selRoute = currentRoutes.get(pos) + + routeDescriptionTextView.text = selRoute.longName + + viewModel.setRouteIDQuery(selRoute.gtfsId) + + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + + } + } + + patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{ + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { + val patternWithStops = currentPatterns.get(position) + + setPatternAndReqStops(patternWithStops) + + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + } + } + } + + return rootView + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if(context is CommonFragmentListener) + mListener = context + else throw RuntimeException(context.toString() + + " must implement CommonFragmentListener") + } + + override fun onResume() { + super.onResume() + mListener?.readyGUIfor(FragmentKind.LINES) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) + + //val lines = viewModel.(); + viewModel.routesGTTLiveData.observe(this) { + setRoutes(it) + } + + viewModel.patternsWithStopsByRouteLiveData.observe(this){ + patterns -> + run { + currentPatterns = patterns.sortedBy { p->"${p.pattern.directionId} - ${p.pattern.headsign}" } + patternsAdapter?.let { + it.clear() + it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) + it.notifyDataSetChanged() + } + + val pos = patternsSpinner.selectedItemPosition + if(pos!= INVALID_POSITION){ + setPatternAndReqStops(currentPatterns[pos]) + } + + } + } + + viewModel.stopsForPatternLiveData.observe(this){stops-> + Log.d("BusTO-LinesFragment", "Setting stops from DB") + setCurrentStops(stops) + } + + } + + override fun getBaseViewForSnackBar(): View? { + return null + } + + private fun setRoutes(routes: List){ + currentRoutes = routes.sortedWith(linesComparator) + + linesAdapter?.clear() + linesAdapter?.addAll(currentRoutes.map { r -> r.shortName }) + linesAdapter?.notifyDataSetChanged() + } + + private fun setCurrentStops(stops: List){ + + val orderBy = currentPatternStops.withIndex().associate{it.value.stopGtfsId to it.index} + val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } + val numStops = stopsSorted.size + Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") + + var setNewAdapter = true; + if(stopsRecyclerView.adapter is StopRecyclerAdapter){ + val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter + if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID) + && (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID) + ){ + Log.d(DEBUG_TAG, "Found same stops on recyclerview") + setNewAdapter = false + } + /*else { + Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops") + adapter.stops = stopsSorted + adapter.notifyDataSetChanged() + }*/ + + } + if(setNewAdapter){ + stopsRecyclerView.adapter = StopRecyclerAdapter( + stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES, + NameCapitalize.FIRST + ) + } + + + } + + private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ + Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") + currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order } + + viewModel.requestStopsForPatternWithStops(patternWithStops) + } + + override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { + super.onCreateContextMenu(menu, v, menuInfo) + Log.d("BusTO-LinesFragment", "Creating context menu ") + + + if (v.id == R.id.patternStopsRecyclerView) { + // if we aren't attached to activity, return null + if (activity == null) return + val inflater = activity!!.menuInflater + inflater.inflate(R.menu.menu_line_item, menu) + + + } + } + + + override fun onContextItemSelected(item: MenuItem): Boolean { + val info = item.getMenuInfo(); + + if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false + val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter + val stop = adapter.stops.get(adapter.getPosition()) + + val acId = item.itemId + if(acId == R.id.action_view_on_map){ + // view on the map + if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) { + Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show() + return true + } + mListener!!.showMapCenteredOnStop(stop) + return true + } else if (acId == R.id.action_show_arrivals){ + mListener?.requestArrivalsForStopID(stop.ID); + return true + } + return false + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt @@ -0,0 +1,77 @@ +package it.reyboz.bustorino.fragments + +import android.app.Application +import android.util.Log +import androidx.lifecycle.* +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.GtfsRepository +import it.reyboz.bustorino.data.NextGenDB +import it.reyboz.bustorino.data.OldDataRepository +import it.reyboz.bustorino.data.gtfs.GtfsDatabase +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops +import java.util.concurrent.Executors + +class LinesViewModel(application: Application) : AndroidViewModel(application) { + + private val gtfsRepo: GtfsRepository + private val oldRepo: OldDataRepository + //val patternsByRouteLiveData: LiveData> + + private val routeIDToSearch = MutableLiveData() + + val stopsForPatternLiveData = MutableLiveData>() + val executor = Executors.newFixedThreadPool(2) + + init { + val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() + gtfsRepo = GtfsRepository(gtfsDao) + + oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + + } + + val routesGTTLiveData: LiveData> by lazy{ + gtfsRepo.getLinesLiveDataForFeed("gtt") + } + val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { + gtfsRepo.getPatternsWithStopsForRouteID(it) + + } + val routesName: LiveData> = Transformations.map(routesGTTLiveData) { + it.map { route -> route.longName } + } + + fun setRouteIDQuery(routeID: String){ + routeIDToSearch.value = routeID + } + + fun requestStopsForGTFSIDs(gtfsIDs: List){ + oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { + if (it.isSuccess) { + stopsForPatternLiveData.postValue(it.result) + } else { + Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") + it.exception?.printStackTrace() + } + } + } + + fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ + val gtfsIDs = ArrayList() + for(pat in patternStops.stopsIndices){ + gtfsIDs.add(pat.stopGtfsId) + } + requestStopsForGTFSIDs(gtfsIDs) + } + + + /*fun getLinesGTT(): MutableLiveData> { + val routesData = MutableLiveData>() + viewModelScope.launch { + val routes=gtfsRepo.getLinesForFeed("gtt") + routesData.postValue(routes) + } + return routesData + }*/ +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/MapFragment.java b/src/it/reyboz/bustorino/fragments/MapFragment.java --- a/src/it/reyboz/bustorino/fragments/MapFragment.java +++ b/src/it/reyboz/bustorino/fragments/MapFragment.java @@ -610,7 +610,7 @@ final BoundingBoxLimit limit = limits[0]; //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); - NextGenDB dbHelper = new NextGenDB(fragmentWeakReference.get().getContext()); + NextGenDB dbHelper = NextGenDB.getInstance(fragmentWeakReference.get().getContext()); ArrayList stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, limit.longFrom, limit.latitTo); dbHelper.close(); diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; @@ -36,6 +37,8 @@ import androidx.appcompat.widget.AppCompatButton; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.work.WorkInfo; + import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -49,6 +52,9 @@ import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType; +import it.reyboz.bustorino.backend.mato.MapiArrivalRequest; +import it.reyboz.bustorino.data.DatabaseUpdate; +import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.*; @@ -62,8 +68,7 @@ private FragmentListenerMain mListener; private FragmentLocationListener fragmentLocationListener; - private final String[] PROJECTION = {StopsTable.COL_ID,StopsTable.COL_LAT,StopsTable.COL_LONG, - StopsTable.COL_NAME,StopsTable.COL_TYPE,StopsTable.COL_LINES_STOPPING}; + private final static String DEBUG_TAG = "NearbyStopsFragment"; private final static String FRAGMENT_TYPE_KEY = "FragmentType"; public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; @@ -78,7 +83,6 @@ private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; - boolean canStartDBQuery = true; private Location lastReceivedLocation = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance; @@ -100,6 +104,10 @@ private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; + private boolean dbUpdateRunning = false; + + private ArrayList currentNearbyStops = new ArrayList<>(); + public NearbyStopsFragment() { // Required empty public constructor } @@ -129,10 +137,10 @@ } locManager = AppLocationManager.getInstance(getContext()); fragmentLocationListener = new FragmentLocationListener(this); - globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences),Context.MODE_PRIVATE); - - - globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + if (getContext()!=null) { + globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); + globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + } } @@ -153,51 +161,28 @@ titleTextView = root.findViewById(R.id.titleTextView); switchButton = root.findViewById(R.id.switchButton); - preferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { + scrollListener = new CommonScrollListener(mListener,false); + switchButton.setOnClickListener(v -> switchFragmentType()); + Log.d(DEBUG_TAG, "onCreateView"); + + DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Log.d(DEBUG_TAG,"Key "+key+" was changed"); - if(key.equals(getString(R.string.databaseUpdatingPref))){ - if(!sharedPreferences.getBoolean(getString(R.string.databaseUpdatingPref),true)){ - canStartDBQuery = true; - Log.d(DEBUG_TAG,"The database has finished updating, can start update now"); - } + public void onChanged(List workInfos) { + if(workInfos.isEmpty()) return; + + WorkInfo wi = workInfos.get(0); + if (wi.getState() == WorkInfo.State.RUNNING && locManager.isRequesterRegistered(fragmentLocationListener)) { + locManager.removeLocationRequestFor(fragmentLocationListener); + dbUpdateRunning = true; + } else if(!locManager.isRequesterRegistered(fragmentLocationListener)){ + locManager.addLocationRequestFor(fragmentLocationListener); + dbUpdateRunning = false; } } - }; - scrollListener = new CommonScrollListener(mListener,false); - switchButton.setOnClickListener(v -> { - switchFragmentType(); }); - Log.d(DEBUG_TAG, "onCreateView"); return root; } - protected ArrayList createStopListFromCursor(Cursor data){ - ArrayList stopList = new ArrayList<>(); - final int col_id = data.getColumnIndex(StopsTable.COL_ID); - final int latInd = data.getColumnIndex(StopsTable.COL_LAT); - final int lonInd = data.getColumnIndex(StopsTable.COL_LONG); - final int nameindex = data.getColumnIndex(StopsTable.COL_NAME); - final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE); - final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); - - data.moveToFirst(); - for(int i=0; i onCreateLoader(int id, Bundle args) { //BUILD URI - lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); + if (args!=null) + lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); Uri.Builder builder = new Uri.Builder(); builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stops").appendPath("location") .appendPath(String.valueOf(lastReceivedLocation.getLatitude())) .appendPath(String.valueOf(lastReceivedLocation.getLongitude())) .appendPath(String.valueOf(distance)); //distance - CursorLoader cl = new CursorLoader(getContext(),builder.build(),PROJECTION,null,null,null); + CursorLoader cl = new CursorLoader(getContext(),builder.build(),NextGenDB.QUERY_COLUMN_stops_all,null,null,null); cl.setUpdateThrottle(2000); return cl; } @@ -331,50 +316,58 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { if (0 > MAX_DISTANCE) throw new AssertionError(); //Cursor might be null - if(cursor==null){ - Log.e(DEBUG_TAG,"Null cursor, something really wrong happened"); + if (cursor == null) { + Log.e(DEBUG_TAG, "Null cursor, something really wrong happened"); return; } - Log.d(DEBUG_TAG, "Num stops found: "+cursor.getCount()+", Current distance: "+distance); + Log.d(DEBUG_TAG, "Num stops found: " + cursor.getCount() + ", Current distance: " + distance); - if(!isDBUpdating() && (cursor.getCount()0) + currentNearbyStops = NextGenDB.getStopsFromCursorAllFields(cursor); - if(cursor.getCount()>0) { - ArrayList stopList = createStopListFromCursor(cursor); - double minDistance = Double.POSITIVE_INFINITY; - for(Stop s: stopList){ - minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); - } + showCurrentStops(); + } + /** + * Display the stops, or run new set of requests for arrivals + */ + private void showCurrentStops(){ + if (currentNearbyStops.isEmpty()) { + setNoStopsLayout(); + return; + } - //quick trial to hopefully always get the stops in the correct order - Collections.sort(stopList,new StopSorterByDistance(lastReceivedLocation)); - switch (fragment_type){ - case TYPE_STOPS: - showStopsInRecycler(stopList); - break; - case TYPE_ARRIVALS: - arrivalsManager = new ArrivalsManager(stopList); - flatProgressBar.setVisibility(View.VISIBLE); - flatProgressBar.setProgress(0); - flatProgressBar.setIndeterminate(false); - //for the moment, be satisfied with only one location - //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); - break; - default: - } + double minDistance = Double.POSITIVE_INFINITY; + for(Stop s: currentNearbyStops){ + minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); + } - } else { - setNoStopsLayout(); + + //quick trial to hopefully always get the stops in the correct order + Collections.sort(currentNearbyStops,new StopSorterByDistance(lastReceivedLocation)); + switch (fragment_type){ + case TYPE_STOPS: + showStopsInRecycler(currentNearbyStops); + break; + case TYPE_ARRIVALS: + arrivalsManager = new ArrivalsManager(currentNearbyStops); + flatProgressBar.setVisibility(View.VISIBLE); + flatProgressBar.setProgress(0); + flatProgressBar.setIndeterminate(false); + //for the moment, be satisfied with only one location + //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); + break; + default: } } @@ -411,15 +404,12 @@ gridRecyclerView.setAdapter(arrivalsStopAdapter); } fragmentLocationListener.lastUpdateTime = -1; - locManager.removeLocationRequestFor(fragmentLocationListener); - locManager.addLocationRequestFor(fragmentLocationListener); + //locManager.removeLocationRequestFor(fragmentLocationListener); + //locManager.addLocationRequestFor(fragmentLocationListener); + showCurrentStops(); } //useful methods - protected boolean isDBUpdating(){ - return globalSharedPref.getBoolean(getString(R.string.databaseUpdatingPref),false); - } - /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ @@ -454,7 +444,8 @@ if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything - routesPairList.add(new Pair<>(p,r)); + if (r.passaggi != null && !r.passaggi.isEmpty()) + routesPairList.add(new Pair<>(p,r)); } } if (getContext()==null){ @@ -493,31 +484,30 @@ messageTextView.setVisibility(View.GONE); } - class ArrivalsManager implements FiveTAPIVolleyRequest.ResponseListener, Response.ErrorListener{ - final HashMap mStops; - final Map> routesToAdd = new HashMap<>(); + class ArrivalsManager implements Response.Listener, Response.ErrorListener{ + final HashMap palinasDone = new HashMap<>(); + //final Map> routesToAdd = new HashMap<>(); final static String REQUEST_TAG = "NearbyArrivals"; - private final QueryType[] types = {QueryType.ARRIVALS,QueryType.DETAILS}; final NetworkVolleyManager volleyManager; int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0; ArrivalsManager(List stops){ - mStops = new HashMap<>(); volleyManager = NetworkVolleyManager.getInstance(getContext()); int MAX_ARRIVAL_STOPS = 35; + Date currentDate = new Date(); + int timeRange = 3600; + int departures = 10; + int numreq = 0; for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){ - mStops.put(s.ID,new Palina(s)); - for(QueryType t: types) { - final FiveTAPIVolleyRequest req = FiveTAPIVolleyRequest.getNewRequest(t, s.ID, this, this); - if (req != null) { - req.setTag(REQUEST_TAG); - volleyManager.addToRequestQueue(req); - activeRequestCount++; - } - } + + final MapiArrivalRequest req = new MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this); + req.setTag(REQUEST_TAG); + volleyManager.addToRequestQueue(req); + activeRequestCount++; + numreq++; } - flatProgressBar.setMax(activeRequestCount); + flatProgressBar.setMax(numreq); } @@ -546,40 +536,19 @@ } @Override - public void onResponse(Palina result, QueryType type) { + public void onResponse(Palina result) { //counter for requests activeRequestCount--; reqSuccessCount++; - - - final Palina palinaInMap = mStops.get(result.ID); + //final Palina palinaInMap = palinasDone.get(result.ID); //palina cannot be null here //sorry for the brutal crash when it happens - if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); - //necessary to split the Arrivals and Details cases - switch (type){ - case ARRIVALS: - palinaInMap.addInfoFromRoutes(result.queryAllRoutes()); - final List possibleRoutes = routesToAdd.get(result.ID); - if(possibleRoutes!=null) { - palinaInMap.addInfoFromRoutes(possibleRoutes); - routesToAdd.remove(result.ID); - } - break; - case DETAILS: - if(palinaInMap.queryAllRoutes().size()>0){ - //merge the branches - palinaInMap.addInfoFromRoutes(result.queryAllRoutes()); - } else { - routesToAdd.put(result.ID,result.queryAllRoutes()); - } - break; - default: - throw new IllegalArgumentException("Wrong QueryType in onResponse"); - } - + //if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); + //add the palina to the successful one + //TODO: Avoid redoing everything every time a new Result arrives + palinasDone.put(result.ID, result); final ArrayList outList = new ArrayList<>(); - for(Palina p: mStops.values()){ + for(Palina p: palinasDone.values()){ final List routes = p.queryAllRoutes(); if(routes!=null && routes.size()>0) outList.add(p); } @@ -613,14 +582,14 @@ public void onLocationChanged(Location location) { //set adapter float accuracy = location.getAccuracy(); - if(accuracy<60 && canStartDBQuery) { + if(accuracy<60 && !dbUpdateRunning) { distance = 20; final Bundle msgBundle = new Bundle(); msgBundle.putParcelable(BUNDLE_LOCATION,location); getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); } lastUpdateTime = System.currentTimeMillis(); - Log.d("BusTO:NearPositListen","can start loader "+ canStartDBQuery); + Log.d("BusTO:NearPositListen","can start loader "+ !dbUpdateRunning); } @Override diff --git a/src/it/reyboz/bustorino/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java --- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -25,10 +25,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.*; +import androidx.room.Database; import it.reyboz.bustorino.R; +import it.reyboz.bustorino.data.DatabaseUpdate; import java.lang.ref.WeakReference; @@ -63,6 +66,25 @@ //ListPreference preference = findPreference(R.string.arrival_times) + Preference dbUpdateNow = findPreference("pref_db_update_now"); + if (dbUpdateNow!=null) + dbUpdateNow.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + //trigger update + if(getContext()!=null) { + DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true); + Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show(); + return true; + } + return false; + } + } + ); + else { + Log.e("BusTO-Preferences", "Cannot find db update preference"); + } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java b/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java --- a/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java +++ b/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java @@ -250,7 +250,7 @@ @Override public void run() { - final NextGenDB nextGenDB = new NextGenDB(context); + final NextGenDB nextGenDB = NextGenDB.getInstance(context); //ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList branchesValues = new ArrayList<>(routesToInsert.size()*4); ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); diff --git a/src/it/reyboz/bustorino/util/LinesNameSorter.java b/src/it/reyboz/bustorino/util/LinesNameSorter.java --- a/src/it/reyboz/bustorino/util/LinesNameSorter.java +++ b/src/it/reyboz/bustorino/util/LinesNameSorter.java @@ -23,19 +23,93 @@ public class LinesNameSorter implements Comparator { @Override public int compare(String name1, String name2) { + name1 = name1.trim(); + name2 = name2.trim(); + /* if(name1.length()>name2.length()) return 1; if(name1.length()==name2.length()) { + + try{ int num1 = Integer.parseInt(name1.trim()); int num2 = Integer.parseInt(name2.trim()); return num1-num2; } catch (NumberFormatException ex){ //Log.d("BUSTO Compare lines","Cannot compare lines "+name1+" and "+name2); + //return name1.compareTo(name2); + //One of them is not a line + String trim1 = name1.substring(0, name1.length() - 1).trim(); + String trim2 = name2.substring(0, name2.length()-1).trim(); + if(isInteger(trim1)){ //cut away the last part + //this means it's a line + return compare(trim1, name2); + } else if(isInteger(trim2)){ + return compare(name1,trim2); + } + return name1.compareTo(name2); + } + }**/ + //One of them is not + int num1 = -1; + if(isInteger(name1)) num1 = Integer.parseInt(name1); + int num2 = -1; + if (isInteger(name2)) num2 = Integer.parseInt(name2); + + if(num1 >= 0 && num2 >=0){ + //we're very happy + return (num1-num2)*10; + } else if (num1>=0) { + //name2 is not fully integer + final String name2sub = name2.substring(0, name2.length()-1).trim(); + char lastchar = name2.charAt(name2.length()-1); + if(isInteger(name2sub)){ + num2 = Integer.parseInt(name2sub); + int diff = (num1-num2)*10; + return diff - incrementFromLastChar(lastchar); + } else{ + //failed + return name1.compareTo(name2); + } + } else if (num2>=0) { + //name1 is not fully integer + final String name1sub = name1.substring(0, name1.length()-1).trim(); + char lastchar = name1.charAt(name1.length()-1); + if (isInteger(name1sub)){ + num1 = Integer.parseInt(name1sub); + int diff = (num1-num2)*10; + return diff + incrementFromLastChar(lastchar); + } else { return name1.compareTo(name2); } } - return -1; + //last case + return name1.compareTo(name2); } + + public static boolean isInteger(String strNum) { + if (strNum == null) { + return false; + } + try { + int d = Integer.parseInt(strNum); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } + private static int incrementFromLastChar(char lastchar){ + switch (lastchar){ + case 'B': + case 'b': + case '/': + return 1; + case 'n': + case 'N': + return 3; + default: + return 6; + } + } }