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 index 0000000..3074f41 --- /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 index 0000000..678076f --- /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 index 68c4d6a..922ad48 100644 --- a/build.gradle +++ b/build.gradle @@ -1,145 +1,175 @@ buildscript { repositories { jcenter() maven { url 'https://maven.google.com' } google() } ext { + androidXTestVersion = "1.4.0" + //multidex multidex_version = "2.0.1" //libraries versions fragment_version = "1.4.1" activity_version = "1.4.0" appcompat_version = "1.4.1" preference_version = "1.2.0" work_version = "2.7.1" acra_version = "5.7.0" lifecycle_version = "2.4.1" arch_version = "2.1.0" room_version = "2.4.1" //kotlin kotlin_version = '1.6.0' coroutines_version = "1.5.0" } dependencies { classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { jcenter() maven { url 'https://maven.google.com' } google() mavenCentral() } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion 31 buildToolsVersion '30.0.3' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 16 targetSdkVersion 31 versionCode 39 versionName "1.16.3" vectorDrawables.useSupportLibrary = true multiDexEnabled true + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] + } + } + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { + androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) + main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } } lintOptions { abortOnError false } repositories { jcenter() mavenLocal() } 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" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "com.google.android.material:material:1.5.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.14.0' // 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 index 0000000..57a1bbb --- /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 index 0000000..4dbee23 --- /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 index 570f689..a728c51 100644 --- a/res/layout/arrivals_nearby_card.xml +++ b/res/layout/arrivals_nearby_card.xml @@ -1,91 +1,98 @@ - + + \ No newline at end of file diff --git a/res/layout/bus_stop_line_elmt.xml b/res/layout/bus_stop_line_elmt.xml new file mode 100644 index 0000000..75e65ec --- /dev/null +++ b/res/layout/bus_stop_line_elmt.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/entry_bus_stop.xml b/res/layout/entry_bus_stop.xml index ee45178..f6cd113 100644 --- a/res/layout/entry_bus_stop.xml +++ b/res/layout/entry_bus_stop.xml @@ -1,73 +1,74 @@ + 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 index 0000000..f82eeb0 --- /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 index c055a84..9c9c7ef 100644 --- a/res/menu/drawer_main.xml +++ b/res/menu/drawer_main.xml @@ -1,24 +1,28 @@ + \ No newline at end of file diff --git a/res/menu/menu_line_item.xml b/res/menu/menu_line_item.xml new file mode 100644 index 0000000..6b8a40a --- /dev/null +++ b/res/menu/menu_line_item.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 2931740..8ebd3c7 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,170 +1,188 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome 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 Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:

Perché usare BusTO?

- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!

Come Funziona?

Quest\'app ottiene i passaggi dei bus in tempo reale filtrando i dati forniti pubblicamente sul sito www.gtt.to.it o www.5t.torino.it "per uso personale".

Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori!

Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+. Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.

Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine 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 Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari 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 Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo + + Richiesto aggiornamento del database + + Mostra direzioni in maiuscolo Non cambiare 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 index 18be176..5b99d15 100644 --- a/res/values/keys.xml +++ b/res/values/keys.xml @@ -1,5 +1,6 @@ layout_pref + pref_update_db_now \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index ab5e401..06b37fd 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,194 +1,207 @@ BusTO Libre BusTO BusTO dev You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry 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 Help About More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times Welcome!

Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.

Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!

How does it work?

This app will show you bus timetables gathering data from www.gtt.to.it or www.5t.torino.it "for personal use".

Who worked on BusTO:
- Fabio Mazza current senior rockstar developer.
- Andrea Ugo current junior rockstar developer.
- Silviu Chiriac designer of the 2021 logo.
- Marco M rockstar tester and bug hunter.
- Ludovico Pavesi previous senior rockstar developer asd.
- Valerio Bozzolan maintainer and infrastructure sponsor.
- Marco Gagino contributor and icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!

Licenses

The app and the related source code are released by Valerio Bozzolan under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.

Notes

This app has been developed hoping to be useful to everyone but without ANY warranty.

This translation is kindly provided by Riccardo Caniato and Marco Gagino.

Get involved! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recents is invalid Invalid value, put a valid number Finding the position… No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update 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 Show arrivals Show stops Center on my location Follow me Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals 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 The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop + Launching database update Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST + + + Long press for options
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 77ed8a3..5070733 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,61 +1,69 @@ + + + + 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 index 0000000..3cbb073 --- /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 index 35624da..3fb069c 100644 --- a/src/it/reyboz/bustorino/ActivityExperiments.java +++ b/src/it/reyboz/bustorino/ActivityExperiments.java @@ -1,196 +1,155 @@ /* 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; 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.*; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; 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 { ExecutorService executorService; final static String DEBUG_TAG = "ExperimentsGTFS"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_experiments); Button deleteButton = findViewById(R.id.deleteButton); if(deleteButton!=null) deleteButton.setOnClickListener(view -> { File saveFile = new File(getFilesDir(), "gtfs_data.zip"); if(!saveFile.isDirectory() && saveFile.exists()){ //delete the file if(saveFile.delete()) Toast.makeText(this, "Gtfs zip deleted", Toast.LENGTH_SHORT).show(); else Toast.makeText(this, "Cannot delete gtfs zip", Toast.LENGTH_SHORT).show(); } else Toast.makeText(this, "Gtfs data zip not present", Toast.LENGTH_SHORT).show(); }); Button cleanDBButton = findViewById(R.id.deleteDbButton); if(cleanDBButton!=null) cleanDBButton.setOnClickListener(this::deleteDatabase); executorService = Executors.newFixedThreadPool(2); } public void runExp(View v){ final Context appContext = v.getContext().getApplicationContext(); Runnable run = new Runnable() { @Override public void run() { AtomicReference res = new AtomicReference<>(); //List files = GtfsDataParser.readFilesList(res); Date updateDate = GtfsDataParser.getLastGTFSUpdateDate(res); Log.w( "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(); File saveFile = new File(getFilesDir(), "gtfs_data.zip"); if (!saveFile.isDirectory() && saveFile.exists()) { Log.w(DEBUG_TAG, "Zip exists: " + saveFile); try (ZipFile zipFile = new ZipFile(saveFile)) { //ZipInputStream stream = new ZipInputStream(fileStream); // 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(); //String tableName = entry.getName().split("\\.")[0].trim(); if(entry.getName().trim().equals("stop_times.txt")) { readLater.add(entry); continue; } GtfsDataParser.readGtfsZipEntry(entry, zipFile, v.getContext().getApplicationContext()); } 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(); } //saveFile.delete(); } else try { //Toast.makeText(v.getContext(), "Downloading gtfs data", Toast.LENGTH_SHORT).show(); networkTools.saveFileInCache(saveFile, new URL(GtfsDataParser.GTFS_ADDRESS)); Log.w(DEBUG_TAG, "File saved"); } catch (MalformedURLException e) { e.printStackTrace(); } } }; - 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 index 5913140..617281d 100644 --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,595 +1,609 @@ /* BusTO - Arrival times for Turin public transport. 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; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; 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; import com.google.android.material.navigation.NavigationView; 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; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_principal); final SharedPreferences theShPr = getMainSharedPreferences(); Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); requestArrivalsForStopID(busStopID); } //Try (hopefully) database update - DatabaseUpdate.requestDBUpdateWithWork(this, false); + DatabaseUpdate.requestDBUpdateWithWork(this, false, false); /* Watch for database update */ final WorkManager workManager = WorkManager.getInstance(this); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment showMainFragment(); } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //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; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); switch (result) { case PERMISSION_OK: createAndShowMapFragment(null); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> createAndShowMapFragment(null)); break; case PERMISSION_NEG_CANNOT_ASK: String storage_perm = getString(R.string.storage_permission); String text = getString(R.string.too_many_permission_asks, storage_perm); 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); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { boolean foundFragment = false; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } shownFrag.getChildFragmentManager().popBackStackImmediate(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); } else super.onBackPressed(); } /** * Create and show the SnackBar with the message */ private void createDefaultSnackbar() { View baseView = null; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); } 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(); } private MainScreenFragment createAndShowMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); MainScreenFragment fragment = MainScreenFragment.newInstance(); FragmentTransaction transaction = fraMan.beginTransaction(); transaction.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG); transaction.commit(); return fragment; } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment){ fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment) .setReorderingAllowed(true) .addToBackStack(null) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } private MainScreenFragment showMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ mainScreenFragment = createAndShowMainFragment(); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.readyGUIfor(fragmentType); } int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_arrivals); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: 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; } if(getSupportActionBar()!=null && titleResId!=0) getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment); } else { // we have no fragment probableFragment = createAndShowMainFragment(); } } probableFragment.requestArrivalsForStopID(ID); mNavView.setCheckedItem(R.id.nav_arrivals); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(Stop stop) { createAndShowMapFragment(stop); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop){ FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_about: startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; case R.id.action_hack: openIceweasel(getString(R.string.hack_url), activityContext); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; case R.id.action_experiments: startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); default: } return false; } } } diff --git a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java index fc582dc..cd051f2 100644 --- a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -1,272 +1,294 @@ /* BusTO - UI components Copyright (C) 2017 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.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; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentListenerMain; import it.reyboz.bustorino.util.RoutePositionSorter; import it.reyboz.bustorino.util.StopSorterByDistance; 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; userPosition = pos; this.routesPairList = routesPairList; context = con.getApplicationContext(); 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); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS final Pair stopRoutePair = routesPairList.get(position); if(stopRoutePair!=null && stopRoutePair.first!=null){ final Stop stop = stopRoutePair.first; final Route r = stopRoutePair.second; final Double distance = stop.getDistanceFromLocation(userPosition); if(distance!=Double.POSITIVE_INFINITY){ holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID); holder.stopNameView.setText(stopText); //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); holder.lineDirectionTextView.setVisibility(View.INVISIBLE); //holder.arrivalsTextView.setVisibility(View.INVISIBLE); } /* EXPERIMENTS if(r.destinazione==null || r.destinazione.trim().isEmpty()){ holder.lineDirectionTextView.setVisibility(View.GONE); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId()); holder.arrivalsDescriptionTextView.setLayoutParams(params); } else { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.removeRule(RelativeLayout.RIGHT_OF); holder.arrivalsDescriptionTextView.setLayoutParams(params); holder.lineDirectionTextView.setVisibility(View.VISIBLE); } */ holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { 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; TextView stopNameView; TextView arrivalsDescriptionTextView; TextView arrivalsTextView; TextView distancetextView; String stopID; ViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView); lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView); stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName); arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView); arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView); distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void resetRoutesPairList(List stopList){ Collections.sort(stopList,new StopSorterByDistance(userPosition)); this.routesPairList = new ArrayList<>(stopList.size()); int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: stopList.subList(0,maxNum)){ //if there are no routes available, skip stop 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)); } } } public void setUserPosition(@Nullable Location userPosition) { this.userPosition = userPosition; } public void setRoutesPairListAndPosition(List> mRoutesPairList, @Nullable Location pos) { if(pos!=null){ this.userPosition = pos; } if(mRoutesPairList!=null){ //this.routesPairList = routesPairList; //remove duplicates sortAndRemoveDuplicates(mRoutesPairList, this.userPosition); //routesPairList = mRoutesPairList; //STUPID CODE if (this.routesPairList == null || routesPairList.size() == 0){ routesPairList = mRoutesPairList; notifyDataSetChanged(); } else{ final HashMap, Integer> indexMapIn = getRouteIndexMap(mRoutesPairList); final HashMap, Integer> indexMapExisting = getRouteIndexMap(routesPairList); //List> oldList = routesPairList; routesPairList = mRoutesPairList; /* for (Pair pair: indexMapIn.keySet()){ final Integer posIn = indexMapIn.get(pair); if (posIn == null) continue; if (indexMapExisting.containsKey(pair)){ final Integer posExisting = indexMapExisting.get(pair); //THERE IS ALREADY //routesPairList.remove(posExisting.intValue()); //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemMoved(posExisting, posIn); indexMapExisting.remove(pair); } else{ //INSERT IT //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemInserted(posIn); } }// //REMOVE OLD STOPS for (Pair pair: indexMapExisting.keySet()) { final Integer posExisting = indexMapExisting.get(pair); if (posExisting == null) continue; //routesPairList.remove(posExisting.intValue()); notifyItemRemoved(posExisting); } //*/notifyDataSetChanged(); } //remove and join the } } /** * Sort and remove the repetitions for the routesPairList */ private void resetListAndPosition(){ Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } /** * Sort and remove the repetitions in the list */ private static void sortAndRemoveDuplicates(List< Pair > routesPairList, Location positionToSort ){ Collections.sort(routesPairList,new RoutePositionSorter(positionToSort)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } private static HashMap, Integer> getRouteIndexMap(List> routesPairList){ final HashMap, Integer> myMap = new HashMap<>(); for (int i=0; i(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); } return myMap; } } diff --git a/src/it/reyboz/bustorino/adapters/NameCapitalize.java b/src/it/reyboz/bustorino/adapters/NameCapitalize.java new file mode 100644 index 0000000..67f1b07 --- /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 index 15a6505..05daef0 100644 --- a/src/it/reyboz/bustorino/adapters/StopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopAdapter.java @@ -1,118 +1,118 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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.adapters; import android.content.Context; import androidx.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import java.util.List; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; /** * @see PalinaAdapter */ 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; private static final int cityIcon = R.drawable.city; private static class ViewHolder { TextView busStopIDTextView; TextView busStopNameTextView; //TextView busLineVehicleIcon; TextView busStopLinesTextView; TextView busStopLocaLityTextView; } public StopAdapter(Context context, List stops) { super(context, row_layout, stops); li = LayoutInflater.from(context); } @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { ViewHolder vh; if(convertView == null) { convertView = li.inflate(row_layout, null); vh = new ViewHolder(); vh.busStopIDTextView = (TextView) convertView.findViewById(R.id.busStopID); vh.busStopNameTextView = (TextView) convertView.findViewById(R.id.busStopName); vh.busStopLinesTextView = (TextView) convertView.findViewById(R.id.routesThatStopHere); vh.busStopLocaLityTextView = (TextView) convertView.findViewById(R.id.busStopLocality); convertView.setTag(vh); } else { vh = (ViewHolder) convertView.getTag(); } Stop stop = getItem(position); vh.busStopIDTextView.setText(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()); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { vh.busStopLinesTextView.setVisibility(View.GONE); } else { vh.busStopLinesTextView.setText(whatStopsHere); vh.busStopLinesTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } if(stop.type == null) { vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); } else { switch(stop.type) { case BUS: default: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: case RAILWAY: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: // è l'opposto della città ma va beh, dettagli. vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(cityIcon, 0, 0, 0); } } if (stop.location == null) { vh.busStopLocaLityTextView.setVisibility(View.GONE); } else { vh.busStopLocaLityTextView.setText(stop.location); vh.busStopLocaLityTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } return convertView; } } diff --git a/src/it/reyboz/bustorino/adapters/AdapterListener.java b/src/it/reyboz/bustorino/adapters/StopAdapterListener.java similarity index 91% rename from src/it/reyboz/bustorino/adapters/AdapterListener.java rename to src/it/reyboz/bustorino/adapters/StopAdapterListener.java index 7f61e25..e61e716 100644 --- a/src/it/reyboz/bustorino/adapters/AdapterListener.java +++ b/src/it/reyboz/bustorino/adapters/StopAdapterListener.java @@ -1,24 +1,26 @@ /* BusTO - Adapter 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.adapters; 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 index e184500..5007ced 100644 --- a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java @@ -1,174 +1,232 @@ /* BusTO - Adapter 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.adapters; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; 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; protected static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener{ TextView busStopIDTextView; TextView busStopNameTextView; //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(""); itemView.setOnClickListener(view -> { listener.onTappedStop(mStop); }); } //many thanks to https://stackoverflow.com/questions/26466877/how-to-create-context-menu-for-recyclerview @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){ this.stops = stops; notifyDataSetChanged(); } public List getStops() { return stops; } public int getPosition() { return position; } public void setPosition(int position) { this.position = position; } @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 public void onViewRecycled(@NonNull StopRecyclerAdapter.ViewHolder holder) { holder.itemView.setOnLongClickListener(null); super.onViewRecycled(holder); } @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()); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { vh.busStopLinesTextView.setVisibility(View.GONE); } else { vh.busStopLinesTextView.setText(whatStopsHere); vh.busStopLinesTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } if(stop.type == null) { vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); } else { switch(stop.type) { case BUS: default: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: case RAILWAY: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: // è l'opposto della città ma va beh, dettagli. vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(cityIcon, 0, 0, 0); } } 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 index 530fd1f..e0627dc 100644 --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -1,429 +1,430 @@ /* BusTO - Backend components Copyright (C) 2018 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; import androidx.annotation.Nullable; import android.util.Log; import it.reyboz.bustorino.data.GTTInfoInject; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.*; import java.util.concurrent.atomic.AtomicReference; public class FiveTAPIFetcher implements ArrivalsFetcher{ private static final String DEBUG_NAME = "FiveTAPIFetcher"; private final Map defaultHeaders = getDefaultHeaders(); final static LinkedList apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab")); @Override public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) { //set the date for the request as now Palina p = new Palina(stopID); //request parameters String response = performAPIRequest(QueryType.ARRIVALS,stopID,res); if(response==null) { if(res.get()== Result.SERVER_ERROR_404) { Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the address is wrong"); //res.set(Result.S); } return p; } List routes = parseArrivalsServerResponse(response, res); if(res.get()==Result.OK) { for (Route r : routes) { p.addRoute(r); } p.sortRoutes(); } return p; } @Override public Passaggio.Source getSourceForFetcher() { return Passaggio.Source.FiveTAPI; } List parseArrivalsServerResponse(String JSONresponse, AtomicReference res){ ArrayList routes = new ArrayList<>(3); /* Slight problem: "longName": ==> DESCRIPTION "name": "13N", "departures": [ { "arrivalTimeInt": 1272, "time": "21:12", "rt": false }] "lineType": "URBANO" ==> URBANO can be either bus or tram or METRO */ JSONArray arr; try{ arr = new JSONArray(JSONresponse); String type; Route.Type routetype = Route.Type.UNKNOWN; for(int i =0; i parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{ if(response == null || response.equals("null") || response.length()==0) throw new IllegalArgumentException("Response string is null or void"); ArrayList routes = new ArrayList<>(10); JSONArray lines =new JSONArray(response); for(int i=0; i 1) { String secondo = exploded[exploded.length-2]; if (secondo.contains("festivo")) { festivo = Route.FestiveInfo.FESTIVO; } else if (secondo.contains("feriale")) { festivo = Route.FestiveInfo.FERIALE; } else if(secondo.contains("lun. - ven")) { serviceDays = Route.reduced_week; } else if(secondo.contains("sab - fest.")){ serviceDays = Route.weekend; festivo = Route.FestiveInfo.FESTIVO; } else { /* Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+ "Couldn't find a the service days\n"+ "Description: "+secondo+","+description ); */ } if(exploded.length>2){ switch (exploded[exploded.length-3].trim()) { case "bus": t = Route.Type.BUS; break; case "tram": //never happened, but if it could happen you can get it t = Route.Type.TRAM; break; default: //nothing } } } else //only one piece if(description.contains("festivo")){ festivo = Route.FestiveInfo.FESTIVO; } else if(description.contains("feriale")){ festivo = Route.FestiveInfo.FERIALE; } if(t == Route.Type.UNKNOWN &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM; //check for the presence of parenthesis String preParenthesis, postParenthesis; boolean hasParenth = false; if (description.contains("(")){ hasParenth =true; preParenthesis = description.split("\\(")[0]; postParenthesis = description.split("\\(")[1]; } else { preParenthesis = description; postParenthesis = ""; } if(preParenthesis.contains("-")){ //Sometimes the actual filtered direction still remains the full line (including both extremes) preParenthesis = preParenthesis.split("-")[1]; } final String directionFinal = hasParenth? preParenthesis.trim() + " (" + postParenthesis : preParenthesis; Route r = new Route(lineName.trim(),directionFinal.trim(),t,new ArrayList<>()); if(serviceDays.length>0) r.serviceDays = serviceDays; r.festivo = festivo; r.branchid = branchid; r.description = description.trim(); //check if we have the stop list if (branchJSON.has("branchDetail")) { final String stops = branchJSON.getJSONObject("branchDetail").getString("stops"); r.setStopsList(Arrays.asList(stops.split(","))); } routes.add(r); } return routes; } public List getDirectionsForStop(String stopID, AtomicReference res) { String response = performAPIRequest(QueryType.DETAILS,stopID,res); List routes; try{ routes = parseDirectionsFromResponse(response); res.set(Result.OK); } catch (JSONException | IllegalArgumentException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); routes = null; } return routes; } public ArrayList getAllStopsFromGTT(AtomicReference res){ String response = performAPIRequest(QueryType.STOPS_ALL,null,res); if(response==null) return null; ArrayList stopslist; try{ //JSONObject responseJSON = new JSONObject(response); JSONArray stops = new JSONArray(response);//responseJSON.getJSONArray("stops"); stopslist = new ArrayList<>(stops.length()); for (int i=0;i getAllLinesFromGTT(AtomicReference res){ String resp = performAPIRequest(QueryType.LINES,null,res); if(resp==null) { return null; } ArrayList routes = null; try { JSONArray lines = new JSONArray(resp); routes = new ArrayList<>(lines.length()); for(int i = 0; i getDefaultHeaders(){ HashMap param = new HashMap<>(); param.put("Host","www.5t.torino.it"); param.put("Connection","Keep-Alive"); param.put("Accept-Encoding", "gzip"); return param; } /** * Create and perform the network request. This method adds parameters and returns the result * @param t type of request to be performed * @param stopID optional parameter, stop ID which you need for passages and branches * @param res result container * @return a String which contains the result of the query, to be parsed */ @Nullable public static String performAPIRequest(QueryType t,@Nullable String stopID, AtomicReference res){ URL u; Map param; try { String address = getURLForOperation(t,stopID); //Log.d(DEBUG_NAME,"The address to query is: "+address); param = getDefaultHeaders(); u = new URL(address); } catch (UnsupportedEncodingException |MalformedURLException e) { e.printStackTrace(); res.set(Result.PARSER_ERROR); return null; } return networkTools.queryURL(u,res,param); } /** * Get the right url for the operation you are doing, to be fed into the queryURL method * @param t type of operation * @param stopID stop on which you are working on * @return the Url to go to * @throws UnsupportedEncodingException if it cannot be converted to utf-8 */ public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException { final StringBuilder sb = new StringBuilder(); sb.append("http://www.5t.torino.it/ws2.1/rest/"); if(t!=QueryType.LINES) sb.append("stops/"); switch (t){ case ARRIVALS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/departures"); break; case DETAILS: sb.append(URLEncoder.encode(stopID,"utf-8")); sb.append("/branches/details"); break; case STOPS_ALL: sb.append("all"); break; case STOPS_VERSION: sb.append("version"); break; case LINES: sb.append("lines/all"); break; } return sb.toString(); } public enum QueryType { ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES } } diff --git a/src/it/reyboz/bustorino/backend/Notifications.java b/src/it/reyboz/bustorino/backend/Notifications.java index eddbe9c..4bcfb7b 100644 --- a/src/it/reyboz/bustorino/backend/Notifications.java +++ b/src/it/reyboz/bustorino/backend/Notifications.java @@ -1,46 +1,47 @@ package it.reyboz.bustorino.backend; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; import it.reyboz.bustorino.R; 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 // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = context.getString(R.string.default_notification_channel); String description = context.getString(R.string.default_notification_channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } /** * Register a notification channel on Android Oreo and above * @param con a Context * @param name channel name * @param description channel description * @param importance channel importance (from NotificationManager) * @param ID channel ID */ public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = con.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } } diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java index 5049cd2..50b8c1b 100644 --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -1,380 +1,380 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.List; import it.reyboz.bustorino.util.LinesNameSorter; /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop { private ArrayList routes = new ArrayList<>(); private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } 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) { super(name, ID, location, type, routesThatStopHere); } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6))); } public int addRoute(Route r){ this.routes.add(r); routesModified = true; buildRoutesString(); return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } @Nullable @Override protected String buildRoutesString() { // no routes => no string if(routes == null || routes.size() == 0) { return ""; } final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim())); int i, lenMinusOne = routes.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routes.get(i).getName().trim()).append(", "); } // last one: sb.append(routes.get(i).getName()); setRoutesThatStopHereString(sb.toString()); return routesThatStopHereToString(); } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); buildRoutesString(); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } if (count> 0) buildRoutesString(); return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i0) min = Math.min(min,r.numPassaggi()); } if (min == Integer.MAX_VALUE) return 0; else return min; } //private void mergeRoute } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Result.java b/src/it/reyboz/bustorino/backend/Result.java new file mode 100644 index 0000000..273127b --- /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 index 20b06f5..b282856 100644 --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -1,302 +1,305 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi 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; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import it.reyboz.bustorino.util.LinesNameSorter; import java.net.URLEncoder; import java.util.Collections; import java.util.List; import java.util.Locale; public class Stop implements Comparable { // remove "final" in case you need to set these from outside the parser\scrapers\fetchers public final @NonNull String ID; private @Nullable String name; private @Nullable String username; public @Nullable String location; public @Nullable Route.Type type; private @Nullable List routesThatStopHere; private final @Nullable Double lat; private final @Nullable Double lon; // leave this non-final private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; // public @Nullable String gtfsID = null; /** * Hey, look, method overloading! */ public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) { this.ID = ID; this.name = name; this.username = null; this.location = (location != null && location.length() == 0) ? null : location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = null; this.lon = null; } /** * Hey, look, method overloading! */ public Stop(final @NonNull String ID) { this.ID = ID; this.name = null; this.username = null; this.location = null; this.type = null; this.routesThatStopHere = null; this.lat = null; this.lon = null; } /** * 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; this.location = location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; + this.gtfsID = gtfsID; } public @Nullable String routesThatStopHereToString() { // M E M O I Z A T I O N if(this.routesThatStopHereString != null) { return this.routesThatStopHereString; } // no string yet? build it! return buildRoutesString(); } @Nullable public String getAbsurdGTTPlaceName() { return absurdGTTPlaceName; } public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) { this.absurdGTTPlaceName = absurdGTTPlaceName; } public void setRoutesThatStopHere(@Nullable List routesThatStopHere) { this.routesThatStopHere = routesThatStopHere; } protected void setRoutesThatStopHereString(String routesStopping){ this.routesThatStopHereString = routesStopping; } @Nullable protected List getRoutesThatStopHere(){ return routesThatStopHere; } protected @Nullable String buildRoutesString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; } StringBuilder sb = new StringBuilder(); Collections.sort(routesThatStopHere,new LinesNameSorter()); int i, lenMinusOne = routesThatStopHere.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routesThatStopHere.get(i)).append(", "); } // last one: sb.append(routesThatStopHere.get(i)); this.routesThatStopHereString = sb.toString(); return this.routesThatStopHereString; } @Override public int compareTo(@NonNull Stop other) { int res; int thisAsInt = networkTools.failsafeParseInt(this.ID); int otherAsInt = networkTools.failsafeParseInt(other.ID); // numeric stop IDs if(thisAsInt != 0 && otherAsInt != 0) { return thisAsInt - otherAsInt; } else { // non-numeric res = this.ID.compareTo(other.ID); if (res != 0) { return res; } } // try with name, then if(this.name != null && other.name != null) { res = this.name.compareTo(other.name); } // and give up return res; } /** * Sets a name. * * @param name stop name as string (not null) */ public final void setStopName(@NonNull String name) { this.name = name; } /** * Sets user name. Empty string is converted to null. * * @param name a string of non-zero length, or null */ public final void setStopUserName(@Nullable String name) { if(name == null) { this.username = null; } else if(name.length() == 0) { this.username = null; } else { this.username = name; } } /** * Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
* * @return string if known, null if still unknown */ public final @Nullable String getStopDisplayName() { if(this.username == null) { return this.name; } else { return this.username; } } /** * Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName * refers to the method exactly above this one and not some arcane and esoteric unknown symbol. */ public final @Nullable String getStopDefaultName() { return this.name; } /** * Same as getStopDisplayName, only returns user name.
* Also, never an empty string. */ public final @Nullable String getStopUserName() { return this.username; } /** * Gets username and name from other stop if they exist, sets itself accordingly. * * @param other another Stop * @return did we actually set/change anything? */ public final boolean mergeNameFrom(Stop other) { boolean ret = false; if(other.name != null) { if(this.name == null || !this.name.equals(other.name)) { this.name = other.name; ret = true; } } if(other.username != null) { if(this.username == null || !this.username.equals(other.username)) { this.username = other.username; ret = true; } } return ret; } public final @Nullable String getGeoURL() { if(this.lat == null || this.lon == null) { return null; } // Android documentation suggests US for machine readable output (use dot as decimal separator) return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); if(url == null) { return null; } if(this.location != null) { try { String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8")); return url.concat(addThis); } catch (Exception ignored) {} } return url; } @Nullable public Double getLatitude() { return lat; } @Nullable public Double getLongitude() { return lon; } public Double getDistanceFromLocation(Location loc){ if(this.lat!=null && this.lon !=null) return utils.measuredistanceBetween(this.lat,this.lon,loc.getLatitude(),loc.getLongitude()); else return Double.POSITIVE_INFINITY; } } diff --git a/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java new file mode 100644 index 0000000..0374be0 --- /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 index 5fdfb39..b54f482 100644 --- a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -1,156 +1,153 @@ /* BusTO - Backend 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.backend.mato; import android.util.Log; import androidx.annotation.Nullable; import com.android.volley.AuthFailureError; import com.android.volley.NetworkResponse; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.HttpHeaderParser; import org.json.JSONArray; 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; import it.reyboz.bustorino.backend.Palina; public class MapiArrivalRequest extends MapiVolleyRequest { private final String stopName; private final Date startingTime; private final int timeRange, numberOfDepartures; private final AtomicReference reqRes; private final String DEBUG_TAG = "BusTO-MAPIArrivalReq"; public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, int numberOfDepartures, 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; this.numberOfDepartures = numberOfDepartures; this.reqRes = res; } public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, int numberOfDepartures, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { this(stopName, startingTime, timeRange, numberOfDepartures, new AtomicReference<>(), listener, errorListener); } @Nullable @Override public byte[] getBody() throws AuthFailureError { JSONObject variables = new JSONObject(); JSONObject data = new JSONObject(); try { data.put("operationName","AllStopsDirect"); variables.put("name", stopName); variables.put("startTime", (long) startingTime.getTime()/1000); variables.put("timeRange", timeRange); variables.put("numberOfDepartures", numberOfDepartures); data.put("variables", variables); data.put("query", MatoQueries.QUERY_ARRIVALS); } catch (JSONException e) { e.printStackTrace(); throw new AuthFailureError("Error with JSON enconding",e); } String requestBody = data.toString(); Log.d(DEBUG_TAG, "Request variables: "+ variables); return requestBody.getBytes(); } @Override protected Response parseNetworkResponse(NetworkResponse response) { if(response.statusCode != 200) { reqRes.set(Fetcher.Result.SERVER_ERROR); return Response.error(new VolleyError("Response Error Code " + response.statusCode)); } final String stringResponse = new String(response.data); Palina p = null; try { JSONObject data = new JSONObject(stringResponse).getJSONObject("data"); JSONArray allStopsFound = data.getJSONArray("stops"); boolean stopFound = false; for (int i=0; i extends Request { 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); this.type = type; this.listener = listener; } @Override protected void deliverResponse(T response) { listener.onResponse(response); } @Override public Map getHeaders() throws AuthFailureError { return MatoAPIFetcher.Companion.getREQ_PARAMETERS(); } } diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index 7f2152f..6c94dd7 100644 --- a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,268 +1,430 @@ /* BusTO - Backend 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.backend.mato 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 { var appContext: Context? = null set(value) { field = value!!.applicationContext } constructor(): this(2) override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { stopID!! val now = Calendar.getInstance().time var numMinutes = 0 var palina = Palina(stopID) var numPassaggi = 0 var trials = 0 val numDepartures = 4 while (numPassaggi < minNumPassaggi && trials < 4) { //numDepartures+=2 numMinutes += 20 val future = RequestFuture.newFuture() val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue - request.setTag(getVolleyReqTag(QueryType.ARRIVALS)) + request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) try { val palinaResult = future.get(5, TimeUnit.SECONDS) if (palinaResult!=null) { if (BuildConfig.DEBUG) for (r in palinaResult.queryAllRoutes()){ Log.d(DEBUG_TAG, "route " + r.gtfsId + " has " + r.passaggi.size + " passaggi: "+ r.passaggiToString) } palina = palinaResult numPassaggi = palina.minNumberOfPassages } else{ Log.d(DEBUG_TAG, "Result palina is null") } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() if (res.get() == Fetcher.Result.OK) res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "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" } } /** * Get stops from the MatoAPI, set [res] accordingly */ fun getAllStopsGTT(context: Context, res: AtomicReference?): List{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue 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) { 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() } return palinaList } /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") 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 for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.setGtfsId(routeBaseInfo.getString("gtfsId").trim()) baseRoutes.add(r) } if (jsonStop.has("desc")){ palina.location = jsonStop.getString("desc") } //there is also "zoneId" which is the zone of the stop (0-> city, etc) if(jsonStop.has("stoptimesForPatterns")) { val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") for (i in 0 until routesStopTimes.length()) { val patternJSON = routesStopTimes.getJSONObject(i) val mRoute = parseRouteStoptimesJSON(patternJSON) //Log.d("BusTO-MapiFetcher") //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") //TODO: use directionId palina.addRoute(mRoute) for (r in baseRoutes) { if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) { baseRoutes.remove(r) break } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() return palina } fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( routeJSON.getString("shortName"), patternJSON.getString("headsign"), routeType, passages, ) route.setGtfsId(gtfsId) return route } fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ val data = JSONObject() data.put("operationName", requestName) data.put("variables", variables) data.put("query", query) 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 index d870676..b7fbac2 100644 --- a/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt @@ -1,90 +1,166 @@ /* BusTO - Backend 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.backend.mato class MatoQueries { companion object{ const val QUERY_ARRIVALS="""query AllStopsDirect( ${'$'}name: String ${'$'}startTime: Long ${'$'}timeRange: Int ${'$'}numberOfDepartures: Int ) { stops(name: ${'$'}name) { __typename lat lon gtfsId code name desc wheelchairBoarding routes { __typename gtfsId shortName } stoptimesForPatterns( startTime: ${'$'}startTime timeRange: ${'$'}timeRange numberOfDepartures: ${'$'}numberOfDepartures ) { __typename pattern { __typename headsign directionId route { __typename gtfsId shortName mode } } stoptimes { __typename scheduledArrival realtimeArrival realtime realtimeState } } } } """ const val ALL_STOPS_BY_FEEDS=""" query AllStops(${'$'}feeds: [String!]){ stops(feeds: ${'$'}feeds) { lat lon gtfsId code name desc routes { gtfsId shortName } } } """ + + 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 index 0000000..ecf79b2 --- /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 index 0000000..ff0e563 --- /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 index 1420152..5e4ab0c 100644 --- a/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt +++ b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt @@ -1,79 +1,79 @@ /* BusTO - Backend 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.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 it.reyboz.bustorino.backend.Palina import org.json.JSONArray import org.json.JSONException import org.json.JSONObject class VolleyAllStopsRequest( listener: Response.Listener>, errorListener: Response.ErrorListener, ) : MapiVolleyRequest>( - MatoAPIFetcher.QueryType.ALL_STOPS,listener, errorListener) { + MatoQueries.QueryType.ALL_STOPS,listener, errorListener) { private val FEEDS = JSONArray() init { FEEDS.put("gtt") } override fun getBody(): ByteArray { val variables = JSONObject() variables.put("feeds", FEEDS) val data = MatoAPIFetcher.makeRequestParameters("AllStops", variables, MatoQueries.ALL_STOPS_BY_FEEDS) 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 stringResponse = String(response.data) val palinas = ArrayList() try { val allData = JSONObject(stringResponse).getJSONObject("data") val allStops = allData.getJSONArray("stops") for (i in 0 until allStops.length()){ val jsonStop = allStops.getJSONObject(i) palinas.add(MatoAPIFetcher.parseStopJSON(jsonStop)) } } catch (e: JSONException){ Log.e("VolleyBusTO","Cannot parse response as JSON") e.printStackTrace() return Response.error(VolleyError("Error parsing JSON")) } 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 index 06b17a0..a638a89 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,185 +1,226 @@ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.Nullable; import java.io.PrintWriter; import java.io.StringWriter; 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); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/EarthRadius); } /* public static int convertDipToPixels(Context con,float dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } */ public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } 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(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i. */ package it.reyboz.bustorino.data; import android.content.*; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.util.Log; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.util.List; import static it.reyboz.bustorino.data.UserDB.getFavoritesColumnNamesAsArray; public class AppDataProvider extends ContentProvider { public static final String AUTHORITY = BuildConfig.APPLICATION_ID +".provider"; private static final int STOP_OP = 1; private static final int LINE_OP = 2; private static final int BRANCH_OP = 3; private static final int FAVORITES_OP =4; private static final int MANY_STOPS = 5; private static final int ADD_UPDATE_BRANCHES = 6; private static final int LINE_INSERT_OP = 7; private static final int CONNECTIONS = 8; private static final int LOCATION_SEARCH = 9; private static final int GET_ALL_FAVORITES =10; public static final String FAVORITES = "favorites"; private static final String DEBUG_TAG="AppDataProvider"; private Context con; private NextGenDB appDBHelper; private UserDB userDBHelper; private SQLiteDatabase db; private DBStatusManager preferences; public AppDataProvider() { } private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. */ sUriMatcher.addURI(AUTHORITY, "stop/#", STOP_OP); sUriMatcher.addURI(AUTHORITY,"stops",MANY_STOPS); sUriMatcher.addURI(AUTHORITY,"stops/location/*/*/*",LOCATION_SEARCH); /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ sUriMatcher.addURI(AUTHORITY, "line/#", LINE_OP); sUriMatcher.addURI(AUTHORITY,"branch/#",BRANCH_OP); sUriMatcher.addURI(AUTHORITY,"line/insert",LINE_INSERT_OP); sUriMatcher.addURI(AUTHORITY,"branches",ADD_UPDATE_BRANCHES); sUriMatcher.addURI(AUTHORITY,"connections",CONNECTIONS); sUriMatcher.addURI(AUTHORITY,"favorites/#",FAVORITES_OP); sUriMatcher.addURI(AUTHORITY,FAVORITES,GET_ALL_FAVORITES); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Implement this to handle requests to delete one or more rows. db = appDBHelper.getWritableDatabase(); int rows; switch (sUriMatcher.match(uri)){ case MANY_STOPS: rows = db.delete(NextGenDB.Contract.StopsTable.TABLE_NAME,null,null); break; default: throw new UnsupportedOperationException("Not yet implemented"); } return rows; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. int match = sUriMatcher.match(uri); String baseTypedir = "vnd.android.cursor.dir/"; String baseTypeitem = "vnd.android.cursor.item/"; switch (match){ case LOCATION_SEARCH: return baseTypedir+"stop"; case LINE_OP: return baseTypeitem+"line"; case CONNECTIONS: return baseTypedir+"stops"; } return baseTypedir+"/item"; } @Override public Uri insert(Uri uri, ContentValues values) throws IllegalArgumentException{ //AVOID OPENING A DB CONNECTION, WILL THROW VERY NASTY ERRORS if(preferences.isDBUpdating(true)) return null; db = appDBHelper.getWritableDatabase(); Uri finalUri; long last_rowid = -1; switch (sUriMatcher.match(uri)){ case ADD_UPDATE_BRANCHES: Log.d("InsBranchWithProvider","new Insert request"); String line_name = values.getAsString(NextGenDB.Contract.LinesTable.COLUMN_NAME); if(line_name==null) throw new IllegalArgumentException("No line name given"); long lineid = -1; Cursor c = db.query(LinesTable.TABLE_NAME, new String[]{LinesTable._ID,LinesTable.COLUMN_NAME,LinesTable.COLUMN_DESCRIPTION},NextGenDB.Contract.LinesTable.COLUMN_NAME +" =?", new String[]{line_name},null,null,null); Log.d("InsBranchWithProvider","finding line in the database: "+c.getCount()+" matches"); if(c.getCount() == 0){ //There are no lines, insert? //NOPE /* c.close(); ContentValues cv = new ContentValues(); cv.put(LinesTable.COLUMN_NAME,line_name); lineid = db.insert(LinesTable.TABLE_NAME,null,cv); */ break; }else { c.moveToFirst(); /* while(c.moveToNext()){ 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); values.put(BranchesTable.COL_LINE,lineid); last_rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE); break; case MANY_STOPS: //Log.d("AppDataProvider_busTO","New stop insert request"); try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; case CONNECTIONS: try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.ConnectionsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; default: throw new IllegalArgumentException("Invalid parameters"); } finalUri = ContentUris.withAppendedId(uri,last_rowid); return finalUri; } @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); } else { preferences = null; Log.e(DEBUG_TAG,"Cannot get shared preferences"); } return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws UnsupportedOperationException,IllegalArgumentException { //IMPORTANT //The app should not query when the DB is updating, but apparently, it does if(preferences.isDBUpdating(true)) //throw new UnsupportedOperationException("DB is updating"); return null; SQLiteDatabase db = appDBHelper.getReadableDatabase(); List parts = uri.getPathSegments(); switch (sUriMatcher.match(uri)){ case LOCATION_SEARCH: //authority/stops/location/"Lat"/"Lon"/"distance" //distance in metres (integer) if(parts.size()>=4 && "location".equals(parts.get(1))){ Double latitude = Double.parseDouble(parts.get(2)); Double longitude = Double.parseDouble(parts.get(3)); //converting distance to a float to not lose precision float distance = parts.size()>=5 ? Float.parseFloat(parts.get(4))/1000 : 0.02f; //if(parts.size()>=5) //Log.d("LocationSearch"," given distance to search is "+parts.get(4)+" m"); Double distasAngle = (distance/6371)*180/Math.PI; //small angles approximation, still valid for about 500 metres String whereClause = StopsTable.COL_LAT+ "< "+(latitude+distasAngle)+" AND " +StopsTable.COL_LAT +" > "+(latitude-distasAngle)+" AND "+ StopsTable.COL_LONG+" < "+(longitude+distasAngle)+" AND "+StopsTable.COL_LONG+" > "+(longitude-distasAngle); //Log.d("Provider-LOCSearch","Querying stops by position, query args: \n"+whereClause); return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null); } else { Log.w(DEBUG_TAG,"Not enough parameters"); if(parts.size()>=5) for(String s:parts) Log.d(DEBUG_TAG,"\t element "+parts.indexOf(s)+" is: "+s); return null; } case FAVORITES_OP: final String stopFavSelection = getFavoritesColumnNamesAsArray[0]+" = ?"; db = userDBHelper.getReadableDatabase(); Log.d(DEBUG_TAG,"Asked information on Favorites about stop with id "+uri.getLastPathSegment()); return db.query(UserDB.TABLE_NAME,projection,stopFavSelection,new String[]{uri.getLastPathSegment()},null,null,sortOrder); case STOP_OP: //Let's try this plain and simple final String[] selectionValues = {uri.getLastPathSegment()}; final String stopSelection = StopsTable.COL_ID+" = ?"; Log.d(DEBUG_TAG,"Asked information about stop with id "+selectionValues[0]); return db.query(StopsTable.TABLE_NAME,projection,stopSelection,selectionValues,null,null,sortOrder); case MANY_STOPS: return db.query(StopsTable.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); case GET_ALL_FAVORITES: db = userDBHelper.getReadableDatabase(); return db.query(UserDB.TABLE_NAME, projection, selection, selectionArgs, null, null,sortOrder); default: Log.e("DataProvider","got request "+uri.getPath()+" which doesn't match anything"); } throw new UnsupportedOperationException("Not yet implemented"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } // public static Uri getBaseUriGivenOp(int operationType); public static Uri.Builder getUriBuilderToComplete(){ final Uri.Builder b = new Uri.Builder(); b.scheme("content").authority(AUTHORITY); return b; } @Override public void onLowMemory() { super.onLowMemory(); } } diff --git a/src/it/reyboz/bustorino/data/AppRepository.java b/src/it/reyboz/bustorino/data/AppRepository.java deleted file mode 100644 index 8bf5313..0000000 --- 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 index 704514a..3d669c1 100644 --- a/src/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/src/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -1,166 +1,184 @@ /* 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; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Notifications; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DBUpdateWorker extends Worker{ public static final String ERROR_CODE_KEY ="Error_Code"; public static final String ERROR_REASON_KEY = "ERROR_REASON"; public static final int ERROR_FETCHING_VERSION = 4; public static final int ERROR_DOWNLOADING_STOPS = 5; public static final int ERROR_DOWNLOADING_LINES = 6; public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; 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) { super(context, workerParams); } @SuppressLint("RestrictedApi") @NonNull @Override 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); final int new_DB_version = DatabaseUpdate.getNewVersion(); final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); 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); /* SKIP CHECK (Reason: The Old API might fail at any moment) if (new_DB_version < 0){ //there has been an error final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) .putInt(ERROR_CODE_KEY,new_DB_version).build(); cancelNotification(notificationID); return ListenableWorker.Result.failure(out); } */ //we got a good version 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()); } //start the real update AtomicReference resultAtomicReference = new AtomicReference<>(); DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); 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: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); break; case ERROR_LINES_DOWNLOAD: 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!"); //update the version in the shared preference final SharedPreferences.Editor editor = shPr.edit(); editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version); 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()); } public static Constraints getWorkConstraints(){ return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging(false).build(); } public static WorkRequest newFirstTimeWorkRequest(){ return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) //.setInputData(new Data.Builder().putBoolean()) .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()); notificationManager.cancel(notificationID); } } diff --git a/src/it/reyboz/bustorino/data/DatabaseUpdate.java b/src/it/reyboz/bustorino/data/DatabaseUpdate.java index f98df74..514a110 100644 --- a/src/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/src/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,224 +1,303 @@ /* 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; import android.content.ContentValues; import android.content.Context; 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; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { public static final String DEBUG_TAG = "BusTO-DBUpdate"; public static final int VERSION_UNAVAILABLE = -2; public static final int JSON_PARSING_ERROR = -4; public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; enum Result { 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); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } /** * Request update using workmanager framework * @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, ExistingPeriodicWorkPolicy.REPLACE, wr); } /* public static boolean isDBUpdating(){ return false; 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 index 0000000..98867d2 --- /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 index e500d0e..511bdfa 100644 --- a/src/it/reyboz/bustorino/data/NextGenDB.java +++ b/src/it/reyboz/bustorino/data/NextGenDB.java @@ -1,370 +1,461 @@ /* BusTO (middleware) Copyright (C) 2018 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; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.util.Log; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import java.util.*; import static it.reyboz.bustorino.data.NextGenDB.Contract.*; public class NextGenDB extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "bustodatabase.db"; public static final int DATABASE_VERSION = 3; public static final String DEBUG_TAG = "NextGenDB-BusTO"; //NO Singleton instance //private static volatile NextGenDB instance = null; //Some generating Strings private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+ Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+ Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+ "UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )"; private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+ Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+ Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+ Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+ //SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+ //DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+ BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+ "FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") " +")"; private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+ Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+ Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+ "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") " +")"; private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ StopsTable.COL_GTFS_ID+" TEXT, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; public static final String[] QUERY_COLUMN_stops_all = { StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_GTFS_ID, StopsTable.COL_LOCATION, StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING}; public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " + 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 public void onCreate(SQLiteDatabase db) { Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+ SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE); db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion<2 && newVersion == 2){ //DROP ALL TABLES db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME); db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME); db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME); db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME); //RECREATE THE TABLES WITH THE NEW SCHEMA db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints 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"); //add the new column db.execSQL("ALTER TABLE "+StopsTable.TABLE_NAME+ " ADD COLUMN "+StopsTable.COL_GTFS_ID+" TEXT "); // DatabaseUpdate.requestDBUpdateWithWork(appContext, true); } } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL("PRAGMA foreign_keys=ON"); } public static String getSqlCreateStopsTable(String tableName){ return "CREATE TABLE "+tableName+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; } /** * Query some bus stops inside a map view * * You can obtain the coordinates from OSMDroid using something like this: * BoundingBoxE6 bb = mMapView.getBoundingBox(); * double latFrom = bb.getLatSouthE6() / 1E6; * double latTo = bb.getLatNorthE6() / 1E6; * double lngFrom = bb.getLonWestE6() / 1E6; * double lngTo = bb.getLonEastE6() / 1E6; */ public synchronized ArrayList queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) { ArrayList stops = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); // coordinates must be strings in the where condition String minLatRaw = String.valueOf(minLat); String maxLatRaw = String.valueOf(maxLat); String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); if(db == null) { return stops; } try { final Cursor result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw}, null, null, null); stops = getStopsFromCursorAllFields(result); result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); e.printStackTrace(); return stops; }finally { db.close(); } 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 * @return an Array of the stops found in the query */ public static ArrayList getStopsFromCursorAllFields(Cursor result){ final int colID = result.getColumnIndex(StopsTable.COL_ID); final int colName = result.getColumnIndex(StopsTable.COL_NAME); 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); int count = result.getCount(); ArrayList stops = new ArrayList<>(count); int i = 0; 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); if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) { locationSometimesEmpty = null; } 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 stops){ return 0; } public static List splitLinesString(String linesStr){ return Arrays.asList(linesStr.split("\\s*,\\s*")); } public static final class Contract{ //Ok, I get it, it really is a pain in the ass.. // But it's the only way to have maintainable code public interface DataTables { String getTableName(); String[] getFields(); } public static final class LinesTable implements BaseColumns, DataTables { //The fields public static final String TABLE_NAME = "lines"; public static final String COLUMN_NAME = "line_name"; public static final String COLUMN_DESCRIPTION = "line_description"; public static final String COLUMN_TYPE = "line_bacino"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE}; } } public static final class BranchesTable implements BaseColumns, DataTables { public static final String TABLE_NAME = "branches"; public static final String COL_BRANCHID = "branchid"; public static final String COL_LINE = "lineid"; public static final String COL_DESCRIPTION = "branch_description"; public static final String COL_DIRECTION = "branch_direzione"; public static final String COL_FESTIVO = "branch_festivo"; public static final String COL_TYPE = "branch_type"; public static final String COL_LUN="runs_lun"; public static final String COL_MAR="runs_mar"; public static final String COL_MER="runs_mer"; public static final String COL_GIO="runs_gio"; public static final String COL_VEN="runs_ven"; public static final String COL_SAB="runs_sab"; public static final String COL_DOM="runs_dom"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION, COL_DIRECTION,COL_FESTIVO,COL_TYPE, COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM }; } } public static final class ConnectionsTable implements DataTables { public static final String TABLE_NAME = "connections"; public static final String COLUMN_BRANCH = "branchid"; public static final String COLUMN_STOP_ID = "stopid"; public static final String COLUMN_ORDER = "ordine"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER}; } } public static final class StopsTable implements DataTables { public static final String TABLE_NAME = "stops"; public static final String COL_ID = "stopid"; //integer public static final String COL_TYPE = "stop_type"; public static final String COL_NAME = "stop_name"; public static final String COL_GTFS_ID = "gtfs_id"; public static final String COL_LAT = "stop_latitude"; public static final String COL_LONG = "stop_longitude"; public static final String COL_LOCATION = "stop_location"; public static final String COL_PLACE = "stop_placeName"; public static final String COL_LINES_STOPPING = "stop_lines"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_GTFS_ID,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING}; } } } public static final class DBUpdatingException extends Exception{ public DBUpdatingException(String message) { super(message); } } } diff --git a/src/it/reyboz/bustorino/data/OldDataRepository.java b/src/it/reyboz/bustorino/data/OldDataRepository.java new file mode 100644 index 0000000..c521381 --- /dev/null +++ b/src/it/reyboz/bustorino/data/OldDataRepository.java @@ -0,0 +1,58 @@ +/* + 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; + +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 index 453630e..5f6e054 100644 --- a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt +++ b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt @@ -1,115 +1,114 @@ /* 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.gtfs 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() private var stopsIDsPresent: HashSet? = null private var tripsIDsPresent: HashSet? = null 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() } } fun addElement(csvLineElements: Map) { when(tableName){ "stops" -> elementsList.add(GtfsStop(csvLineElements)) "routes" -> elementsList.add(GtfsRoute(csvLineElements)) "calendar" -> elementsList.add(GtfsService(csvLineElements)) "calendar_dates" -> elementsList.add(GtfsServiceDate(csvLineElements)) "trips" -> elementsList.add(GtfsTrip(csvLineElements)) "shapes" -> elementsList.add(GtfsShape(csvLineElements)) "stop_times" -> { //filter stop val stopTime = GtfsStopTime(csvLineElements) /* val stopOk = //tripsIDsPresent?.contains(stopTime.tripID) == true (stopsIDsPresent?.contains(stopTime.stopID) == true)// && // tripsIDsPresent?.contains(stopTime.tripID) == true) if (stopOk) */ elementsList.add(stopTime) } } if(elementsList.size >= MAX_ELEMENTS){ //have to insert if (tableName == "routes") - dao.insertRoutes(elementsList.filterIsInstance()) + databaseDao.insertRoutes(elementsList.filterIsInstance()) else insertDataInDatabase() elementsList.clear() } } private fun insertDataInDatabase(){ //Log.d(DEBUG_TAG, "Inserting batch of elements now, list size: "+elementsList.size) 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{ const val MAX_ELEMENTS = 5000 const val DEBUG_TAG="BusTO - TableInserter" } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt new file mode 100644 index 0000000..e53729a --- /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 similarity index 62% rename from src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt rename to src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt index 32f862c..a54be35 100644 --- a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt @@ -1,90 +1,128 @@ /* 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.gtfs import androidx.lifecycle.LiveData 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) fun getAllTripsIDs() : List @Query("SELECT "+GtfsStop.COL_STOP_ID+" FROM "+GtfsStop.DB_TABLE) fun getAllStopsIDs() : List @Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" LIKE :queryID") fun getStopByStopID(queryID: String): LiveData> @Query("SELECT * FROM "+GtfsShape.DB_TABLE+ " WHERE "+GtfsShape.COL_SHAPE_ID+" LIKE :shapeID"+ " ORDER BY "+GtfsShape.COL_POINT_SEQ+ " ASC" ) 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) fun insertCalendarServices(services: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertShapes(shapes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertDates(dates: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertServices(services: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertTrips(trips: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStopTimes(stopTimes: List) @Query("DELETE FROM "+GtfsRoute.DB_TABLE) fun deleteAllRoutes() @Query("DELETE FROM "+GtfsStop.DB_TABLE) fun deleteAllStops() @Query("DELETE FROM "+GtfsTrip.DB_TABLE) fun deleteAllTrips() @Update(onConflict = OnConflictStrategy.REPLACE) fun updateShapes(shapes: List) : Int @Transaction fun updateAllStops(stops: List){ deleteAllStops() insertStops(stops) } @Query("DELETE FROM "+GtfsStopTime.DB_TABLE) fun deleteAllStopTimes() @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 index 986f01f..9b81cd4 100644 --- a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt @@ -1,57 +1,85 @@ /* 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.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 private var INSTANCE: GtfsDatabase? =null 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 index 0000000..8105fa6 --- /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 index 0000000..a9b7f2a --- /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 index fef6167..e68dcb9 100644 --- a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt @@ -1,79 +1,83 @@ /* 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.gtfs import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @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( valuesByColumn[COL_ROUTE_ID]!!, valuesByColumn["agency_id"]!!, 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", "route_type", "route_color", "route_text_color", COL_SORT_ORDER ) + + //const val CREATE_SQL = "" } override fun getColumns(): Array { return COLUMNS } } diff --git a/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt new file mode 100644 index 0000000..5e6e09b --- /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 index d44a58e..344b1fc 100644 --- a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -1,299 +1,305 @@ /* BusTO - Fragments 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.fragments; import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; 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; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; public class FavoritesFragment extends ScreenBaseFragment { private RecyclerView favoriteRecyclerView; private EditText busStopNameText; private TextView favoriteTipTextView; private ImageView angeryBusImageView; @Nullable private CommonFragmentListener mListener; 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; + } }; public static FavoritesFragment newInstance() { FavoritesFragment fragment = new FavoritesFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } public FavoritesFragment(){ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_favorites, container, false); favoriteRecyclerView = root.findViewById(R.id.favoritesRecyclerView); //favoriteListView = root.findViewById(R.id.favoriteListView); /*favoriteRecyclerView.setOn((parent, view, position, id) -> { /* * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ /* Stop busStop = (Stop) parent.getItemAtPosition(position); if(mListener!=null){ mListener.requestArrivalsForStopID(busStop.ID); } }); */ LinearLayoutManager llManager = new LinearLayoutManager(getContext()); llManager.setOrientation(LinearLayoutManager.VERTICAL); favoriteRecyclerView.setLayoutManager(llManager); DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(favoriteRecyclerView.getContext(), llManager.getOrientation()); favoriteRecyclerView.addItemDecoration(dividerItemDecoration); 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); model.getFavorites().observe(getViewLifecycleOwner(), this::showStops); showStops(new ArrayList<>()); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } /* 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; MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public void onResume() { super.onResume(); if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item .getMenuInfo(); if(!(favoriteRecyclerView.getAdapter() instanceof StopRecyclerAdapter)) return false; StopRecyclerAdapter adapter = (StopRecyclerAdapter) favoriteRecyclerView.getAdapter(); Stop busStop = adapter.getStops().get(adapter.getPosition()); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, result -> { }).execute(busStop); return true; case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: if (busStop.getLatitude() == null | busStop.getLongitude() == null | mListener==null ) { Toast.makeText(getContext(), R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show(); return true; } //GeoPoint point = new GeoPoint(busStop.getLatitude(), busStop.getLongitude()); mListener.showMapCenteredOnStop(busStop); return true; default: return super.onContextItemSelected(item); } } @Nullable @Override public View getBaseViewForSnackBar() { return null; } void showStops(List busStops){ // If no data is found show a friendly message if(BuildConfig.DEBUG) Log.d("BusTO - Favorites", "We have "+busStops.size()+" favorites in the list"); if (busStops.size() == 0) { favoriteRecyclerView.setVisibility(View.INVISIBLE); // TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); //assert favoriteTipTextView != null; favoriteTipTextView.setVisibility(View.VISIBLE); //ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); angeryBusImageView.setVisibility(View.VISIBLE); } else { favoriteRecyclerView.setVisibility(View.VISIBLE); favoriteTipTextView.setVisibility(View.INVISIBLE); angeryBusImageView.setVisibility(View.INVISIBLE); } /* There's a nice method called notifyDataSetChanged() to avoid building the ListView * all over again. This method exists in a billion answers on Stack Overflow, but * it's nowhere to be seen around here, Android Studio can't find it no matter what. * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I * guess) and requires to modify the list with .add() and .clear() and some other * methods, so to update a single stop we need to completely rebuild the list for no * reason. It would probably end up as "slow" as throwing away the old ListView and * 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) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); busStopNameText.setText(busStop.getStopDisplayName()); busStopNameText.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); builder.setPositiveButton(getString(android.R.string.ok), (dialog, which) -> { String busStopUsername = busStopNameText.getText().toString(); String oldUserName = busStop.getStopUserName(); // changed to none if(busStopUsername.length() == 0) { // unless it was already empty, set new if(oldUserName != null) { busStop.setStopUserName(null); } } else { // changed to something // something different? if(!busStopUsername.equals(oldUserName)) { busStop.setStopUserName(busStopUsername); } } launchUpdate(busStop); }); builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, (dialog, which) -> { // delete user name from database busStop.setStopUserName(null); launchUpdate(busStop); }); builder.show(); } private void launchUpdate(Stop busStop){ if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, result -> { //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); }).execute(busStop); } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentKind.java b/src/it/reyboz/bustorino/fragments/FragmentKind.java index 6aeac73..721b1ae 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentKind.java +++ b/src/it/reyboz/bustorino/fragments/FragmentKind.java @@ -1,22 +1,23 @@ /* BusTO (fragments) Copyright (C) 2018 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; 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 index c37740e..4dccc91 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java +++ b/src/it/reyboz/bustorino/fragments/FragmentListenerMain.java @@ -1,29 +1,32 @@ /* BusTO - Fragments components Copyright (C) 2018 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; +/** + * This interface is for the subfragments + */ public interface FragmentListenerMain extends CommonFragmentListener { void toggleSpinner(boolean state); /** * Tell activity that we need to enable/disable the refreshLayout * @param yes or no */ void enableRefreshLayout(boolean yes); } diff --git a/src/it/reyboz/bustorino/fragments/LinesFragment.kt b/src/it/reyboz/bustorino/fragments/LinesFragment.kt new file mode 100644 index 0000000..41b4e44 --- /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 index 0000000..0b88b2d --- /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 index 509958d..2b045a3 100644 --- a/src/it/reyboz/bustorino/fragments/MapFragment.java +++ b/src/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,645 +1,645 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo 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.fragments; import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.location.Location; import android.location.LocationManager; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.preference.PreferenceManager; import it.reyboz.bustorino.backend.utils; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.lang.ref.WeakReference; import java.util.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.map.CustomInfoWindow; import it.reyboz.bustorino.map.LocationOverlay; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.util.Permissions; public class MapFragment extends ScreenBaseFragment { private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; //the asynctask used to get the stops from the database private AsyncStopFetcher stopFetcher = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; private boolean followingLocation = false; protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(double stopLatit, double stopLong, String stopName, String stopID){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stopLatit); args.putDouble(BUNDLE_LONGIT, stopLong); args.putString(BUNDLE_NAME, stopName); args.putString(BUNDLE_ID, stopID); fragment.setArguments(args); return fragment; } public static MapFragment getInstance(@NonNull Stop stop){ return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.activity_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); btCenterMap = root.findViewById(R.id.ic_center_map); btFollowMe = root.findViewById(R.id.ic_follow_me); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.locationPermissionGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.locationPermissionGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); saveMapState(); //cancel asynctask Log.w(DEBUG_TAG, "On pause called"); if (stopFetcher!= null) stopFetcher.cancel(true); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); } @Override public void onResume() { super.onResume(); if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == null || map ==null) //nothing else to do return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //clear previous overlays map.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); } //ask for location permission if(!Permissions.locationPermissionGranted(activity)){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //TODO: show dialog for permission rationale Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show(); } positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.locationPermissionGranted(activity), map); // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); setLocationFollowing(false); // put the center a little bit off (animate later) startPoint = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.locationPermissionGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; setLocationFollowing(true); } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker Marker stopMarker = makeMarker(marker, name , ID, true); map.getController().animateTo(marker); } } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); if(!map.isShown()){ if(good) Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); good = false; continue; } else if(map.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } good=true; } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName, responder); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_marker, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(ID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return null; } /** * Simple asyncTask class to load the stops in the background * Holds a weak reference to the fragment to do callbacks */ static class AsyncStopFetcher extends AsyncTask>{ final WeakReference fragmentWeakReference; public AsyncStopFetcher(MapFragment fragment) { this.fragmentWeakReference = new WeakReference<>(fragment); } @Override protected List doInBackground(BoundingBoxLimit... limits) { if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return null; } 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(); return stops; } @Override protected void onPostExecute(List stops) { super.onPostExecute(stops); //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); if(fragmentWeakReference.get()==null) { Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return; } if (stops!=null) Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); fragmentWeakReference.get().showStopsMarkers(stops); } private static class BoundingBoxLimit{ final double longFrom, longTo, latitFrom, latitTo; public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { this.longFrom = longFrom; this.longTo = longTo; this.latitFrom = latitFrom; this.latitTo = latitTo; } } } } diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 6a983f5..0f87260 100644 --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,705 +1,674 @@ /* BusTO - Fragments components Copyright (C) 2018 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.content.SharedPreferences; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; 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; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; 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; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; import com.android.volley.*; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; 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.*; import it.reyboz.bustorino.adapters.SquareStopAdapter; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks { 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; private int fragment_type; public final static String FRAGMENT_TAG="NearbyStopsFrag"; //data Bundle private final String BUNDLE_LOCATION = "location"; private final int LOADER_ID = 0; private RecyclerView gridRecyclerView; private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; - boolean canStartDBQuery = true; private Location lastReceivedLocation = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance; protected SharedPreferences globalSharedPref; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private TextView messageTextView,titleTextView; private CommonScrollListener scrollListener; private AppCompatButton switchButton; private boolean firstLocForStops = true,firstLocForArrivals = true; public static final int COLUMN_WIDTH_DP = 250; private Integer MAX_DISTANCE = -3; private int MIN_NUM_STOPS = -1; private int TIME_INTERVAL_REQUESTS = -1; private AppLocationManager locManager; //These are useful for the case of nearby arrivals private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; + private boolean dbUpdateRunning = false; + + private ArrayList currentNearbyStops = new ArrayList<>(); + public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ public static NearbyStopsFragment newInstance(int fragmentType) { if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); NearbyStopsFragment fragment = new NearbyStopsFragment(); final Bundle args = new Bundle(1); args.putInt(FRAGMENT_TYPE_KEY,fragmentType); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { setFragmentType(getArguments().getInt(FRAGMENT_TYPE_KEY)); } 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); + } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment if (getContext() == null) throw new RuntimeException(); View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); gridRecyclerView.setLayoutManager(gridLayoutManager); gridRecyclerView.setHasFixedSize(false); circlingProgressBar = root.findViewById(R.id.loadingBar); flatProgressBar = root.findViewById(R.id.horizontalProgressBar); messageTextView = root.findViewById(R.id.messageTextView); 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; } @Override 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: } } @Override public void onLoaderReset(@NonNull Loader loader) { } /** * To enable targeting from the Button */ public void switchFragmentType(View v){ switchFragmentType(); } /** * Call when you need to switch the type of fragment */ private void switchFragmentType(){ if(fragment_type==TYPE_ARRIVALS){ setFragmentType(TYPE_STOPS); switchButton.setText(getString(R.string.show_arrivals)); titleTextView.setText(getString(R.string.nearby_stops_message)); if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); if(dataAdapter!=null) gridRecyclerView.setAdapter(dataAdapter); } else if (fragment_type==TYPE_STOPS){ setFragmentType(TYPE_ARRIVALS); titleTextView.setText(getString(R.string.nearby_arrivals_message)); switchButton.setText(getString(R.string.show_stops)); if(arrivalsStopAdapter!=null) 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){ if(firstLocForStops) { dataAdapter = new SquareStopAdapter(stops, mListener, lastReceivedLocation); gridRecyclerView.setAdapter(dataAdapter); firstLocForStops = false; }else { dataAdapter.setStops(stops); dataAdapter.setUserPosition(lastReceivedLocation); } dataAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage(); if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); } private void showArrivalsInRecycler(List palinas){ Collections.sort(palinas,new StopSorterByDistance(lastReceivedLocation)); final ArrayList> routesPairList = new ArrayList<>(10); //int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: palinas){ //if there are no routes available, skip stop 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){ Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached"); return; } if(firstLocForArrivals){ arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastReceivedLocation); gridRecyclerView.setAdapter(arrivalsStopAdapter); firstLocForArrivals = false; } else { arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastReceivedLocation); } //arrivalsStopAdapter.notifyDataSetChanged(); showRecyclerHidingLoadMessage(); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); } private void setNoStopsLayout(){ messageTextView.setVisibility(View.VISIBLE); messageTextView.setText(R.string.no_stops_nearby); circlingProgressBar.setVisibility(View.GONE); } /** * Does exactly what is says on the tin */ private void showRecyclerHidingLoadMessage(){ if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } 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); } @Override public void onErrorResponse(VolleyError error) { if(error instanceof ParseError){ //TODO Log.w(DEBUG_TAG,"Parsing error for stop request"); } else if (error instanceof NetworkError){ String s; if(error.networkResponse!=null) s = new String(error.networkResponse.data); else s=""; Log.w(DEBUG_TAG,"Network error: "+s); }else { Log.w(DEBUG_TAG,"Volley Error: "+error.getMessage()); } if(error.networkResponse!=null){ Log.w(DEBUG_TAG, "Error status code: "+error.networkResponse.statusCode); } //counters activeRequestCount--; reqErrorCount++; flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); } @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); } showArrivalsInRecycler(outList); flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); if(activeRequestCount==0) { flatProgressBar.setIndeterminate(true); flatProgressBar.setVisibility(View.GONE); } } void cancelAllRequests(){ volleyManager.getRequestQueue().cancelAll(REQUEST_TAG); flatProgressBar.setVisibility(View.GONE); } } /** * Local locationListener, to use for the GPS */ class FragmentLocationListener implements AppLocationManager.LocationRequester{ LoaderManager.LoaderCallbacks callbacks; private int oldLocStatus = -2; private LocationCriteria cr; private long lastUpdateTime = -1; public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) { this.callbacks = callbacks; } @Override 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 public void onLocationStatusChanged(int status) { switch(status){ case AppLocationManager.LOCATION_GPS_AVAILABLE: messageTextView.setVisibility(View.GONE); break; case AppLocationManager.LOCATION_UNAVAILABLE: messageTextView.setText(R.string.enableGpsText); messageTextView.setVisibility(View.VISIBLE); break; default: Log.e(DEBUG_TAG,"Location status not recognized"); } } @Override public LocationCriteria getLocationCriteria() { return new LocationCriteria(120,TIME_INTERVAL_REQUESTS); } @Override public long getLastUpdateTimeMillis() { return lastUpdateTime; } void resetUpdateTime(){ lastUpdateTime = -1; } @Override public void onLocationProviderAvailable() { } @Override public void onLocationDisabled() { } } /** * Simple trick to get an automatic number of columns (from https://www.journaldev.com/13792/android-gridlayoutmanager-example) * */ class AutoFitGridLayoutManager extends GridLayoutManager { private int columnWidth; private boolean columnWidthChanged = true; public AutoFitGridLayoutManager(Context context, int columnWidth) { super(context, 1); setColumnWidth(columnWidth); } public void setColumnWidth(int newColumnWidth) { if (newColumnWidth > 0 && newColumnWidth != columnWidth) { columnWidth = newColumnWidth; columnWidthChanged = true; } } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (columnWidthChanged && columnWidth > 0) { int totalSpace; if (getOrientation() == VERTICAL) { totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); } else { totalSpace = getHeight() - getPaddingTop() - getPaddingBottom(); } int spanCount = Math.max(1, totalSpace / columnWidth); setSpanCount(spanCount); columnWidthChanged = false; } super.onLayoutChildren(recycler, state); } } } diff --git a/src/it/reyboz/bustorino/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java index afc4d83..8d8681c 100644 --- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -1,146 +1,168 @@ /* BusTO - Fragments components Copyright (C) 2020 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.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; 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; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = SettingsFragment.class.getName(); private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; //private static final Handler mHandler; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mHandler = new Handler(); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { //getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences)); convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext()); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); setPreferencesFromResource(R.xml.preferences,rootKey); /*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents)); editPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setSelection(0,editText.getText().length()); }); */ //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"); + } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); Log.d(TAG,"Preference key "+key+" changed"); //sometimes this happens if(getContext()==null) return; /* THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES if (key.equals(getString(R.string.pref_key_num_recents))){ //check that is it an int String value = sharedPreferences.getString(key,""); boolean valid = value.length() != 0; try{ Integer intValue = Integer.parseInt(value); } catch (NumberFormatException ex){ valid = false; } if (!valid){ Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show(); if(pref instanceof EditTextPreference){ EditTextPreference prefEdit = (EditTextPreference) pref; //Intent intent = prefEdit.getIntent(); Log.d(TAG, "opening preference, dialog showing "+ (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) ); //getPreferenceManager().showDialog(pref); //onDisplayPreferenceDialog(prefEdit); mHandler.postDelayed(new DelayedDisplay(prefEdit), 500); } } } */ Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll()); } private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){ if (con == null) return; SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(getContext()); try{ Integer val = defaultSharedPref.getInt(preferenceKey, 0); } catch (NumberFormatException | ClassCastException ex){ //convert the preference //final String preferenceNumRecents = getString(R.string.pref_key_num_recents); Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey); String currentValue = defaultSharedPref.getString(preferenceKey, "10"); int newValue; try{ newValue = Integer.parseInt(currentValue); } catch (NumberFormatException e){ newValue = 10; } final SharedPreferences.Editor editor = defaultSharedPref.edit(); editor.remove(preferenceKey); editor.putInt(preferenceKey, newValue); editor.apply(); } } class DelayedDisplay implements Runnable{ private final WeakReference preferenceWeakReference; public DelayedDisplay(DialogPreference preference) { this.preferenceWeakReference = new WeakReference<>(preference); } @Override public void run() { if(preferenceWeakReference.get()==null) return; getPreferenceManager().showDialog(preferenceWeakReference.get()); } } } diff --git a/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java b/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java index 13a8491..ad8f980 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java +++ b/src/it/reyboz/bustorino/middleware/AsyncArrivalsSearcher.java @@ -1,332 +1,332 @@ /* BusTO (middleware) Copyright (C) 2018 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.middleware; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.SQLException; import android.net.Uri; import android.os.AsyncTask; import androidx.annotation.NonNull; import android.util.Log; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.Calendar; /** * This should be used to download data, but not to display it */ public class AsyncArrivalsSearcher extends AsyncTask{ private static final String TAG = "BusTO-DataDownload"; private static final String DEBUG_TAG = TAG; private boolean failedAll = false; private final AtomicReference finalResultRef; private String query; WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); private final ArrivalsFetcher[] theFetchers; @SuppressLint("StaticFieldLeak") private final Context context; private final boolean replaceFragment; public AsyncArrivalsSearcher(FragmentHelper fh, @NonNull ArrivalsFetcher[] fetchers, Context context) { helperRef = new WeakReference<>(fh); fh.setLastTaskRef(this); finalResultRef = new AtomicReference<>(); this.context = context.getApplicationContext(); this.replaceFragment = true; theFetchers = fetchers; if (theFetchers.length < 1){ throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); } } @Override protected Palina doInBackground(String... params) { RecursionHelper r = new RecursionHelper<>(theFetchers); Palina resultPalina = null; FragmentHelper fh = helperRef.get(); ArrayList results = new ArrayList<>(theFetchers.length); //If the FragmentHelper is null, that means the activity doesn't exist anymore if (fh == null){ return null; } //Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue()); while(r.valid()) { if(this.isCancelled()) { return null; } //get the data from the fetcher ArrivalsFetcher f = r.getAndMoveForward(); AtomicReference resRef = new AtomicReference<>(); if (f instanceof MatoAPIFetcher){ ((MatoAPIFetcher)f).setAppContext(context); } Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); Palina p; String stopID; if(params.length>0) stopID=params[0]; //(it's a Palina) else if(lastSearchedBusStop!=null) stopID = lastSearchedBusStop.ID; //(it's a Palina) else { publishProgress(Fetcher.Result.QUERY_TOO_SHORT); return null; } //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times try { if (f instanceof FiveTAPIFetcher && Integer.parseInt(stopID) >= 8200) continue; } catch (NumberFormatException ex){ Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); } p= f.ReadArrivalTimesAll(stopID,resRef); //if (res.get()!= Fetcher.Result.OK) Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+resRef.get()); if(f instanceof FiveTAPIFetcher){ AtomicReference gres = new AtomicReference<>(); List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); Log.d(DEBUG_TAG, "FiveTArrivals fetcher: "+f+"\n\tDetails req: "+gres.get()); if(gres.get() == Fetcher.Result.OK){ p.addInfoFromRoutes(branches); Thread t = new Thread(new BranchInserter(branches, context)); t.start(); otherActivities.add(t); } else{ resRef.set(Fetcher.Result.NOT_FOUND); } //put updated values into Database } if(lastSearchedBusStop != null && resRef.get()== Fetcher.Result.OK) { // check that we don't have the same stop if(lastSearchedBusStop.ID.equals(p.ID)) { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); if(sn != null) { // "merge" Stop over Palina and we're good to go p.mergeNameFrom(lastSearchedBusStop); } } } p.mergeDuplicateRoutes(0); if (resRef.get() == Fetcher.Result.OK && p.getTotalNumberOfPassages() == 0 ) { resRef.set(Fetcher.Result.EMPTY_RESULT_SET); Log.d(DEBUG_TAG, "Setting empty results"); } publishProgress(resRef.get()); //TODO: find a way to avoid overloading the user with toasts if (resultPalina == null && f instanceof MatoAPIFetcher && p.queryAllRoutes().size() > 0){ resultPalina = p; } //find if it went well results.add(resRef.get()); if(resRef.get()== Fetcher.Result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return p; } finalResultRef.set(resRef.get()); } /* boolean emptyResults = true; for (Fetcher.Result re: results){ if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { emptyResults = false; break; } } */ //at this point, we are sure that the result has been negative failedAll=true; return resultPalina; } @Override protected void onProgressUpdate(Fetcher.Result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) for (Fetcher.Result r : values){ //TODO: make Toast fh.showErrorMessage(r, SearchRequestType.ARRIVALS); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Palina p) { FragmentHelper fh = helperRef.get(); if(p == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; fh.createOrUpdateStopFragment( p, replaceFragment); } @Override protected void onCancelled() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(true); } public static class BranchInserter implements Runnable{ private final List routesToInsert; private final Context context; //private final NextGenDB nextGenDB; public BranchInserter(List routesToInsert,@NonNull Context con) { this.routesToInsert = routesToInsert; this.context = con.getApplicationContext(); //nextGenDB = new NextGenDB(context); } @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); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); //values[routesToInsert.indexOf(r)] = cv; branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i0) { starttime = System.currentTimeMillis(); ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]); Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections"); int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME); endtime = System.currentTimeMillis(); Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows"); } nextGenDB.close(); } } } diff --git a/src/it/reyboz/bustorino/util/LinesNameSorter.java b/src/it/reyboz/bustorino/util/LinesNameSorter.java index 2d04bb3..929129f 100644 --- a/src/it/reyboz/bustorino/util/LinesNameSorter.java +++ b/src/it/reyboz/bustorino/util/LinesNameSorter.java @@ -1,41 +1,115 @@ /* BusTO (util) Copyright (C) 2019 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.util; import java.util.Comparator; 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; + } + } }