Index: app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/3.json
===================================================================
--- /dev/null
+++ app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/3.json
@@ -0,0 +1,699 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "f5110d1db452ee714d9d93838860a335",
+ "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` TEXT NOT NULL, `limited_route` INTEGER NOT NULL, `pattern_code` TEXT NOT NULL DEFAULT '', `semantic_hash` TEXT, 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": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLimitedRoute",
+ "columnName": "limited_route",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_code",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "semanticHash",
+ "columnName": "semantic_hash",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "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`)"
+ },
+ {
+ "name": "index_gtfs_trips_trip_id",
+ "unique": false,
+ "columnNames": [
+ "trip_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_trip_id` ON `${TABLE_NAME}` (`trip_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": [
+ {
+ "name": "index_mato_patterns_pattern_code",
+ "unique": false,
+ "columnNames": [
+ "pattern_code"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_mato_patterns_pattern_code` ON `${TABLE_NAME}` (`pattern_code`)"
+ },
+ {
+ "name": "index_mato_patterns_pattern_route_id",
+ "unique": false,
+ "columnNames": [
+ "pattern_route_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_mato_patterns_pattern_route_id` ON `${TABLE_NAME}` (`pattern_route_id`)"
+ }
+ ],
+ "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": [
+ {
+ "name": "index_patterns_stops_pattern_gtfs_id",
+ "unique": false,
+ "columnNames": [
+ "pattern_gtfs_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_patterns_stops_pattern_gtfs_id` ON `${TABLE_NAME}` (`pattern_gtfs_id`)"
+ }
+ ],
+ "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, 'f5110d1db452ee714d9d93838860a335')"
+ ]
+ }
+}
\ No newline at end of file
Index: app/build.gradle
===================================================================
--- app/build.gradle
+++ app/build.gradle
@@ -69,63 +69,64 @@
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
- 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-ktx:$lifecycle_version"
- // Lifecycles only (without ViewModel or LiveData)
- 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"
- //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"
+ 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'
+ //osmbonuspack is not needed yet, when you DO need it remember to add maven repo "https://jitpack.io"
+ //implementation 'com.github.MKergall:osmbonuspack:6.9.0'
+ // 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-ktx:$lifecycle_version"
+ // Lifecycles only (without ViewModel or LiveData)
+ 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"
+ implementation "androidx.work:work-runtime-ktx:$work_version"
+ kapt "androidx.room:room-compiler:$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"
}
Index: app/res/drawable/bus_stop.xml
===================================================================
--- /dev/null
+++ app/res/drawable/bus_stop.xml
@@ -0,0 +1,12 @@
+
+
+
Index: app/res/drawable/point_heading_icon.xml
===================================================================
--- /dev/null
+++ app/res/drawable/point_heading_icon.xml
@@ -0,0 +1,14 @@
+
+
+
+
Index: app/res/layout/bus_info_window.xml
===================================================================
--- /dev/null
+++ app/res/layout/bus_info_window.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
Index: app/res/layout/map_popup.xml
===================================================================
--- app/res/layout/map_popup.xml
+++ app/res/layout/map_popup.xml
@@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
- android:textColor="@color/blue_700"
+ android:textColor="@color/red_darker"
android:textSize="16sp"
android:maxWidth="400sp"
/>
Index: app/res/values-it/strings.xml
===================================================================
--- app/res/values-it/strings.xml
+++ app/res/values-it/strings.xml
@@ -160,6 +160,7 @@
Canale unico delle notifiche
Database
Informazioni sul database (aggiornamento)
+ Downloading trips from MaTO server
Chiesto troppe volte per il permesso %1$s
Index: app/res/values/colors.xml
===================================================================
--- app/res/values/colors.xml
+++ app/res/values/colors.xml
@@ -3,6 +3,8 @@
#ff9800
#F57C00
+ #cc6600
+ #994d00
#2196F3
#2a65e8
#2060dd
@@ -18,9 +20,10 @@
#DE0908
+ #b30000
#2060DD
#FFFFFF
#000000
@color/blue_mid_2
\ No newline at end of file
Index: app/res/values/strings.xml
===================================================================
--- app/res/values/strings.xml
+++ app/res/values/strings.xml
@@ -183,6 +183,7 @@
Default channel for notifications
Database
Notifications on the update of the database
+ Downloading trips from MaTO server
Asked for %1$s permission too many times
Cannot use the map with the storage permission!
Index: app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java
===================================================================
--- app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java
+++ app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java
@@ -48,6 +48,7 @@
// Array of all migrations
private static final Migration[] ALL_MIGRATIONS = new Migration[]{
- GtfsDatabase.Companion.getMIGRATION_1_2()};
+ GtfsDatabase.Companion.getMIGRATION_1_2(),
+ };
}
Index: app/src/it/reyboz/bustorino/ActivityPrincipal.java
===================================================================
--- app/src/it/reyboz/bustorino/ActivityPrincipal.java
+++ app/src/it/reyboz/bustorino/ActivityPrincipal.java
@@ -24,6 +24,7 @@
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
@@ -350,6 +351,13 @@
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode==STORAGE_PERMISSION_REQ){
final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE;
+ if (permissionDoneRunnables.containsKey(storagePerm)) {
+ Runnable toRun = permissionDoneRunnables.get(storagePerm);
+ if (toRun != null)
+ toRun.run();
+ permissionDoneRunnables.remove(storagePerm);
+ }
+
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions));
@@ -469,8 +477,17 @@
}
private void requestMapFragment(final boolean allowReturn){
+ // starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
+ //nothing to do
+ Log.d(DEBUG_TAG, "Build codes allow the showing of the map");
+ createAndShowMapFragment(null, allowReturn);
+ return;
+ }
final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ);
+ Log.d(DEBUG_TAG, "Permission for storage: "+result);
switch (result) {
case PERMISSION_OK:
createAndShowMapFragment(null, allowReturn);
Index: app/src/it/reyboz/bustorino/backend/ServiceType.java
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/backend/ServiceType.java
@@ -0,0 +1,9 @@
+package it.reyboz.bustorino.backend;
+
+public enum ServiceType {
+ URBANO,
+ EXTRAURBANO,
+ TURISTICO,
+ FERROVIA,
+ UNKNOWN,
+}
Index: app/src/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt
@@ -0,0 +1,59 @@
+/*
+ BusTO - Backend components
+ Copyright (C) 2023 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.gtfs
+
+import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship
+import com.google.transit.realtime.GtfsRealtime.VehiclePosition
+import com.google.transit.realtime.GtfsRealtime.VehiclePosition.OccupancyStatus
+
+data class GtfsPositionUpdate(
+ val tripID: String,
+ val startTime: String,
+ val startDate: String,
+ val routeID: String,
+
+ val latitude: Float,
+ val longitude: Float,
+ val bearing: Float,
+
+ val timestamp: Long,
+
+ val vehicleInfo: VehicleInfo,
+ val occupancyStatus: OccupancyStatus?,
+ val scheduleRelationship: ScheduleRelationship?
+){
+ constructor(position: VehiclePosition) : this(
+ position.trip.tripId,
+ position.trip.startTime,
+ position.trip.startDate,
+ position.trip.routeId,
+ position.position.latitude,
+ position.position.longitude,
+ position.position.bearing,
+ position.timestamp,
+ VehicleInfo(position.vehicle.id, position.vehicle.label),
+ position.occupancyStatus,
+ null
+ )
+ data class VehicleInfo(
+ val id: String,
+ val label:String
+ )
+}
+
+
Index: app/src/it/reyboz/bustorino/backend/gtfs/GtfsRealtimeRequest.kt
===================================================================
--- app/src/it/reyboz/bustorino/backend/gtfs/GtfsRealtimeRequest.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package it.reyboz.bustorino.backend.gtfs
-
-import com.android.volley.NetworkResponse
-import com.android.volley.Request
-import com.android.volley.Response
-import com.android.volley.VolleyError
-import com.android.volley.toolbox.HttpHeaderParser
-import com.google.transit.realtime.GtfsRealtime
-
-class GtfsRealtimeRequest(url: String?,
- errorListener: Response.ErrorListener?,
- val listener: RequestListener) :
- Request(Method.GET, url, errorListener) {
- override fun parseNetworkResponse(response: NetworkResponse?): Response {
- if (response == null){
- return Response.error(VolleyError("Null response"))
- }
-
- if (response.statusCode != 200)
- return Response.error(VolleyError("Error code is ${response.statusCode}"))
-
- val gtfsreq = GtfsRealtime.FeedMessage.parseFrom(response.data)
-
- return Response.success(gtfsreq, HttpHeaderParser.parseCacheHeaders(response))
- }
-
- override fun deliverResponse(response: GtfsRealtime.FeedMessage?) {
- listener.onResponse(response)
- }
-
- companion object{
- const val URL_POSITION = "http://percorsieorari.gtt.to.it/das_gtfsrt/vehicle_position.aspx"
-
- const val URL_TRIP_UPDATES ="http://percorsieorari.gtt.to.it/das_gtfsrt/trip_update.aspx"
- const val URL_ALERTS = "http://percorsieorari.gtt.to.it/das_gtfsrt/alerts.aspx"
-
- public interface RequestListener{
- fun onResponse(response: GtfsRealtime.FeedMessage?)
- }
- }
-
-
-}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt
@@ -0,0 +1,72 @@
+/*
+ BusTO - Backend components
+ Copyright (C) 2023 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.gtfs
+
+import com.android.volley.NetworkResponse
+import com.android.volley.Request
+import com.android.volley.Response
+import com.android.volley.VolleyError
+import com.android.volley.toolbox.HttpHeaderParser
+import com.google.transit.realtime.GtfsRealtime
+import com.google.transit.realtime.GtfsRealtime.VehiclePosition
+
+class GtfsRtPositionsRequest(
+ errorListener: Response.ErrorListener?,
+ val listener: RequestListener) :
+ Request>(Method.GET, URL_POSITION, errorListener) {
+ override fun parseNetworkResponse(response: NetworkResponse?): Response> {
+ if (response == null){
+ return Response.error(VolleyError("Null response"))
+ }
+
+ if (response.statusCode != 200)
+ return Response.error(VolleyError("Error code is ${response.statusCode}"))
+
+ val gtfsreq = GtfsRealtime.FeedMessage.parseFrom(response.data)
+
+ val positionList = ArrayList()
+
+ if (gtfsreq.hasHeader() && gtfsreq.entityCount>0){
+ for (i in 0 until gtfsreq.entityCount){
+ val entity = gtfsreq.getEntity(i)
+ if (entity.hasVehicle()){
+ positionList.add(GtfsPositionUpdate(entity.vehicle))
+ }
+ }
+ }
+
+ return Response.success(positionList, HttpHeaderParser.parseCacheHeaders(response))
+ }
+
+ override fun deliverResponse(response: ArrayList?) {
+ listener.onResponse(response)
+ }
+
+ companion object{
+ const val URL_POSITION = "http://percorsieorari.gtt.to.it/das_gtfsrt/vehicle_position.aspx"
+
+ const val URL_TRIP_UPDATES ="http://percorsieorari.gtt.to.it/das_gtfsrt/trip_update.aspx"
+ const val URL_ALERTS = "http://percorsieorari.gtt.to.it/das_gtfsrt/alerts.aspx"
+
+ public interface RequestListener{
+ fun onResponse(response: ArrayList?)
+ }
+ }
+
+
+}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java
@@ -0,0 +1,62 @@
+/*
+ BusTO - Backend components
+ Copyright (C) 2023 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.gtfs;
+
+import androidx.core.util.Pair;
+import it.reyboz.bustorino.backend.ServiceType;
+
+abstract public class GtfsUtils {
+ public static Pair getRouteInfoFromGTFS(String routeID){
+ String[] explo = routeID.split(":");
+ //default is
+ String toParse = routeID;
+ if(explo.length>1) {
+ toParse = explo[1];
+ }
+ ServiceType serviceType=ServiceType.UNKNOWN;
+ final int length = toParse.length();
+ final char v =toParse.charAt(length-1);
+ switch (v){
+ case 'E':
+ serviceType = ServiceType.EXTRAURBANO;
+ break;
+ case 'F':
+ serviceType = ServiceType.FERROVIA;
+ break;
+ case 'T':
+ serviceType = ServiceType.TURISTICO;
+ break;
+ case 'U':
+ serviceType=ServiceType.URBANO;
+ }
+ //boolean barrato=false;
+ String num = toParse.substring(0, length-1);
+ /*if(toParse.charAt(length-2)=='B'){
+ //is barrato
+ barrato = true;
+ num = toParse.substring(0,length-2)+" /";
+ }else {
+ num = toParse.substring(0,length-1);
+ }*/
+ return new Pair<>(serviceType,num);
+ }
+
+ public static String getLineNameFromGtfsID(String routeID){
+ return getRouteInfoFromGTFS(routeID).second;
+ }
+}
Index: app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
===================================================================
--- app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
+++ app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
@@ -38,23 +38,25 @@
import kotlin.collections.ArrayList
-open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher {
+open class MatoAPIFetcher(
+ private val minNumPassaggi: Int
+) : ArrivalsFetcher {
var appContext: Context? = null
set(value) {
field = value!!.applicationContext
}
- constructor(): this(2)
+ constructor(): this(DEF_MIN_NUMPASSAGGI)
override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina {
stopID!!
val now = Calendar.getInstance().time
- var numMinutes = 0
+ var numMinutes = 30
var palina = Palina(stopID)
var numPassaggi = 0
var trials = 0
- val numDepartures = 4
+ val numDepartures = 8
while (numPassaggi < minNumPassaggi && trials < 2) {
//numDepartures+=2
@@ -87,7 +89,7 @@
} catch (e: ExecutionException) {
e.printStackTrace()
if (res.get() == Fetcher.Result.OK)
- res.set(Fetcher.Result.SERVER_ERROR)
+ res.set(Fetcher.Result.SERVER_ERROR)
} catch (e: TimeoutException) {
res.set(Fetcher.Result.CONNECTION_ERROR)
e.printStackTrace()
@@ -107,6 +109,7 @@
const val VOLLEY_TAG = "MatoAPIFetcher"
const val DEBUG_TAG = "BusTO:MatoAPIFetcher"
+ const val DEF_MIN_NUMPASSAGGI=2
val REQ_PARAMETERS = mapOf(
"Content-Type" to "application/json; charset=utf-8",
@@ -122,6 +125,7 @@
MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds"
MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes"
MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute"
+ MatoQueries.QueryType.TRIP -> VOLLEY_TAG+"_Trip"
}
}
Index: app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt
===================================================================
--- app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt
+++ app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt
@@ -147,6 +147,28 @@
}
}
"""
+ const val TRIP_DETAILS="""
+ query TripInfo(${'$'}field: String!){
+ trip(id: ${'$'}field){
+ gtfsId
+ serviceId
+ route{
+ gtfsId
+ }
+ pattern{
+ name
+ code
+ headsign
+ }
+ wheelchairAccessible
+ activeDates
+ tripShortName
+ tripHeadsign
+ bikesAllowed
+ semanticHash
+ }
+ }
+ """
fun getNameAndRequest(type: QueryType): Pair{
return when (type){
@@ -155,12 +177,13 @@
QueryType.ARRIVALS -> Pair("AllStopsDirect", QUERY_ARRIVALS)
QueryType.ROUTES -> Pair("AllRoutes", ROUTES_BY_FEED)
QueryType.PATTERNS_FOR_ROUTES -> Pair("RoutesWithPatterns", ROUTES_WITH_PATTERNS)
+ QueryType.TRIP -> Pair("TripInfo", TRIP_DETAILS)
}
}
}
enum class QueryType {
- ARRIVALS, ALL_STOPS, FEEDS, ROUTES, PATTERNS_FOR_ROUTES
+ ARRIVALS, ALL_STOPS, FEEDS, ROUTES, PATTERNS_FOR_ROUTES, TRIP
}
}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt
===================================================================
--- app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt
+++ app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt
@@ -35,7 +35,13 @@
return Response.error(VolleyError("Response not ready, status "+response.statusCode))
val obj:JSONObject
try {
- obj = JSONObject(String(response.data)).getJSONObject("data")
+ val resp = JSONObject(String(response.data))
+ obj = resp.getJSONObject("data")
+ if (resp.has("errors")){
+
+ Log.e("BusTO:MatoJSON","Errors encountered in the response: ${resp.getJSONObject("errors")}\n" +
+ "Variables to the query where: ${variables}\nThe next action done with the data is probably going to fail")
+ }
}catch (ex: JSONException){
Log.e("BusTO-VolleyJSON","Cannot parse response as JSON")
ex.printStackTrace()
Index: app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt
===================================================================
--- app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt
+++ app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt
@@ -17,12 +17,20 @@
*/
package it.reyboz.bustorino.backend.mato
+import android.util.Log
import it.reyboz.bustorino.data.gtfs.*
+import org.json.JSONException
import org.json.JSONObject
+/**
+ * Class to hold the code for the parsing of responses from the Mato API,
+ * from the JSON Object
+ */
abstract class ResponseParsing{
companion object{
+
+ final val DEBUG_TAG="BusTO:MatoResponseParse"
fun parseAgencyJSON(jsonObject: JSONObject): GtfsAgency {
return GtfsAgency(
jsonObject.getString("gtfsId"),
@@ -115,6 +123,31 @@
return patternsOut
}
+ fun parseTripInfo(jsonData: JSONObject): GtfsTrip?{
+
+
+ return try {
+ val jsonTrip = jsonData.getJSONObject("trip")
+
+ val routeId = jsonTrip.getJSONObject("route").getString("gtfsId")
+
+ val patternId =jsonTrip.getJSONObject("pattern").getString("code")
+ // still have "activeDates" which are the days in which the pattern is active
+ //Log.d("BusTO:RequestParsing", "Making GTFS trip for: $jsonData")
+ val trip = GtfsTrip(
+ routeId, jsonTrip.getString("serviceId"), jsonTrip.getString("gtfsId"),
+ jsonTrip.getString("tripHeadsign"), -1, "", "",
+ Converters.wheelchairFromString(jsonTrip.getString("wheelchairAccessible")),
+ false, patternId, jsonTrip.getString("semanticHash")
+ )
+ trip
+ } catch (e: JSONException){
+ Log.e(DEBUG_TAG, "Cannot parse json to make trip")
+ Log.e(DEBUG_TAG, "Json Data: $jsonData")
+ e.printStackTrace()
+ null
+ }
+ }
}
}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/backend/utils.java
===================================================================
--- app/src/it/reyboz/bustorino/backend/utils.java
+++ app/src/it/reyboz/bustorino/backend/utils.java
@@ -281,6 +281,20 @@
return outFetchers;
}
}
+ /*public String getShorterDirection(String headSign){
+ String[] parts = headSign.split(",");
+ if (parts.length<=1){
+ return headSign.trim();
+ }
+ String first = parts[0].trim();
+ String second = parts[1].trim();
+ String firstLower = first.toLowerCase(Locale.ITALIAN);
+ switch (firstLower){
+ case "circolare destra":
+ case "circolare sinistra":
+ case
+ }
+ }*/
/**
* 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
Index: app/src/it/reyboz/bustorino/data/GtfsRepository.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/GtfsRepository.kt
+++ app/src/it/reyboz/bustorino/data/GtfsRepository.kt
@@ -1,17 +1,15 @@
package it.reyboz.bustorino.data
+import android.content.Context
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
+import it.reyboz.bustorino.data.gtfs.*
class GtfsRepository(
val gtfsDao: GtfsDBDao
) {
-
+ constructor(context: Context) : this(GtfsDatabase.getGtfsDatabase(context).gtfsDao())
fun getLinesLiveDataForFeed(feed: String): LiveData>{
//return withContext(Dispatchers.IO){
return gtfsDao.getRoutesForFeed(feed)
@@ -19,7 +17,7 @@
}
fun getPatternsForRouteID(routeID: String): LiveData>{
return if(routeID.isNotEmpty())
- gtfsDao.getPatternsByRouteID(routeID)
+ gtfsDao.getPatternsLiveDataByRouteID(routeID)
else
MutableLiveData(listOf())
}
Index: app/src/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt
@@ -0,0 +1,132 @@
+/*
+ BusTO - Data components
+ Copyright (C) 2023 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.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.lifecycle.viewModelScope
+import androidx.work.CoroutineWorker
+import androidx.work.ForegroundInfo
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.android.volley.Response
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.backend.Notifications
+import it.reyboz.bustorino.data.gtfs.GtfsTrip
+import kotlinx.coroutines.launch
+import java.util.concurrent.CountDownLatch
+
+class MatoDownloadTripsWorker(appContext: Context, workerParams: WorkerParameters)
+ : CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ //val imageUriInput =
+ // inputData.("IMAGE_URI") ?: return Result.failure()
+ val tripsList = inputData.getStringArray(TRIPS_KEYS)
+
+ if (tripsList== null){
+ Log.e(DEBUG_TAG,"trips list given is null")
+ return Result.failure()
+ }
+ val gtfsRepository = GtfsRepository(applicationContext)
+ val matoRepository = MatoRepository(applicationContext)
+ //clear the matoTrips
+ val queriedMatoTrips = HashSet()
+
+ val downloadedMatoTrips = ArrayList()
+ val failedMatoTripsDownload = HashSet()
+
+
+ Log.i(DEBUG_TAG, "Requesting download for the trips")
+ val requestCountDown = CountDownLatch(tripsList.size);
+ for(trip in tripsList){
+ queriedMatoTrips.add(trip)
+ matoRepository.requestTripUpdate(trip,{error->
+ Log.e(DEBUG_TAG, "Cannot download Gtfs Trip")
+ error.printStackTrace()
+ failedMatoTripsDownload.add(trip)
+ requestCountDown.countDown()
+ }){
+
+ if(it.isSuccess){
+ downloadedMatoTrips.add(it.result!!)
+ } else{
+ failedMatoTripsDownload.add(trip)
+ }
+ Log.i(
+ DEBUG_TAG,"Result download, so far, trips: ${queriedMatoTrips.size}, failed: ${failedMatoTripsDownload.size}," +
+ " succeded: ${downloadedMatoTrips.size}")
+ //check if we can insert the trips
+ requestCountDown.countDown()
+ }
+
+ }
+ requestCountDown.await()
+ val tripsIDsCompleted = downloadedMatoTrips.map { trip-> trip.tripID }
+ val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted)
+ Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert")
+ if(doInsert){
+
+ gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips)
+
+ }
+
+ return Result.success()
+ }
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notificationManager =
+ applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val context = applicationContext
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ Notifications.DB_UPDATE_CHANNELS_ID,
+ context.getString(R.string.database_notification_channel),
+ NotificationManager.IMPORTANCE_MIN
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ val notification = NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID)
+ //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE))
+ .setSmallIcon(R.drawable.bus)
+ .setOngoing(true)
+ .setAutoCancel(true)
+ .setOnlyAlertOnce(true)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setContentTitle(context.getString(R.string.app_name))
+ .setLocalOnly(true)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .setContentText("Downloading data")
+ .build()
+ return ForegroundInfo(NOTIFICATION_ID, notification)
+ }
+
+
+ companion object{
+ const val TRIPS_KEYS = "tripsToDownload"
+ const val WORK_TAG = "tripsDownloaderAndInserter"
+ const val DEBUG_TAG="BusTO:MatoTripDownWRK"
+ const val NOTIFICATION_ID=424242
+ }
+}
Index: app/src/it/reyboz/bustorino/data/MatoRepository.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/data/MatoRepository.kt
@@ -0,0 +1,58 @@
+/*
+ BusTO - Data components
+ Copyright (C) 2023 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.Context
+import android.util.Log
+import com.android.volley.Response
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.backend.Result
+import it.reyboz.bustorino.backend.mato.MatoQueries
+import it.reyboz.bustorino.backend.mato.MatoVolleyJSONRequest
+import it.reyboz.bustorino.backend.mato.ResponseParsing
+import it.reyboz.bustorino.data.gtfs.GtfsTrip
+import org.json.JSONException
+import org.json.JSONObject
+
+class MatoRepository(val mContext: Context) {
+ private val netVolleyManager = NetworkVolleyManager.getInstance(mContext)
+ fun requestTripUpdate(tripId: String, errorListener: Response.ErrorListener?, callback: Callback){
+ val params = JSONObject()
+ params.put("field",tripId)
+ Log.i(DEBUG_TAG, "Requesting info for trip id: $tripId")
+ netVolleyManager.addToRequestQueue(MatoVolleyJSONRequest(
+ MatoQueries.QueryType.TRIP,params,{
+ try {
+ val result = Result.success(ResponseParsing.parseTripInfo(it))
+ callback.onResultAvailable(result)
+ } catch (e: JSONException){
+ // this might happen when the json is "{'data': {'trip': None}}"
+ callback.onResultAvailable(Result.failure(e))
+ }
+ },
+ errorListener
+ ))
+ }
+
+ fun interface Callback {
+ fun onResultAvailable(result: Result)
+ }
+ companion object{
+ final val DEBUG_TAG ="BusTO:MatoRepository"
+ }
+}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/data/gtfs/Converters.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/gtfs/Converters.kt
+++ app/src/it/reyboz/bustorino/data/gtfs/Converters.kt
@@ -78,26 +78,26 @@
}
}
- fun wheelchairFromString(string: String?): GtfsStop.WheelchairAccess?{
+ fun wheelchairFromString(string: String?): WheelchairAccess{
string?.let { if (it.trim() == "1")
- return GtfsStop.WheelchairAccess.SOMETIMES
+ return WheelchairAccess.SOMETIMES
else if(it.trim() == "0")
- return GtfsStop.WheelchairAccess.UNKNOWN
+ return WheelchairAccess.UNKNOWN
else if(it.trim() == "2")
- return GtfsStop.WheelchairAccess.IMPOSSIBLE
+ return WheelchairAccess.IMPOSSIBLE
else //throw Exception("Cannot convert $string to wheelchair access") }
- return GtfsStop.WheelchairAccess.UNKNOWN
+ return WheelchairAccess.UNKNOWN
}
- return null
+ return WheelchairAccess.UNKNOWN
}
@TypeConverter
- fun wheelchairToInt(access:GtfsStop.WheelchairAccess): Int{
+ fun wheelchairToInt(access: WheelchairAccess): Int{
return access.value;
}
@TypeConverter
- fun wheelchairFromInt(value: Int): GtfsStop.WheelchairAccess{
- return GtfsStop.WheelchairAccess.getByValue(value)?: GtfsStop.WheelchairAccess.UNKNOWN
+ fun wheelchairFromInt(value: Int): WheelchairAccess {
+ return WheelchairAccess.getByValue(value)?: WheelchairAccess.UNKNOWN
}
}
}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
+++ app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt
@@ -22,9 +22,14 @@
@Dao
interface GtfsDBDao {
+ // get queries
@Query("SELECT * FROM "+GtfsRoute.DB_TABLE)
fun getAllRoutes() : LiveData>
+ @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_ROUTE_ID} IN (:routeGtfsIds)")
+ fun getRoutesByIDs(routeGtfsIds: List): LiveData>
+
+
@Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE)
fun getAllTripsIDs() : List
@@ -40,12 +45,14 @@
)
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>
+ fun getPatternsLiveDataByRouteID(routeID: String): LiveData>
+
+ @Query("SELECT * FROM "+MatoPattern.TABLE_NAME+" WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeGtfsId")
+ fun getPatternsForRouteID(routeGtfsId: String) : List
@Query("SELECT * FROM ${PatternStop.TABLE_NAME} WHERE ${PatternStop.COL_PATTERN_ID} LIKE :patternGtfsID")
fun getStopsByPatternID(patternGtfsID: String): LiveData>
@@ -54,6 +61,18 @@
@Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID")
fun getPatternsWithStopsByRouteID(routeID: String): LiveData>
+ @Transaction
+ @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_CODE} IN (:patternGtfsIDs)")
+ fun getPatternsWithStopsFromIDs(patternGtfsIDs: List) : LiveData>
+
+ @Transaction
+ @Query("SELECT * FROM ${GtfsTrip.DB_TABLE} WHERE ${GtfsTrip.COL_TRIP_ID} IN (:tripsIds)")
+ fun getTripsFromIDs(tripsIds: List) : List
+
+ @Transaction
+ @Query("SELECT * FROM ${GtfsTrip.DB_TABLE} WHERE ${GtfsTrip.COL_TRIP_ID} IN (:trips)")
+ fun getTripPatternStops(trips: List): LiveData>
+
fun getRoutesForFeed(feed:String): LiveData>{
val agencyID = "${feed}:%"
return getRoutesByAgency(agencyID)
@@ -120,9 +139,6 @@
@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
Index: app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt
+++ app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt
@@ -37,6 +37,9 @@
PatternStop::class
],
version = GtfsDatabase.VERSION,
+ autoMigrations = [
+ AutoMigration(from=2,to=3)
+ ]
)
@TypeConverters(Converters::class)
abstract class GtfsDatabase : RoomDatabase() {
@@ -62,7 +65,7 @@
}
}
- const val VERSION = 2
+ const val VERSION = 3
const val FOREIGNKEY_ONDELETE = ForeignKey.CASCADE
val MIGRATION_1_2 = Migration(1,2) {
Index: app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt
+++ app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt
@@ -50,7 +50,7 @@
valuesByColumn[COL_LATITUDE]?.toDoubleOrNull()!!,
valuesByColumn[COL_LONGITUDE]?.toDoubleOrNull()!!,
//valuesByColumn["zone_id"]?.toIntOrNull()!!,
- Converters.wheelchairFromString(valuesByColumn[COL_WHEELCHAIR])!!
+ Converters.wheelchairFromString(valuesByColumn[COL_WHEELCHAIR])
)
companion object{
const val DB_TABLE="stops_gtfs"
@@ -77,14 +77,4 @@
return COLUMNS
}
- enum class WheelchairAccess(val value: Int){
- UNKNOWN(0),
- SOMETIMES(1),
- IMPOSSIBLE(2);
-
- companion object {
- private val VALUES = values()
- fun getByValue(value: Int) = VALUES.firstOrNull { it.value == value }
- }
- }
}
Index: app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt
+++ app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt
@@ -33,7 +33,7 @@
onDelete = GtfsDatabase.FOREIGNKEY_ONDELETE),
*/
],
- indices = [Index(GtfsTrip.COL_ROUTE_ID)]
+ indices = [Index(GtfsTrip.COL_ROUTE_ID), Index(GtfsTrip.COL_TRIP_ID)]
)
data class GtfsTrip(
@ColumnInfo(name = COL_ROUTE_ID )
@@ -52,10 +52,13 @@
@ColumnInfo(name = COL_SHAPE_ID)
val shapeID: String,
@ColumnInfo(name = COL_WHEELCHAIR)
- val isWheelchairAccess: Boolean,
+ val isWheelchairAccess: WheelchairAccess,
@ColumnInfo(name = COL_LIMITED_R)
val isLimitedRoute: Boolean,
-
+ @ColumnInfo(name= COL_PATTERN_ID, defaultValue = "")
+ val patternId: String,
+ @ColumnInfo(name = COL_SEM_HASH)
+ val semanticHash: String?,
): GtfsTable {
constructor(valuesByColumn: Map) : this(
@@ -66,8 +69,10 @@
valuesByColumn[COL_DIRECTION_ID]?.toIntOrNull()?: 0,
valuesByColumn[COL_BLOCK_ID]!!,
valuesByColumn[COL_SHAPE_ID]!!,
- Converters.fromStringNum(valuesByColumn[COL_WHEELCHAIR], false),
- Converters.fromStringNum(valuesByColumn[COL_LIMITED_R], false)
+ Converters.wheelchairFromString(valuesByColumn[COL_WHEELCHAIR]),
+ Converters.fromStringNum(valuesByColumn[COL_LIMITED_R], false),
+ valuesByColumn[COL_PATTERN_ID]?:"",
+ valuesByColumn[COL_SEM_HASH],
)
companion object{
@@ -82,6 +87,8 @@
const val COL_SHAPE_ID = "shape_id"
const val COL_WHEELCHAIR="wheelchair_accessible"
const val COL_LIMITED_R="limited_route"
+ const val COL_PATTERN_ID="pattern_code"
+ const val COL_SEM_HASH="semantic_hash"
val COLUMNS= arrayOf(
COL_ROUTE_ID,
@@ -104,4 +111,26 @@
override fun getColumns(): Array {
return COLUMNS
}
+
+}
+
+data class TripAndPatternWithStops(
+ @Embedded val trip: GtfsTrip,
+ @Relation(
+ parentColumn = GtfsTrip.COL_PATTERN_ID,
+ entityColumn = MatoPattern.COL_CODE
+ )
+ 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
Index: app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt
===================================================================
--- app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt
+++ app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt
@@ -27,7 +27,8 @@
childColumns = [MatoPattern.COL_ROUTE_ID],
onDelete = ForeignKey.CASCADE,
)
- ]
+ ],
+ indices = [Index(MatoPattern.COL_CODE), Index(MatoPattern.COL_ROUTE_ID)]
)
data class MatoPattern(
@ColumnInfo(name= COL_NAME)
@@ -103,7 +104,8 @@
childColumns = [PatternStop.COL_PATTERN_ID],
onDelete = ForeignKey.CASCADE
)
- ]
+ ],
+ indices = [Index(PatternStop.COL_PATTERN_ID)]
)
data class PatternStop(
@ColumnInfo(name= COL_PATTERN_ID)
Index: app/src/it/reyboz/bustorino/data/gtfs/WheelchairAccess.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/data/gtfs/WheelchairAccess.kt
@@ -0,0 +1,12 @@
+package it.reyboz.bustorino.data.gtfs
+
+enum class WheelchairAccess(val value: Int){
+ UNKNOWN(0),
+ SOMETIMES(1),
+ IMPOSSIBLE(2);
+
+ companion object {
+ private val VALUES = values()
+ fun getByValue(value: Int) = VALUES.firstOrNull { it.value == value }
+ }
+}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/fragments/MapFragment.java
===================================================================
--- app/src/it/reyboz/bustorino/fragments/MapFragment.java
+++ app/src/it/reyboz/bustorino/fragments/MapFragment.java
@@ -19,12 +19,15 @@
package it.reyboz.bustorino.fragments;
import android.Manifest;
+import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationManager;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -38,9 +41,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
+import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
+import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate;
+import it.reyboz.bustorino.backend.gtfs.GtfsUtils;
import it.reyboz.bustorino.backend.utils;
+import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops;
+import it.reyboz.bustorino.map.*;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
@@ -60,11 +68,10 @@
import java.lang.ref.WeakReference;
import java.util.*;
+import kotlin.Pair;
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;
@@ -106,8 +113,16 @@
private Bundle savedMapState = null;
protected ImageButton btCenterMap;
protected ImageButton btFollowMe;
+ private boolean hasMapStartFinished = false;
private boolean followingLocation = false;
+ private MapViewModel mapViewModel ; //= new ViewModelProvider(this).get(MapViewModel.class);
+
+ private final HashMap busPositionMarkersByTrip = new HashMap<>();
+ private FolderOverlay busPositionsOverlay = null;
+
+ private final HashMap tripMarkersAnimators = new HashMap<>();
+
protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() {
@Override
public void onActionUp(@NonNull String stopID, @Nullable String stopName) {
@@ -201,6 +216,13 @@
//setup FolderOverlay
stopsFolderOverlay = new FolderOverlay();
+ //setup Bus Markers Overlay
+ busPositionsOverlay = new FolderOverlay();
+ //reset shown bus updates
+ busPositionMarkersByTrip.clear();
+ tripMarkersAnimators.clear();
+ //set map not done
+ hasMapStartFinished = false;
//Start map from bundle
@@ -254,6 +276,7 @@
public void onAttach(@NonNull Context context) {
super.onAttach(context);
+ mapViewModel = new ViewModelProvider(this).get(MapViewModel.class);
if (context instanceof FragmentListenerMain) {
listenerMain = (FragmentListenerMain) context;
} else {
@@ -265,6 +288,8 @@
public void onDetach() {
super.onDetach();
listenerMain = null;
+ //stop animations
+
// setupOnAttached = true;
Log.w(DEBUG_TAG, "Fragment detached");
}
@@ -274,6 +299,13 @@
super.onPause();
Log.w(DEBUG_TAG, "On pause called mapfrag");
saveMapState();
+ for (ObjectAnimator animator : tripMarkersAnimators.values()) {
+ if(animator!=null && animator.isRunning()){
+ animator.cancel();
+ }
+ }
+ tripMarkersAnimators.clear();
+
if (stopFetcher!= null)
stopFetcher.cancel(true);
}
@@ -309,6 +341,14 @@
public void onResume() {
super.onResume();
if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP);
+ if(mapViewModel!=null) {
+ mapViewModel.requestUpdates();
+ //mapViewModel.testCascade();
+ mapViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> {
+ Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat);
+ mapViewModel.downloadandInsertTripsInDB(dat);
+ });
+ }
}
@Override
@@ -480,7 +520,25 @@
Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true);
map.getController().animateTo(marker);
}
+ //add the overlays with the bus stops
+ if(busPositionsOverlay == null){
+ //Log.i(DEBUG_TAG, "Null bus positions overlay,redo");
+ busPositionsOverlay = new FolderOverlay();
+ }
+
+ if(mapViewModel!=null){
+ //should always be the case
+ mapViewModel.getUpdatesWithTripAndPatterns().observe(this, data->{
+ Log.d(DEBUG_TAG, "Have "+data.size()+" trip updates, has Map start finished: "+hasMapStartFinished);
+ if (hasMapStartFinished) updateBusPositionsInMap(data);
+ if(!isDetached())
+ mapViewModel.requestDelayedUpdates(4000);
+ });
+ }
+ map.getOverlays().add(this.busPositionsOverlay);
+ //set map as started
+ hasMapStartFinished = true;
}
/**
@@ -501,6 +559,104 @@
new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo));
}
+ private void updateBusMarker(final Marker marker,final GtfsPositionUpdate posUpdate,@Nullable boolean justCreated){
+ GeoPoint position;
+ final String updateID = posUpdate.getTripID();
+ if(!justCreated){
+ position = marker.getPosition();
+ if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){
+ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude());
+ ObjectAnimator valueAnimator = MarkerAnimation.makeMarkerAnimator(map, marker, newpos, new GeoPointInterpolator.LinearFixed(), 2500);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ valueAnimator.setAutoCancel(true);
+ } else if(tripMarkersAnimators.containsKey(updateID)) {
+ ObjectAnimator otherAnim = tripMarkersAnimators.get(updateID);
+ assert otherAnim != null;
+ otherAnim.cancel();
+ }
+ tripMarkersAnimators.put(updateID,valueAnimator);
+ valueAnimator.start();
+ }
+ //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()));
+ } else {
+
+ position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude());
+ marker.setPosition(position);
+ }
+
+ marker.setRotation(posUpdate.getBearing()*(-1.f));
+ }
+
+ private void updateBusPositionsInMap(HashMap> tripsPatterns){
+ Log.d(DEBUG_TAG, "Updating positions of the buses");
+ //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
+ for(String tripID: tripsPatterns.keySet()) {
+ final Pair pair = tripsPatterns.get(tripID);
+ if (pair == null) continue;
+ final GtfsPositionUpdate update = pair.getFirst();
+ final TripAndPatternWithStops tripWithPatternStops = pair.getSecond();
+
+
+ //check if Marker is already created
+ if (busPositionMarkersByTrip.containsKey(tripID)){
+ //need to change the position of the marker
+ final Marker marker = busPositionMarkersByTrip.get(tripID);
+ assert marker!=null;
+ updateBusMarker(marker, update, false);
+ if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){
+ BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow();
+ if(tripWithPatternStops != null) {
+ //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID);
+ window.setPatternAndDraw(tripWithPatternStops.getPattern());
+ }
+
+ }
+ } else{
+ //marker is not there, need to make it
+ if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode");
+ final Marker marker = new Marker(map);
+
+ /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources(
+ getResources(),
+ R.drawable.point_heading_icon,
+ R.dimen.map_icons_size, R.dimen.map_icons_size);
+
+ */
+ String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID());
+ final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.point_heading_icon, null);
+ /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(),
+ R.drawable.point_heading_icon,
+ R.color.white,
+ route,12);
+
+ */
+ assert mdraw != null;
+ //mdraw.setBounds(0,0,28,28);
+ marker.setIcon(mdraw);
+ if(tripWithPatternStops == null){
+ Log.d(DEBUG_TAG, "Trip "+tripID+" has no pattern");
+ }
+ marker.setInfoWindow(new BusInfoWindow(map, update, tripWithPatternStops != null ? tripWithPatternStops.getPattern() : null, new BusInfoWindow.onTouchUp() {
+ @Override
+ public void onActionUp() {
+
+ }
+ }));
+
+ updateBusMarker(marker, update, true);
+ // the overlay is null when it's not attached yet?
+ // cannot recreate it because it becomes null very soon
+ // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay();
+ //save the marker
+ if(busPositionsOverlay!=null) {
+ busPositionsOverlay.add(marker);
+ busPositionMarkersByTrip.put(tripID, marker);
+ }
+
+ }
+ }
+ }
+
/**
* Add stops as Markers on the map
* @param stops the list of stops that must be included
@@ -522,7 +678,7 @@
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");
+ Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already");
good = false;
continue;
} else if(map.getRepository() == null){
@@ -584,7 +740,7 @@
// add to it an icon
//marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
- marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_marker, ctx.getTheme()));
+ marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme()));
// add to it a title
marker.setTitle(stopName);
// set the description as the ID
Index: app/src/it/reyboz/bustorino/fragments/MapViewModel.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/fragments/MapViewModel.kt
@@ -0,0 +1,235 @@
+/*
+ BusTO - View Models components
+ Copyright (C) 2023 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.Application
+import android.util.Log
+import androidx.lifecycle.*
+import androidx.work.*
+import com.android.volley.Response
+import com.google.transit.realtime.GtfsRealtime.VehiclePosition
+import it.reyboz.bustorino.backend.NetworkVolleyManager
+import it.reyboz.bustorino.backend.Result
+import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate
+import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest
+import it.reyboz.bustorino.data.*
+import it.reyboz.bustorino.data.gtfs.GtfsTrip
+import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executors
+
+/**
+ * View Model for the map. For containing the stops, the trips and whatever
+ */
+class MapViewModel(application: Application): AndroidViewModel(application) {
+ private val gtfsRepo = GtfsRepository(application)
+ private val executor = Executors.newFixedThreadPool(2)
+
+ private val oldRepo= OldDataRepository(executor, NextGenDB.getInstance(application))
+ private val matoRepository = MatoRepository(application)
+ private val netVolleyManager = NetworkVolleyManager.getInstance(application)
+
+
+ val positionsLiveData = MutableLiveData>()
+ private val positionsRequestRunning = MutableLiveData()
+
+
+ private val positionRequestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{
+ override fun onResponse(response: ArrayList?) {
+ Log.i(DEBUG_TI,"Got response from the GTFS RT server")
+ response?.let {it:ArrayList ->
+ if (it.size == 0) {
+ Log.w(DEBUG_TI,"No position updates from the server")
+ return
+ }
+ else {
+ //Log.i(DEBUG_TI, "Posting value to positionsLiveData")
+ viewModelScope.launch { positionsLiveData.postValue(it) }
+
+ }
+ }
+ //whatever the result, launch again the update TODO
+
+ }
+
+ }
+ private val positionRequestErrorListener = Response.ErrorListener {
+ //error listener, it->VolleyError
+ Log.e(DEBUG_TI, "Could not download the update, error:\n"+it.stackTrace)
+
+ //TODO: launch again if needed
+
+ }
+
+ fun requestUpdates(){
+ if(positionsRequestRunning.value == null || !positionsRequestRunning.value!!) {
+ val request = GtfsRtPositionsRequest(positionRequestErrorListener, positionRequestListener)
+ netVolleyManager.requestQueue.add(request)
+ Log.i(DEBUG_TI, "Requested GTFS realtime position updates")
+ positionsRequestRunning.value = true
+ }
+
+ }
+ /*suspend fun requestDelayedUpdates(timems: Long){
+ delay(timems)
+ requestUpdates()
+ }
+ */
+ fun requestDelayedUpdates(timems: Long){
+ viewModelScope.launch {
+ delay(timems)
+ requestUpdates()
+ }
+ }
+
+ // TRIPS IDS that have to be queried to the DB
+ val tripsIDsInUpdates : LiveData> = positionsLiveData.map {
+
+ Log.i(DEBUG_TI, "positionsLiveData changed")
+ //allow new requests for the positions of buses
+ positionsRequestRunning.value = false
+ //add "gtt:" prefix because it's implicit in GTFS Realtime API
+ return@map it.map { pos -> "gtt:"+pos.tripID }
+ }
+ // trips that are in the DB
+ val gtfsTripsInDB = tripsIDsInUpdates.switchMap {
+ //Log.i(DEBUG_TI, "tripsIds in updates changed: ${it.size}")
+ gtfsRepo.gtfsDao.getTripPatternStops(it)
+ }
+ //trip IDs to query, which are not present in the DB
+ val tripsGtfsIDsToQuery: LiveData> = gtfsTripsInDB.map { tripswithPatterns ->
+ val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID }
+ Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB")
+ if (tripsIDsInUpdates.value!=null)
+ return@map tripsIDsInUpdates.value!!.filter { !tripNames.contains(it) }
+ else {
+ Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??")
+ return@map ArrayList()
+ }
+ }
+
+ val updatesWithTripAndPatterns = gtfsTripsInDB.map { tripPatterns->
+ Log.i(DEBUG_TI, "Mapping trips and patterns")
+ val mdict = HashMap>()
+ if(positionsLiveData.value!=null)
+ for(update in positionsLiveData.value!!){
+ val trID = update.tripID
+ var found = false
+ for(trip in tripPatterns){
+ if (trip.trip.tripID == "gtt:$trID"){
+ found = true
+ //insert directly
+ mdict[trID] = Pair(update,trip)
+ break
+ }
+ }
+ if (!found){
+ //give the update anyway
+ mdict[trID] = Pair(update,null)
+ }
+ }
+ return@map mdict
+ }
+ /*
+ There are two strategies for the queries, since we cannot query a bunch of tripIDs all together
+ to Mato API -> we need to query each separately:
+ 1 -> wait until they are all queried to insert in the DB
+ 2 -> after each request finishes, insert it into the DB
+
+ Keep in mind that trips do not change very often, so they might only need to be inserted once every two months
+ TODO: find a way to avoid trips getting scrubbed (check if they are really scrubbed)
+ */
+
+ init {
+ /*
+ //what happens when the trips to query with Mato are determined
+ tripsIDsQueried.addSource(tripsGtfsIDsToQuery) { tripList ->
+ // avoid infinite loop of querying to Mato, insert in DB and
+ // triggering another query update
+
+ val tripsToQuery =
+ Log.d(DEBUG_TI, "Querying trips IDs to Mato: $tripsToQuery")
+ for (t in tripsToQuery){
+ matoRepository.requestTripUpdate(t,matoTripReqErrorList, matoTripCallback)
+ }
+ tripsIDsQueried.value = tripsToQuery
+ }
+
+ tripsToInsert.addSource(tripsFromMato) { matoTrips ->
+ if (tripsIDsQueried.value == null) return@addSource
+ val tripsIdsToInsert = matoTrips.map { trip -> trip.tripID }
+
+ //val setTripsToInsert = HashSet(tripsIdsToInsert)
+ val tripsRequested = HashSet(tripsIDsQueried.value!!)
+ val insertInDB = tripsRequested.containsAll(tripsIdsToInsert)
+ if(insertInDB){
+ gtfsRepo.gtfsDao.insertTrips(matoTrips)
+ }
+ Log.d(DEBUG_TI, "MatoTrips: ${matoTrips.size}, total trips req: ${tripsRequested.size}, inserting: $insertInDB")
+ }
+ */
+ Log.d(DEBUG_TI, "MapViewModel created")
+ Log.d(DEBUG_TI, "Observers of positionsLiveData ${positionsLiveData.hasActiveObservers()}")
+
+ positionsRequestRunning.value = false;
+ }
+ fun testCascade(){
+ val n = ArrayList()
+ n.add(GtfsPositionUpdate("22920721U","lala","lalal","lol",1000.0f,1000.0f, 9000.0f,
+ 378192810192, GtfsPositionUpdate.VehicleInfo("aj","a"),
+ null, null
+
+ ))
+ positionsLiveData.value = n
+ }
+ private val queriedMatoTrips = HashSet()
+
+ private val downloadedMatoTrips = ArrayList()
+ private val failedMatoTripsDownload = HashSet()
+
+ /**
+ * Start downloading missing GtfsTrips and Insert them in the DB
+ */
+ fun downloadandInsertTripsInDB(trips: List): Boolean{
+ if (trips.isEmpty()) return false
+ val workManager = WorkManager.getInstance(getApplication())
+ val info = workManager.getWorkInfosForUniqueWork(MatoDownloadTripsWorker.WORK_TAG).get()
+
+ val runNewWork = if(info.isEmpty()){
+ true
+ } else info[0].state!=WorkInfo.State.RUNNING && info[0].state!=WorkInfo.State.ENQUEUED
+ Log.d(DEBUG_TI, "Request to download and insert ${trips.size} trips, proceed: $runNewWork")
+ if(runNewWork) {
+ val tripsArr = trips.toTypedArray()
+ val data = Data.Builder().putStringArray(MatoDownloadTripsWorker.TRIPS_KEYS, tripsArr).build()
+ val requ = OneTimeWorkRequest.Builder(MatoDownloadTripsWorker::class.java)
+ .setInputData(data).setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .addTag(MatoDownloadTripsWorker.WORK_TAG)
+ .build()
+ workManager.enqueueUniqueWork(MatoDownloadTripsWorker.WORK_TAG, ExistingWorkPolicy.KEEP, requ)
+ }
+ return true
+ }
+
+ companion object{
+ const val DEBUG_TI="BusTO-MapViewModel"
+ const val DEFAULT_DELAY_REQUESTS: Long=4000
+
+ }
+}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt
===================================================================
--- app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt
+++ app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt
@@ -12,7 +12,8 @@
import com.google.transit.realtime.GtfsRealtime
import it.reyboz.bustorino.R
import it.reyboz.bustorino.backend.NetworkVolleyManager
-import it.reyboz.bustorino.backend.gtfs.GtfsRealtimeRequest
+import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate
+import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
@@ -29,15 +30,17 @@
private lateinit var buttonLaunch: Button
private lateinit var messageTextView: TextView
- private val requestListener = object: GtfsRealtimeRequest.Companion.RequestListener{
- override fun onResponse(response: GtfsRealtime.FeedMessage?) {
+ private val requestListener = object: GtfsRtPositionsRequest.Companion.RequestListener{
+ override fun onResponse(response: ArrayList?) {
if (response == null) return
- if (response.entityCount == 0) {
+ if (response.size == 0) {
messageTextView.text = "No entities in the message"
return
}
- messageTextView.text = "Entity message 0: ${response.getEntity(0)}"
+ val position = response[0]
+ //position.
+ messageTextView.text = "Entity message 0: ${position}"
}
}
@@ -62,7 +65,7 @@
buttonLaunch.setOnClickListener {
context?.let {cont->
- val req = GtfsRealtimeRequest(GtfsRealtimeRequest.URL_POSITION,
+ val req = GtfsRtPositionsRequest(
Response.ErrorListener { Toast.makeText(cont, "Error: ${it.message}",Toast.LENGTH_SHORT) },
requestListener
)
Index: app/src/it/reyboz/bustorino/map/BusInfoWindow.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/map/BusInfoWindow.kt
@@ -0,0 +1,65 @@
+package it.reyboz.bustorino.map
+
+import android.annotation.SuppressLint
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.*
+import android.widget.TextView
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.backend.gtfs.GtfsPositionUpdate
+import it.reyboz.bustorino.backend.gtfs.GtfsUtils
+import it.reyboz.bustorino.data.gtfs.GtfsTrip
+import it.reyboz.bustorino.data.gtfs.MatoPattern
+import org.osmdroid.views.MapView
+import org.osmdroid.views.overlay.infowindow.BasicInfoWindow
+
+@SuppressLint("ClickableViewAccessibility")
+class BusInfoWindow(map: MapView,
+ val update: GtfsPositionUpdate,
+ var pattern: MatoPattern?,
+ private val touchUp: onTouchUp):
+ BasicInfoWindow(R.layout.bus_info_window,map) {
+
+ init {
+ mView.setOnTouchListener { view, motionEvent ->
+ touchUp.onActionUp()
+ close()
+ //mView.performClick()
+ true
+
+ }
+ }
+
+ override fun onOpen(item: Any?) {
+ // super.onOpen(item)
+ val titleView = mView.findViewById(R.id.businfo_title)
+ val descrView = mView.findViewById(R.id.businfo_description)
+ val subdescrView = mView.findViewById(R.id.businfo_subdescription)
+
+ val nameRoute = GtfsUtils.getLineNameFromGtfsID(update.routeID)
+ titleView.text = (mView.resources.getString(R.string.line_fill, nameRoute)
+ )
+ subdescrView.text = update.vehicleInfo.label
+
+
+ if(pattern!=null){
+ descrView.text = pattern!!.headsign
+ descrView.visibility = VISIBLE
+ } else{
+ descrView.visibility = GONE
+ }
+ }
+ fun setPatternAndDraw(pattern: MatoPattern?){
+ if(pattern==null){
+ return
+ }
+ this.pattern = pattern
+ if(isOpen){
+ onOpen(pattern)
+ }
+ }
+
+ fun interface onTouchUp{
+ fun onActionUp()
+ }
+}
\ No newline at end of file
Index: app/src/it/reyboz/bustorino/map/GeoPointInterpolator.java
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/map/GeoPointInterpolator.java
@@ -0,0 +1,85 @@
+package it.reyboz.bustorino.map;
+
+/* Copyright 2013 Google Inc.
+ Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0.html */
+
+import org.osmdroid.util.GeoPoint;
+
+import static java.lang.Math.asin;
+import static java.lang.Math.atan2;
+import static java.lang.Math.cos;
+import static java.lang.Math.pow;
+import static java.lang.Math.sin;
+import static java.lang.Math.sqrt;
+import static java.lang.Math.toDegrees;
+import static java.lang.Math.toRadians;
+
+public interface GeoPointInterpolator {
+ public GeoPoint interpolate(float fraction, GeoPoint a, GeoPoint b);
+
+ public class Linear implements GeoPointInterpolator {
+ @Override
+ public GeoPoint interpolate(float fraction, GeoPoint a, GeoPoint b) {
+ double lat = (b.getLatitude() - a.getLatitude()) * fraction + a.getLatitude();
+ double lng = (b.getLongitude() - a.getLongitude()) * fraction + a.getLongitude();
+ return new GeoPoint(lat, lng);
+ }
+ }
+
+ public class LinearFixed implements GeoPointInterpolator {
+ @Override
+ public GeoPoint interpolate(float fraction, GeoPoint a, GeoPoint b) {
+ double lat = (b.getLatitude() - a.getLatitude()) * fraction + a.getLatitude();
+ double lngDelta = b.getLongitude() - a.getLongitude();
+
+ // Take the shortest path across the 180th meridian.
+ if (Math.abs(lngDelta) > 180) {
+ lngDelta -= Math.signum(lngDelta) * 360;
+ }
+ double lng = lngDelta * fraction + a.getLongitude();
+ return new GeoPoint(lat, lng);
+ }
+ }
+
+ public class Spherical implements GeoPointInterpolator {
+
+ /* From github.com/googlemaps/android-maps-utils */
+ @Override
+ public GeoPoint interpolate(float fraction, GeoPoint from, GeoPoint to) {
+ // http://en.wikipedia.org/wiki/Slerp
+ double fromLat = toRadians(from.getLatitude());
+ double fromLng = toRadians(from.getLongitude());
+ double toLat = toRadians(to.getLatitude());
+ double toLng = toRadians(to.getLongitude());
+ double cosFromLat = cos(fromLat);
+ double cosToLat = cos(toLat);
+
+ // Computes Spherical interpolation coefficients.
+ double angle = computeAngleBetween(fromLat, fromLng, toLat, toLng);
+ double sinAngle = sin(angle);
+ if (sinAngle < 1E-6) {
+ return from;
+ }
+ double a = sin((1 - fraction) * angle) / sinAngle;
+ double b = sin(fraction * angle) / sinAngle;
+
+ // Converts from polar to vector and interpolate.
+ double x = a * cosFromLat * cos(fromLng) + b * cosToLat * cos(toLng);
+ double y = a * cosFromLat * sin(fromLng) + b * cosToLat * sin(toLng);
+ double z = a * sin(fromLat) + b * sin(toLat);
+
+ // Converts interpolated vector back to polar.
+ double lat = atan2(z, sqrt(x * x + y * y));
+ double lng = atan2(y, x);
+ return new GeoPoint(toDegrees(lat), toDegrees(lng));
+ }
+
+ private double computeAngleBetween(double fromLat, double fromLng, double toLat, double toLng) {
+ // Haversine's formula
+ double dLat = fromLat - toLat;
+ double dLng = fromLng - toLng;
+ return 2 * asin(sqrt(pow(sin(dLat / 2), 2) +
+ cos(fromLat) * cos(toLat) * pow(sin(dLng / 2), 2)));
+ }
+ }
+}
Index: app/src/it/reyboz/bustorino/map/MarkerAnimation.java
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/map/MarkerAnimation.java
@@ -0,0 +1,31 @@
+package it.reyboz.bustorino.map;
+
+/* Copyright 2013 Google Inc.
+ Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0.html */
+
+
+ import android.animation.ObjectAnimator;
+ import android.animation.TypeEvaluator;
+ import android.util.Property;
+
+ import org.osmdroid.util.GeoPoint;
+ import org.osmdroid.views.MapView;
+ import org.osmdroid.views.overlay.Marker;
+
+public class MarkerAnimation {
+
+
+ public static ObjectAnimator makeMarkerAnimator(final MapView map, Marker marker, GeoPoint finalPosition, final GeoPointInterpolator GeoPointInterpolator, int durationMs) {
+ TypeEvaluator typeEvaluator = new TypeEvaluator() {
+ @Override
+ public GeoPoint evaluate(float fraction, GeoPoint startValue, GeoPoint endValue) {
+ return GeoPointInterpolator.interpolate(fraction, startValue, endValue);
+ }
+ };
+ Property property = Property.of(Marker.class, GeoPoint.class, "position");
+ ObjectAnimator animator = ObjectAnimator.ofObject(marker, property, typeEvaluator, finalPosition);
+ animator.setDuration(durationMs);
+ //animator.start();
+ return animator;
+ }
+}
Index: app/src/it/reyboz/bustorino/util/DrawableUtils.kt
===================================================================
--- /dev/null
+++ app/src/it/reyboz/bustorino/util/DrawableUtils.kt
@@ -0,0 +1,52 @@
+package it.reyboz.bustorino.util
+
+import android.content.res.Resources
+import android.graphics.*
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.core.content.res.ResourcesCompat
+import kotlin.math.abs
+
+
+class DrawableUtils {
+ companion object {
+ fun getScaledDrawableResources(
+ resources: Resources, @DrawableRes id: Int, @DimenRes width: Int, @DimenRes height: Int): Drawable {
+ val w = resources.getDimension(width).toInt()
+ val h = resources.getDimension(height).toInt()
+ return scaledDrawable(resources,id, w, h)
+ }
+
+ fun scaledDrawable(resources: Resources, @DrawableRes id: Int, width: Int, height: Int): Drawable {
+ val bmp = BitmapFactory.decodeResource(resources, id)
+ val bmpScaled = Bitmap.createScaledBitmap(bmp, width, height, false)
+ return BitmapDrawable(resources, bmpScaled)
+ }
+ /*fun writeOnDrawable(resources: Resources, drawableId: Int, colorId: Int, text: String?, textSize: Float, theme: Resources.Theme?): BitmapDrawable? {
+ //val bm = BitmapFactory.decodeResource(resources, drawableId).copy(Bitmap.Config.ARGB_8888, true)
+ val paint = Paint()
+ paint.style = Paint.Style.FILL
+ paint.color = colorId
+ paint.textSize = textSize
+ paint.textAlign = Paint.Align.CENTER
+
+ //get the drawable instead of bitmap
+ val drawable = ResourcesCompat.getDrawable(resources,drawableId,theme)
+ if (drawable==null){
+ Log.e("BusTO:DrawableRes","DRAWABLE IS NULL")
+ return null
+ }
+ Log.d("BusTO:DrawableRes","getResources drawable ${drawable.bounds}")
+ val canvas = Canvas()
+ drawable.draw(canvas)
+ val bounds = drawable.bounds
+ canvas.drawText(text!!, (abs(bounds.right - bounds.left)/2).toFloat(), (abs(bounds.top - bounds.bottom) /2).toFloat(), paint)
+ return BitmapDrawable(resources,bm)
+ }
+
+ */
+ }
+}
\ No newline at end of file
Index: app/src/test/java/it/reyboz/bustorino/util/GTFSRouteNameTest.java
===================================================================
--- /dev/null
+++ app/src/test/java/it/reyboz/bustorino/util/GTFSRouteNameTest.java
@@ -0,0 +1,29 @@
+package it.reyboz.bustorino.util;
+
+import androidx.core.util.Pair;
+import it.reyboz.bustorino.backend.ServiceType;
+import it.reyboz.bustorino.backend.gtfs.GtfsUtils;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class GTFSRouteNameTest {
+
+ @Test
+ public void testName1(){
+ final String test="gtt:4108E";
+ assertEquals(GtfsUtils.getRouteInfoFromGTFS(test), new Pair<>(ServiceType.EXTRAURBANO,"4108"));
+ }
+
+ @Test
+ public void testName2(){
+ final String test="gtt:63BU";
+ assertEquals(GtfsUtils.getRouteInfoFromGTFS(test), new Pair<>(ServiceType.URBANO,"63B"));
+ }
+
+ @Test
+ public void testName3(){
+ final String test="63BU";
+ assertEquals(GtfsUtils.getRouteInfoFromGTFS(test), new Pair<>(ServiceType.URBANO,"63B"));
+ }
+}
Index: build.gradle
===================================================================
--- build.gradle
+++ build.gradle
@@ -2,7 +2,7 @@
buildscript {
repositories {
- jcenter()
+ mavenCentral()
maven { url 'https://maven.google.com' }
google()
@@ -38,10 +38,10 @@
allprojects {
repositories {
- jcenter()
maven { url 'https://maven.google.com' }
google()
mavenCentral()
+ //maven { url "https://jitpack.io" }
}
}