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,82 @@ +/* + BusTO - Map 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.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" } } }