diff --git a/app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/3.json b/app/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/3.json new file mode 100644 index 0000000..ae5b5ee --- /dev/null +++ b/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 diff --git a/app/build.gradle b/app/build.gradle index afeedb0..2e83e6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,131 +1,132 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 buildToolsVersion '33.0.2' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 16 targetSdkVersion 33 versionCode 46 versionName "1.18.4" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } } lintOptions { abortOnError false } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" 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' + // remember to enable maven repo jitpack.io when wanting to use osmbonuspack + //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" } diff --git a/app/res/drawable/bus_stop.xml b/app/res/drawable/bus_stop.xml new file mode 100644 index 0000000..db67e72 --- /dev/null +++ b/app/res/drawable/bus_stop.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/res/drawable/point_heading_icon.xml b/app/res/drawable/point_heading_icon.xml new file mode 100644 index 0000000..1b9fb58 --- /dev/null +++ b/app/res/drawable/point_heading_icon.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/res/layout/bus_info_window.xml b/app/res/layout/bus_info_window.xml new file mode 100644 index 0000000..ce00e42 --- /dev/null +++ b/app/res/layout/bus_info_window.xml @@ -0,0 +1,64 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/res/layout/map_popup.xml b/app/res/layout/map_popup.xml index edc183e..4358133 100644 --- a/app/res/layout/map_popup.xml +++ b/app/res/layout/map_popup.xml @@ -1,46 +1,46 @@ \ No newline at end of file diff --git a/app/res/layout/stop_line_element.xml b/app/res/layout/stop_line_element.xml index 1a7042e..533ab8d 100644 --- a/app/res/layout/stop_line_element.xml +++ b/app/res/layout/stop_line_element.xml @@ -1,100 +1,104 @@ \ No newline at end of file diff --git a/app/res/values-it/strings.xml b/app/res/values-it/strings.xml index d82904f..01a93c8 100644 --- a/app/res/values-it/strings.xml +++ b/app/res/values-it/strings.xml @@ -1,205 +1,206 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Si No Installare Barcode Scanner? Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner? Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linea Linee Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code trovato, prova ad usare un\'altra app Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

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

Perché usare BusTO?

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

Come Funziona?

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

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

Licenze

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

Note

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

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione in corso… Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti Entra nel canale Telegram La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Fonti orari di arrivo Scegli le fonti di orari da usare Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Canale unico delle notifiche Database Informazioni sul database (aggiornamento) + Downloading trips from MaTO server Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti Schermata da mostrare all\'avvio Tocca per cambiare Tocca a lungo la fermata per le opzioni
diff --git a/app/res/values/colors.xml b/app/res/values/colors.xml index 502ca96..9602d1e 100644 --- a/app/res/values/colors.xml +++ b/app/res/values/colors.xml @@ -1,30 +1,33 @@ #ff9800 #F57C00 + #cc6600 + #994d00 #2196F3 #2a65e8 #2060dd #2378e8 #0079f5 #2a968b #0067ff #009688 #4DB6AC #80cbc4 #F5F5F5 #f8f8f8 #757575 #DE0908 + #b30000 #2060DD #FFFFFF #000000 #1c1c1c @color/blue_mid_2 \ No newline at end of file diff --git a/app/res/values/strings.xml b/app/res/values/strings.xml index 1b8361d..6c5c1c3 100644 --- a/app/res/values/strings.xml +++ b/app/res/values/strings.xml @@ -1,240 +1,241 @@ BusTO Libre BusTO BusTO dev You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Lines: %1$s Line: %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times Welcome!

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

Why use this app?

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

How does it work?

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

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

Licenses

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

Notes

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

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

Get involved! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recents is invalid Invalid value, put a valid number Finding the position… No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to position to show it on the map Please enable GPS Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Center on my location Follow me Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default 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! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Launching database update Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Hello blank fragment
diff --git a/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java b/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java index 3cbb073..494be13 100644 --- a/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java +++ b/app/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java @@ -1,53 +1,54 @@ package it.reyboz.bustorino.data.gtfs; import androidx.room.Room; import androidx.room.migration.Migration; import androidx.room.testing.MigrationTestHelper; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; //@RunWith(AndroidJUnit4.class) public class GtfsDBMigrationsTest { private static final String TEST_DB = "migration-test"; @Rule public MigrationTestHelper helper; public GtfsDBMigrationsTest() { helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), GtfsDatabase.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); } @Test public void migrateAll() throws IOException { // Create earliest version of the database. SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); db.close(); // Open latest version of the database. Room will validate the schema // once all migrations execute. GtfsDatabase appDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().getTargetContext(), GtfsDatabase.class, TEST_DB) .addMigrations(ALL_MIGRATIONS).build(); appDb.getOpenHelper().getWritableDatabase(); appDb.close(); } // Array of all migrations private static final Migration[] ALL_MIGRATIONS = new Migration[]{ - GtfsDatabase.Companion.getMIGRATION_1_2()}; + GtfsDatabase.Companion.getMIGRATION_1_2(), + }; } diff --git a/app/src/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/it/reyboz/bustorino/ActivityPrincipal.java index 4a34d68..94ebd3e 100644 --- a/app/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,731 +1,748 @@ /* BusTO - Arrival times for Turin public transport. Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; private boolean onCreateComplete = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); final SharedPreferences theShPr = getMainSharedPreferences(); boolean showingArrivalsFromIntent = false; //database check GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); boolean dataUpdateRequested = false; final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr); Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version); if (old_version < db_version){ //decide update conditions in the future if(old_version < 2 && db_version >= 2) { dataUpdateRequested = true; DatabaseUpdate.requestDBUpdateWithWork(this, true, true); } PreferencesHolder.setGtfsDBVersion(theShPr, db_version); } Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); requestArrivalsForStopID(busStopID); //this shows the fragment, too showingArrivalsFromIntent = true; } //Try (hopefully) database update if(!dataUpdateRequested) DatabaseUpdate.requestDBUpdateWithWork(this, false, false); /* Watch for database update */ final WorkManager workManager = WorkManager.getInstance(this); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); //if (vl.length() == 0 || vl.equals("arrivals")) { // showMainFragment(); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); if (showingArrivalsFromIntent){ //do nothing but exclude a case }else if (savedInstanceState==null) { //we are not restarting the activity from nothing if (vl.equals("map")) { requestMapFragment(false); } else if (vl.equals("favorites")) { checkAndShowFavoritesFragment(getSupportFragmentManager(), false); } else if (vl.equals("lines")) { showLinesFragment(getSupportFragmentManager(), false, null); } else { showMainFragment(false); } } onCreateComplete = true; //last but not least, set the good default values manageDefaultValuesForSettings(); } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //get Fragment checkAndShowFavoritesFragment(getSupportFragmentManager(), true); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(true); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); requestMapFragment(true); return true; } else if (menuItem.getItemId() == R.id.nav_lines_item) { closeDrawerIfOpen(); showLinesFragment(getSupportFragmentManager(), true,null); return true; } else if(menuItem.getItemId() == R.id.drawer_action_info) { closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; + if (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)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { boolean foundFragment = false; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } shownFrag.getChildFragmentManager().popBackStack(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main back stack also"); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main frame backstack for fragments"); } else super.onBackPressed(); } /** * Create and show the SnackBar with the message */ private void createDefaultSnackbar() { View baseView = null; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param arguments args for the fragment */ private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, MainScreenFragment.class, arguments, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } 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); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> createAndShowMapFragment(null, allowReturn)); break; case PERMISSION_NEG_CANNOT_ASK: String storage_perm = getString(R.string.storage_permission); String text = getString(R.string.too_many_permission_asks, storage_perm); Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); } } private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES); if(fragment!=null){ ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES); }else{ //use new method ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES); } if (addToBackStack) ft.addToBackStack("favorites_main"); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setReorderingAllowed(false); ft.commit(); } private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment f = fragmentManager.findFragmentByTag(LinesFragment.FRAGMENT_TAG); if(f!=null){ ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG); }else{ //use new method ft.replace(R.id.mainActContentFrame,LinesFragment.class,fragArgs,LinesFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack("lines"); ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } private void showMainFragment(boolean addToBackStack){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ createShowMainFragment(fraMan, null, addToBackStack); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment, addToBackStack); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } //return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); if (mainFragmentIfVisible!=null){ mainFragmentIfVisible.readyGUIfor(fragmentType); } int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_arrivals); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; default: titleResId = 0; } if(getSupportActionBar()!=null && titleResId!=0) getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG: "+fragment); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment, true); probableFragment.requestArrivalsForStopID(ID); } else { // we have no fragment final Bundle args = new Bundle(); args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID); //if onCreate is complete, then we are not asking for the first showing fragment boolean addtobackstack = onCreateComplete; createShowMainFragment(fraMan, args ,addtobackstack); } } else { //the MainScreeFragment is shown, nothing to do probableFragment.requestArrivalsForStopID(ID); } mNavView.setCheckedItem(R.id.nav_arrivals); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_about: startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; case R.id.action_hack: openIceweasel(getString(R.string.hack_url), activityContext); return true; case R.id.action_source: openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; case R.id.action_experiments: startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); default: } return false; } } /** * Adjust setting to match the default ones */ private void manageDefaultValuesForSettings(){ SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = mainSharedPref.edit(); //Main fragment to show String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); boolean edit = false; if (screen.isEmpty()){ editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals"); edit=true; } //Fetchers final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>()); if (setSelected.isEmpty()){ String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default); editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals)); edit=true; } if (edit){ editor.commit(); } } } diff --git a/app/src/it/reyboz/bustorino/backend/Palina.java b/app/src/it/reyboz/bustorino/backend/Palina.java index 50b8c1b..902f00e 100644 --- a/app/src/it/reyboz/bustorino/backend/Palina.java +++ b/app/src/it/reyboz/bustorino/backend/Palina.java @@ -1,380 +1,404 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.List; import it.reyboz.bustorino.util.LinesNameSorter; /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop { private ArrayList routes = new ArrayList<>(); private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), null); } public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { super(ID, name, userName, location, null, null, lat, lon, gtfsID); } public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere) { super(name, ID, location, type, routesThatStopHere); } /** * Adds a timetable entry to a route. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6))); } public int addRoute(Route r){ this.routes.add(r); routesModified = true; buildRoutesString(); return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } @Nullable @Override protected String buildRoutesString() { // no routes => no string if(routes == null || routes.size() == 0) { return ""; } - final StringBuilder sb = new StringBuilder(); + /*final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim())); int i, lenMinusOne = routes.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routes.get(i).getName().trim()).append(", "); } // last one: sb.append(routes.get(i).getName()); - - setRoutesThatStopHereString(sb.toString()); + */ + ArrayList names = new ArrayList<>(); + for (Route r: routes){ + names.add(r.getName()); + } + final String routesThatStopHere = buildRoutesStringFromNames(names); + setRoutesThatStopHereString(routesThatStopHere); return routesThatStopHereToString(); } + /** + * Sort the names of the routes for the string "routes stopping here" and make the string + * @param names of the Routes that pass in the stop + * @return the full string of routes stopping (eg, "10, 13, 42" ecc) + */ + public static String buildRoutesStringFromNames(List names){ + final StringBuilder sb = new StringBuilder(); + final LinesNameSorter nameSorter = new LinesNameSorter(); + Collections.sort(names, nameSorter); + int i, lenMinusOne = names.size() - 1; + + for (i = 0; i < lenMinusOne; i++) { + sb.append(names.get(i).trim()).append(", "); + } + //last one + sb.append(names.get(i).trim()); + return sb.toString(); + } + protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); buildRoutesString(); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } if (count> 0) buildRoutesString(); return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Adds a time (passaggio) to the timetable for this route // * // * @param TimeGTT time in GTT format (e.g. "11:22*") // */ // public void addPassaggio(String TimeGTT) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i0) min = Math.min(min,r.numPassaggi()); } if (min == Integer.MAX_VALUE) return 0; else return min; } //private void mergeRoute } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/backend/ServiceType.java b/app/src/it/reyboz/bustorino/backend/ServiceType.java new file mode 100644 index 0000000..9a84afe --- /dev/null +++ b/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, +} diff --git a/app/src/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt b/app/src/it/reyboz/bustorino/backend/gtfs/GtfsPositionUpdate.kt new file mode 100644 index 0000000..2936912 --- /dev/null +++ b/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 + ) +} + + diff --git a/app/src/it/reyboz/bustorino/backend/gtfs/GtfsRealtimeRequest.kt b/app/src/it/reyboz/bustorino/backend/gtfs/GtfsRealtimeRequest.kt deleted file mode 100644 index 9c77406..0000000 --- a/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 diff --git a/app/src/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt b/app/src/it/reyboz/bustorino/backend/gtfs/GtfsRtPositionsRequest.kt new file mode 100644 index 0000000..f2e0bf2 --- /dev/null +++ b/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 diff --git a/app/src/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java b/app/src/it/reyboz/bustorino/backend/gtfs/GtfsUtils.java new file mode 100644 index 0000000..f76349a --- /dev/null +++ b/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; + } +} diff --git a/app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index ab97832..32acc65 100644 --- a/app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/app/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,430 +1,434 @@ /* BusTO - Backend components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.mato import android.content.Context import android.util.Log import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* import it.reyboz.bustorino.data.gtfs.GtfsAgency import it.reyboz.bustorino.data.gtfs.GtfsFeed import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPattern import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList -open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher { +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 numMinutes += 20 val future = RequestFuture.newFuture() val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) try { val palinaResult = future.get(5, TimeUnit.SECONDS) if (palinaResult!=null) { /*if (BuildConfig.DEBUG) for (r in palinaResult.queryAllRoutes()){ Log.d(DEBUG_TAG, "route " + r.gtfsId + " has " + r.passaggi.size + " passaggi: "+ r.passaggiToString) }*/ palina = palinaResult numPassaggi = palina.minNumberOfPassages } else{ Log.d(DEBUG_TAG, "Result palina is null") } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() if (res.get() == Fetcher.Result.OK) - res.set(Fetcher.Result.SERVER_ERROR) + res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" + const val DEF_MIN_NUMPASSAGGI=2 val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "DNT" to "1", "Host" to "mapi.5t.torino.it") private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) fun getVolleyReqTag(type: MatoQueries.QueryType): String{ return when (type){ MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds" MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes" MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute" + MatoQueries.QueryType.TRIP -> VOLLEY_TAG+"_Trip" } } /** * Get stops from the MatoAPI, set [res] accordingly */ fun getAllStopsGTT(context: Context, res: AtomicReference?): List{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture>() val request = VolleyAllStopsRequest(future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS) request.retryPolicy = longRetryPolicy requestQueue.add(request) var palinaList:List = mutableListOf() try { palinaList = future.get(120, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } return palinaList } /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), null, null, latitude, longitude, jsonStop.getString("gtfsId") ) val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf() // get all the possible routes for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.setGtfsId(routeBaseInfo.getString("gtfsId").trim()) baseRoutes.add(r) } if (jsonStop.has("desc")){ palina.location = jsonStop.getString("desc") } //there is also "zoneId" which is the zone of the stop (0-> city, etc) if(jsonStop.has("stoptimesForPatterns")) { val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") for (i in 0 until routesStopTimes.length()) { val patternJSON = routesStopTimes.getJSONObject(i) val mRoute = parseRouteStoptimesJSON(patternJSON) //Log.d("BusTO-MapiFetcher") //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") //TODO: use directionId palina.addRoute(mRoute) for (r in baseRoutes) { if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) { baseRoutes.remove(r) break } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() return palina } fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( routeJSON.getString("shortName"), patternJSON.getString("headsign"), routeType, passages, ) route.setGtfsId(gtfsId) return route } fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ val data = JSONObject() data.put("operationName", requestName) data.put("variables", variables) data.put("query", query) return data } fun getFeedsAndAgencies(context: Context, res: AtomicReference?): Pair, ArrayList> { val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future) request.setRetryPolicy(longRetryPolicy) request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS) requestQueue.add(request) val feeds = ArrayList() val agencies = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val feedsJSON = resObj.getJSONArray("feeds") for (i in 0 until feedsJSON.length()){ val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i)) feeds.add(resTup.first) agencies.addAll(resTup.second) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return Pair(feeds,agencies) } fun getRoutes(context: Context, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() params.put("feeds","gtt") val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future) request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES) request.retryPolicy = longRetryPolicy requestQueue.add(request) val routes = ArrayList() var outObj = "" try { val resObj = future.get(120,TimeUnit.SECONDS) outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i)) routes.add(route) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } return routes } - fun getPatternsWithStops(context: Context, routesGTFSIds: ArrayList, res: AtomicReference?): ArrayList{ + fun getPatternsWithStops(context: Context, routesGTFSIds: MutableCollection, res: AtomicReference?): ArrayList{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture() val params = JSONObject() for (r in routesGTFSIds){ if(r.isEmpty()) routesGTFSIds.remove(r) } val routes = JSONArray(routesGTFSIds) params.put("routes",routes) val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future) request.retryPolicy = longRetryPolicy request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES) requestQueue.add(request) val patterns = ArrayList() //var outObj = "" try { val resObj = future.get(60,TimeUnit.SECONDS) //outObj = resObj.toString(1) val routesJSON = resObj.getJSONArray("routes") for (i in 0 until routesJSON.length()){ val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i)) patterns.addAll(patternList) } } catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } catch (e: JSONException){ e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) //Log.e(DEBUG_TAG, "Downloading feeds: $outObj") } /* var numRequests = 0 for(routeName in routesGTFSIds){ if (!routeName.isEmpty()) numRequests++ } val countDownForRequests = CountDownLatch(numRequests) val lockSave = ReentrantLock() //val countDownFor for (routeName in routesGTFSIds){ val pars = JSONObject() pars.put("") } val goodResponseListener = Response.Listener { } val errorResponseListener = Response.ErrorListener { } */ return patterns } } } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt b/app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt index b7fbac2..66d8f00 100644 --- a/app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt +++ b/app/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt @@ -1,166 +1,189 @@ /* BusTO - Backend components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.mato class MatoQueries { companion object{ const val QUERY_ARRIVALS="""query AllStopsDirect( ${'$'}name: String ${'$'}startTime: Long ${'$'}timeRange: Int ${'$'}numberOfDepartures: Int ) { stops(name: ${'$'}name) { __typename lat lon gtfsId code name desc wheelchairBoarding routes { __typename gtfsId shortName } stoptimesForPatterns( startTime: ${'$'}startTime timeRange: ${'$'}timeRange numberOfDepartures: ${'$'}numberOfDepartures ) { __typename pattern { __typename headsign directionId route { __typename gtfsId shortName mode } } stoptimes { __typename scheduledArrival realtimeArrival realtime realtimeState } } } } """ const val ALL_STOPS_BY_FEEDS=""" query AllStops(${'$'}feeds: [String!]){ stops(feeds: ${'$'}feeds) { lat lon gtfsId code name desc routes { gtfsId shortName } } } """ const val ALL_FEEDS=""" query AllFeeds{ feeds{ feedId agencies{ gtfsId name url fareUrl phone } } } """ const val ROUTES_BY_FEED=""" query AllRoutes(${'$'}feeds: [String]){ routes(feeds: ${'$'}feeds) { agency{ gtfsId } gtfsId shortName longName type desc color textColor } } """ const val ROUTES_WITH_PATTERNS=""" query RoutesWithPatterns(${'$'}routes: [String]) { routes(ids: ${'$'}routes) { gtfsId shortName longName type patterns{ name code semanticHash directionId headsign stops{ gtfsId lat lon } patternGeometry{ length points } } } } """ + 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){ QueryType.FEEDS -> Pair("AllFeeds", ALL_FEEDS) QueryType.ALL_STOPS -> Pair("AllStops", ALL_STOPS_BY_FEEDS) QueryType.ARRIVALS -> Pair("AllStopsDirect", QUERY_ARRIVALS) QueryType.ROUTES -> Pair("AllRoutes", ROUTES_BY_FEED) QueryType.PATTERNS_FOR_ROUTES -> Pair("RoutesWithPatterns", ROUTES_WITH_PATTERNS) + 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 diff --git a/app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt b/app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt index ecf79b2..b5ea626 100644 --- a/app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt +++ b/app/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt @@ -1,48 +1,54 @@ package it.reyboz.bustorino.backend.mato import android.util.Log import com.android.volley.NetworkResponse import com.android.volley.Response import com.android.volley.VolleyError import com.android.volley.toolbox.HttpHeaderParser import org.json.JSONException import org.json.JSONObject class MatoVolleyJSONRequest(type: MatoQueries.QueryType, val variables: JSONObject, listener: Response.Listener, errorListener: Response.ErrorListener?) : MapiVolleyRequest(type, listener, errorListener) { protected val requestName:String protected val requestQuery:String init { val dd = MatoQueries.getNameAndRequest(type) requestName = dd.first requestQuery = dd.second } override fun getBody(): ByteArray { val data = MatoAPIFetcher.makeRequestParameters(requestName, variables, requestQuery) return data.toString().toByteArray() } override fun parseNetworkResponse(response: NetworkResponse?): Response { if (response==null) return Response.error(VolleyError("Null response")) else if(response.statusCode != 200) return Response.error(VolleyError("Response not ready, status "+response.statusCode)) val obj:JSONObject try { - obj = JSONObject(String(response.data)).getJSONObject("data") + 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() return Response.error(VolleyError("Error parsing JSON")) } return Response.success(obj, HttpHeaderParser.parseCacheHeaders(response)) } } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt b/app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt index ff0e563..7c7f80c 100644 --- a/app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt +++ b/app/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt @@ -1,120 +1,153 @@ /* BusTO - Backend components Copyright (C) 2022 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.mato +import 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"), jsonObject.getString("name"), jsonObject.getString("url"), jsonObject.getString("fareUrl"), jsonObject.getString("phone"), null ) } /** * Parse a feed request json, containing the GTFS agencies it is served by */ fun parseFeedJSON(jsonObject: JSONObject): Pair> { val agencies = ArrayList() val feed = GtfsFeed(jsonObject.getString("feedId")) val oo = jsonObject.getJSONArray("agencies") agencies.ensureCapacity(oo.length()) for (i in 0 until oo.length()){ val agObj = oo.getJSONObject(i) agencies.add( GtfsAgency( agObj.getString("gtfsId"), agObj.getString("name"), agObj.getString("url"), agObj.getString("fareUrl"), agObj.getString("phone"), feed ) ) } return Pair(feed, agencies) } fun parseRouteJSON(jsonObject: JSONObject): GtfsRoute { val agencyJSON = jsonObject.getJSONObject("agency") val agencyId = agencyJSON.getString("gtfsId") return GtfsRoute( jsonObject.getString("gtfsId"), agencyId, jsonObject.getString("shortName"), jsonObject.getString("longName"), jsonObject.getString("desc"), GtfsMode.getByValue(jsonObject.getInt("type"))!!, jsonObject.getString("color"), jsonObject.getString("textColor") ) } /** * Parse a route pattern from the JSON response of the MaTO server */ fun parseRoutePatternsStopsJSON(jsonObject: JSONObject) : ArrayList{ val routeGtfsId = jsonObject.getString("gtfsId") val patternsJSON = jsonObject.getJSONArray("patterns") val patternsOut = ArrayList(patternsJSON.length()) var mPatternJSON: JSONObject for(i in 0 until patternsJSON.length()){ mPatternJSON = patternsJSON.getJSONObject(i) val stopsJSON = mPatternJSON.getJSONArray("stops") val stopsCodes = ArrayList(stopsJSON.length()) for(k in 0 until stopsJSON.length()){ stopsCodes.add( stopsJSON.getJSONObject(k).getString("gtfsId") ) } val geometry = mPatternJSON.getJSONObject("patternGeometry") val numGeo = geometry.getInt("length") val polyline = geometry.getString("points") patternsOut.add( MatoPattern( mPatternJSON.getString("name"), mPatternJSON.getString("code"), mPatternJSON.getString("semanticHash"), mPatternJSON.getInt("directionId"), routeGtfsId,mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes ) ) } return patternsOut } + 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 diff --git a/app/src/it/reyboz/bustorino/backend/utils.java b/app/src/it/reyboz/bustorino/backend/utils.java index b11a2da..19d28dd 100644 --- a/app/src/it/reyboz/bustorino/backend/utils.java +++ b/app/src/it/reyboz/bustorino/backend/utils.java @@ -1,341 +1,355 @@ /* BusTO (backend components) Copyright (C) 2019 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.fragments.SettingsFragment; public abstract class utils { private static final double EarthRadius = 6371e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/EarthRadius); } /* public static int convertDipToPixels(Context con,float dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } */ public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } final static Pattern ROMAN_PATTERN = Pattern.compile( "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); private static boolean isRomanNumber(String str){ if(str.isEmpty()) return false; final Matcher matcher = ROMAN_PATTERN.matcher(str); return matcher.find(); } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (String s : arr) { if (s.length() > 0) { String[] allsubs = s.split("\\."); boolean addPoint = s.contains("."); /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli { DOESN'T ALWAYS WORK addPoint = false; allsubs = new String[]{s}; }*/ boolean first = true; for (String subs : allsubs) { if(first) first=false; else { if (addPoint) sb.append("."); sb.append(" "); } if(isRomanNumber(subs)){ //add and skip the rest sb.append(subs); continue; } //SPLIT ON ', check if contains "D'" if(subs.toLowerCase(Locale.ROOT).startsWith("d'")){ sb.append("D'"); subs = subs.substring(2); } int index = 0; char c = subs.charAt(index); if(subs.length() > 1 && c=='('){ sb.append(c); index += 1; c = subs.charAt(index); } sb.append(Character.toUpperCase(c)); if (lowercaseRest) sb.append(subs.substring(index+1).toLowerCase(Locale.ROOT)); else sb.append(subs.substring(index+1)); } if(addPoint && allsubs.length == 1) sb.append('.'); sb.append(" "); /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); */ } else sb.append(s); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static List getDefaultArrivalsFetchers(Context context){ SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); final Set setSelected = new HashSet<>(defSharPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>())); if (setSelected.isEmpty()) { return Arrays.asList(new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()); }else{ ArrayList outFetchers = new ArrayList<>(4); /*for(String s: setSelected){ switch (s){ case "matofetcher": outFetchers.add(new MatoAPIFetcher()); break; case "fivetapifetcher": outFetchers.add(new FiveTAPIFetcher()); break; case "gttjsonfetcher": outFetchers.add(new GTTJSONFetcher()); break; case "fivetscraper": outFetchers.add(new FiveTScraperFetcher()); break; default: throw new IllegalArgumentException(); } }*/ if (setSelected.contains("matofetcher")) { outFetchers.add(new MatoAPIFetcher()); setSelected.remove("matofetcher"); } if (setSelected.contains("fivetapifetcher")) { outFetchers.add(new FiveTAPIFetcher()); setSelected.remove("fivetapifetcher"); } if (setSelected.contains("gttjsonfetcher")){ outFetchers.add(new GTTJSONFetcher()); setSelected.remove("gttjsonfetcher"); } if (setSelected.contains("fivetscraper")) { outFetchers.add(new FiveTScraperFetcher()); setSelected.remove("fivetscraper"); } if(!setSelected.isEmpty()){ Log.e("BusTO-Utils","Getting some fetchers values which are not contemplated: "+setSelected); } 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 */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i Set convertArrayToSet(T[] array) { // Create an empty Set Set set = new HashSet<>(); // Add each element into the set set.addAll(Arrays.asList(array)); // Return the converted Set return set; } public static String giveClassesForArray(T[] array){ StringBuilder sb = new StringBuilder(); for (T f: array){ sb.append(""); sb.append(f.getClass().getSimpleName()); sb.append("; "); } return sb.toString(); } } diff --git a/app/src/it/reyboz/bustorino/data/DatabaseUpdate.java b/app/src/it/reyboz/bustorino/data/DatabaseUpdate.java index 514a110..4d27ce2 100644 --- a/app/src/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/app/src/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -1,303 +1,303 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.gtfs.GtfsAgency; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.data.gtfs.GtfsFeed; import it.reyboz.bustorino.data.gtfs.GtfsRoute; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.PatternStop; import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DatabaseUpdate { public static final String DEBUG_TAG = "BusTO-DBUpdate"; public static final int VERSION_UNAVAILABLE = -2; public static final int JSON_PARSING_ERROR = -4; public static final String DB_VERSION_KEY = "NextGenDB.GTTVersion"; public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; enum Result { DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD } /** * Request the server the version of the database * @return the version of the DB, or an error code */ public static int getNewVersion(){ AtomicReference gres = new AtomicReference<>(); String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); if(networkRequest == null){ return VERSION_UNAVAILABLE; } try { JSONObject resp = new JSONObject(networkRequest); return resp.getInt("id"); } catch (JSONException e) { e.printStackTrace(); Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); return JSON_PARSING_ERROR; } } private static boolean updateGTFSAgencies(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( con, res ); dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); return true; } - private static boolean updateGTFSRoutes(Context con, AtomicReference res){ + private static HashMap> updateGTFSRoutes(Context con, AtomicReference res){ final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); - final List routes= MatoAPIFetcher.Companion.getRoutes( - con, res - ); + final List routes= MatoAPIFetcher.Companion.getRoutes(con, res); + + final HashMap> routesStoppingInStop = new HashMap<>(); + dao.insertRoutes(routes); if(res.get()!= Fetcher.Result.OK){ - return false; + return routesStoppingInStop; } final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); + final HashMap routesMap = new HashMap<>(routes.size()); for(GtfsRoute r: routes){ gtfsRoutesIDs.add(r.getGtfsId()); + routesMap.put(r.getGtfsId(),r); } long t0 = System.currentTimeMillis(); final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); long tend = System.currentTimeMillis() - t0; Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); if(res.get()!=Fetcher.Result.OK){ Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); - return false; + return routesStoppingInStop; } + //match patterns with routes final ArrayList patternStops = new ArrayList<>(patterns.size()); + for(MatoPattern p: patterns){ + //scan patterns final ArrayList stopsIDs = p.getStopsGtfsIDs(); + final GtfsRoute mRoute = routesMap.get(p.getRouteGtfsId()); + if (mRoute == null) { + Log.e(DEBUG_TAG, "Error in parsing the route: " + p.getRouteGtfsId() + " , cannot find the IDs in the map"); + } for (int i=0; i()); + } + Set mset= routesStoppingInStop.get(ID); + assert mset != null; + mset.add(mRoute.getShortName()); } } dao.insertPatterns(patterns); dao.insertPatternStops(patternStops); - return true; + return routesStoppingInStop; } /** * Run the DB Update * @param con a context * @param gres a result reference * @return result of the update */ public static Result performDBUpdate(Context con, AtomicReference gres) { final FiveTAPIFetcher f = new FiveTAPIFetcher(); final NextGenDB dbHelp = NextGenDB.getInstance(con.getApplicationContext()); final SQLiteDatabase db = dbHelp.getWritableDatabase(); final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); if (gres.get() != Fetcher.Result.OK) { Log.w(DEBUG_TAG, "Something went wrong downloading"); return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; } + + // GTFS data fetching + AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); + updateGTFSAgencies(con, gtfsRes); + if (gtfsRes.get()!= Fetcher.Result.OK){ + Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); + } else{ + Log.d(DEBUG_TAG, "Done downloading agencies"); + } + gtfsRes.set(Fetcher.Result.OK); + final HashMap> routesStoppingByStop = updateGTFSRoutes(con,gtfsRes); + if (gtfsRes.get()!= Fetcher.Result.OK){ + Log.w(DEBUG_TAG, "Could not insert the routes into DB"); + } else{ + Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); + } + + /*db.beginTransaction(); + startTime = System.currentTimeMillis(); + int countStop = NextGenDB.writeLinesStoppingHere(db, routesStoppingByStop); + if(countStop!= routesStoppingByStop.size()){ + Log.w(DEBUG_TAG, "Something went wrong in updating the linesStoppingBy, have "+countStop+" lines updated, with " + +routesStoppingByStop.size()+" stops to update"); + } + db.setTransactionSuccessful(); + db.endTransaction(); + endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Updating lines took "+(endTime-startTime)+" ms"); + */ //TODO: Get the type of stop from the lines //Empty the needed tables db.beginTransaction(); //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); //db.delete(LinesTable.TABLE_NAME,null,null); //put new data long startTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); + String routesStoppingString=""; + int patternsStopsHits = 0; for (final Palina p : palinasMatoAPI) { final ContentValues cv = new ContentValues(); cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); if (p.location != null) cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); - cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, p.routesThatStopHereToString()); + if(p.gtfsID!= null && routesStoppingByStop.containsKey(p.gtfsID)){ + final ArrayList routesSs= new ArrayList<>(routesStoppingByStop.get(p.gtfsID)); + routesStoppingString = Palina.buildRoutesStringFromNames(routesSs); + patternsStopsHits++; + } else{ + routesStoppingString = p.routesThatStopHereToString(); + } + cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, routesStoppingString); if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); //Log.d(DEBUG_TAG,cv.toString()); //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); //valuesArr[i] = cv; db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); } db.setTransactionSuccessful(); db.endTransaction(); long endTime = System.currentTimeMillis(); Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); - - // GTFS data fetching - AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); - updateGTFSAgencies(con, gtfsRes); - if (gtfsRes.get()!= Fetcher.Result.OK){ - Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); - } else{ - Log.d(DEBUG_TAG, "Done downloading agencies"); - } - gtfsRes.set(Fetcher.Result.OK); - updateGTFSRoutes(con,gtfsRes); - if (gtfsRes.get()!= Fetcher.Result.OK){ - Log.w(DEBUG_TAG, "Could not insert the routes into DB"); - } else{ - Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); - } - /* - final ArrayList routes = f.getAllLinesFromGTT(gres); - - if (routes == null) { - Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); - dbHelp.close(); - return DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD; - - } - - db.beginTransaction(); - startTime = System.currentTimeMillis(); - for (Route r : routes) { - final ContentValues cv = new ContentValues(); - cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName()); - switch (r.type) { - case BUS: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO"); - break; - case RAILWAY: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA"); - break; - case LONG_DISTANCE_BUS: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA"); - break; - } - cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description); - - //db.insert(LinesTable.TABLE_NAME,null,cv); - int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); - if (rows < 1) { //we haven't changed anything - db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - endTime = System.currentTimeMillis(); - Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); - - */ + Log.d(DEBUG_TAG, "\t"+patternsStopsHits+" routes string were built from the patterns"); dbHelp.close(); return DatabaseUpdate.Result.DONE; } public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); return setDBUpdatingFlag(con, shPr, value); } static boolean setDBUpdatingFlag(Context con, SharedPreferences shPr,boolean value){ final SharedPreferences.Editor editor = shPr.edit(); editor.putBoolean(con.getString(R.string.databaseUpdatingPref),value); return editor.commit(); } /** * Request update using workmanager framework * @param con the context to use * @param forced if you want to force the request to go now */ public static void requestDBUpdateWithWork(Context con,boolean restart, boolean forced){ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); final Data reqData = new Data.Builder() .putBoolean(DBUpdateWorker.FORCED_UPDATE, forced).build(); PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .setInputData(reqData) .build(); final int version = theShPr.getInt(DatabaseUpdate.DB_VERSION_KEY, -10); final long lastDBUpdateTime = theShPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, -10); if ((version >= 0 || lastDBUpdateTime >=0) && !restart) workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.KEEP, wr); else workManager.enqueueUniquePeriodicWork(DBUpdateWorker.DEBUG_TAG, ExistingPeriodicWorkPolicy.REPLACE, wr); } /* public static boolean isDBUpdating(){ return false; TODO } */ public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner, @NonNull Observer> observer) { WorkManager workManager = WorkManager.getInstance(context); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe( lifecycleOwner, observer ); } } diff --git a/app/src/it/reyboz/bustorino/data/GtfsRepository.kt b/app/src/it/reyboz/bustorino/data/GtfsRepository.kt index 98867d2..d3c310c 100644 --- a/app/src/it/reyboz/bustorino/data/GtfsRepository.kt +++ b/app/src/it/reyboz/bustorino/data/GtfsRepository.kt @@ -1,36 +1,34 @@ 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) //} } fun getPatternsForRouteID(routeID: String): LiveData>{ return if(routeID.isNotEmpty()) - gtfsDao.getPatternsByRouteID(routeID) + gtfsDao.getPatternsLiveDataByRouteID(routeID) else MutableLiveData(listOf()) } /** * Get the patterns with the stops lists (gtfsIDs only) */ fun getPatternsWithStopsForRouteID(routeID: String): LiveData>{ return if(routeID.isNotEmpty()) gtfsDao.getPatternsWithStopsByRouteID(routeID) else MutableLiveData(listOf()) } } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt b/app/src/it/reyboz/bustorino/data/MatoDownloadTripsWorker.kt new file mode 100644 index 0000000..da00808 --- /dev/null +++ b/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 + } +} diff --git a/app/src/it/reyboz/bustorino/data/MatoRepository.kt b/app/src/it/reyboz/bustorino/data/MatoRepository.kt new file mode 100644 index 0000000..e398ee6 --- /dev/null +++ b/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 diff --git a/app/src/it/reyboz/bustorino/data/NextGenDB.java b/app/src/it/reyboz/bustorino/data/NextGenDB.java index 511bdfa..ad6ebb6 100644 --- a/app/src/it/reyboz/bustorino/data/NextGenDB.java +++ b/app/src/it/reyboz/bustorino/data/NextGenDB.java @@ -1,461 +1,491 @@ /* BusTO (middleware) Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.util.Log; +import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import java.util.*; +import java.util.stream.Collectors; import static it.reyboz.bustorino.data.NextGenDB.Contract.*; public class NextGenDB extends SQLiteOpenHelper{ public static final String DATABASE_NAME = "bustodatabase.db"; public static final int DATABASE_VERSION = 3; public static final String DEBUG_TAG = "NextGenDB-BusTO"; //NO Singleton instance //private static volatile NextGenDB instance = null; //Some generating Strings private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+ Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+ Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+ "UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )"; private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+ Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+ Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+ Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+ //SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+ //DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+ BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+ "FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") " +")"; private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+ Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+ Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+ "PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+ "FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") " +")"; private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ StopsTable.COL_GTFS_ID+" TEXT, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; public static final String[] QUERY_COLUMN_stops_all = { StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_GTFS_ID, StopsTable.COL_LOCATION, StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING}; public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " + StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG + " >= ? AND "+ StopsTable.COL_LONG + " <= ?"; public static final String QUERY_FROM_GTFS_ID_IN_TO_COMPLETE= StopsTable.COL_GTFS_ID +" IN "; public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?"; private final Context appContext; private static NextGenDB INSTANCE; private NextGenDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); appContext = context.getApplicationContext(); } public static NextGenDB getInstance(Context context) { if (INSTANCE == null){ INSTANCE = new NextGenDB(context); } return INSTANCE; } @Override public void onCreate(SQLiteDatabase db) { Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+ SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE); db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion<2 && newVersion == 2){ //DROP ALL TABLES db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME); db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME); db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME); db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME); //RECREATE THE TABLES WITH THE NEW SCHEMA db.execSQL(SQL_CREATE_LINES_TABLE); db.execSQL(SQL_CREATE_STOPS_TABLE); //tables with constraints db.execSQL(SQL_CREATE_BRANCH_TABLE); db.execSQL(SQL_CREATE_CONNECTIONS_TABLE); DatabaseUpdate.requestDBUpdateWithWork(appContext, true, true); } if(oldVersion < 3 && newVersion == 3){ Log.d("BusTO-Database", "Running upgrades for version 3"); //add the new column db.execSQL("ALTER TABLE "+StopsTable.TABLE_NAME+ " ADD COLUMN "+StopsTable.COL_GTFS_ID+" TEXT "); // DatabaseUpdate.requestDBUpdateWithWork(appContext, true); } } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL("PRAGMA foreign_keys=ON"); } public static String getSqlCreateStopsTable(String tableName){ return "CREATE TABLE "+tableName+" ("+ Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+ Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+ Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+ Contract.StopsTable.COL_LINES_STOPPING +" TEXT )"; } /** * Query some bus stops inside a map view * * You can obtain the coordinates from OSMDroid using something like this: * BoundingBoxE6 bb = mMapView.getBoundingBox(); * double latFrom = bb.getLatSouthE6() / 1E6; * double latTo = bb.getLatNorthE6() / 1E6; * double lngFrom = bb.getLonWestE6() / 1E6; * double lngTo = bb.getLonEastE6() / 1E6; */ public synchronized ArrayList queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) { ArrayList stops = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); // coordinates must be strings in the where condition String minLatRaw = String.valueOf(minLat); String maxLatRaw = String.valueOf(maxLat); String minLngRaw = String.valueOf(minLng); String maxLngRaw = String.valueOf(maxLng); if(db == null) { return stops; } try { final Cursor result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE, new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw}, null, null, null); stops = getStopsFromCursorAllFields(result); result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); e.printStackTrace(); return stops; }finally { db.close(); } return stops; } /** * Query stops in the database having these IDs * REMEMBER TO CLOSE THE DB CONNECTION AFTERWARDS * @param bustoDB readable database instance * @param gtfsIDs gtfs IDs to query * @return list of stops */ public static synchronized ArrayList queryAllStopsWithGtfsIDs(SQLiteDatabase bustoDB, List gtfsIDs){ final ArrayList stops = new ArrayList<>(); if(bustoDB == null){ Log.e(DEBUG_TAG, "Asked query for IDs but database is null"); return stops; } else if (gtfsIDs == null || gtfsIDs.isEmpty()) { return stops; } final StringBuilder builder = new StringBuilder(QUERY_FROM_GTFS_ID_IN_TO_COMPLETE); boolean first = true; builder.append(" ( "); for(int i=0; i< gtfsIDs.size(); i++){ if(first){ first = false; } else{ builder.append(", "); } builder.append("?");//.append("\"").append(id).append("\""); } builder.append(") "); final String whereClause = builder.toString(); final String[] idsQuery = gtfsIDs.toArray(new String[0]); try { final Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause, idsQuery, null, null, null); stops.addAll(getStopsFromCursorAllFields(result)); result.close(); } catch(SQLiteException e) { Log.e(DEBUG_TAG, "SQLiteException occurred"); e.printStackTrace(); } return stops; } /** * Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all} * @param result cursor from query * @return an Array of the stops found in the query */ public static ArrayList getStopsFromCursorAllFields(Cursor result){ final int colID = result.getColumnIndex(StopsTable.COL_ID); final int colName = result.getColumnIndex(StopsTable.COL_NAME); final int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION); final int colType = result.getColumnIndex(StopsTable.COL_TYPE); final int colLat = result.getColumnIndex(StopsTable.COL_LAT); final int colGtfsID = result.getColumnIndex(StopsTable.COL_GTFS_ID); final int colLon = result.getColumnIndex(StopsTable.COL_LONG); final int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING); int count = result.getCount(); ArrayList stops = new ArrayList<>(count); int i = 0; while(result.moveToNext()) { final String stopID = result.getString(colID).trim(); Route.Type type; //if(result.getString(colType) == null) type = Route.Type.BUS; //else type = Route.getTypeFromSymbol(result.getString(colType)); //if(result.getInt(colType) == null) type = Route.Type.BUS; try{ type = Route.Type.fromCode(result.getInt(colType)); } catch (Exception e){ type = Route.Type.BUS; } String lines = result.getString(colLines).trim(); String locationSometimesEmpty = result.getString(colLocation); if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) { locationSometimesEmpty = null; } stops.add(new Stop(stopID, result.getString(colName), null, locationSometimesEmpty, type, splitLinesString(lines), result.getDouble(colLat), result.getDouble(colLon), result.getString(colGtfsID)) ); } return stops; } + + public static synchronized int writeLinesStoppingHere(SQLiteDatabase db, HashMap> linesStoppingBy){ + int rowsUpdated = 0; + for (String stopGtfsID : linesStoppingBy.keySet()){ + if (linesStoppingBy.get(stopGtfsID)==null) continue; + if (linesStoppingBy.get(stopGtfsID).isEmpty()) continue; + ArrayList ll = new ArrayList<>(linesStoppingBy.get(stopGtfsID)); + String stringForStops = Palina.buildRoutesStringFromNames(ll); + + ContentValues cv = new ContentValues(); + cv.put(StopsTable.COL_LINES_STOPPING, stringForStops); + + // Which row to update, based on the title + String selection = StopsTable.COL_GTFS_ID + " LIKE ?"; + String[] selectionArgs = { stopGtfsID }; + + int count = db.update( + StopsTable.TABLE_NAME, + cv, + selection, + selectionArgs); + if (count > 1){ + Log.e(DEBUG_TAG, "Updated the linesStoppingBy for more than one stop"); + } + rowsUpdated += count; + } + return rowsUpdated; + } /* static ArrayList createStopListFromCursor(Cursor data){ ArrayList stopList = new ArrayList<>(); final int col_id = data.getColumnIndex(StopsTable.COL_ID); final int latInd = data.getColumnIndex(StopsTable.COL_LAT); final int lonInd = data.getColumnIndex(StopsTable.COL_LONG); final int nameindex = data.getColumnIndex(StopsTable.COL_NAME); final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE); final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING); data.moveToFirst(); for(int i=0; i stops){ return 0; } public static List splitLinesString(String linesStr){ return Arrays.asList(linesStr.split("\\s*,\\s*")); } public static final class Contract{ //Ok, I get it, it really is a pain in the ass.. // But it's the only way to have maintainable code public interface DataTables { String getTableName(); String[] getFields(); } public static final class LinesTable implements BaseColumns, DataTables { //The fields public static final String TABLE_NAME = "lines"; public static final String COLUMN_NAME = "line_name"; public static final String COLUMN_DESCRIPTION = "line_description"; public static final String COLUMN_TYPE = "line_bacino"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE}; } } public static final class BranchesTable implements BaseColumns, DataTables { public static final String TABLE_NAME = "branches"; public static final String COL_BRANCHID = "branchid"; public static final String COL_LINE = "lineid"; public static final String COL_DESCRIPTION = "branch_description"; public static final String COL_DIRECTION = "branch_direzione"; public static final String COL_FESTIVO = "branch_festivo"; public static final String COL_TYPE = "branch_type"; public static final String COL_LUN="runs_lun"; public static final String COL_MAR="runs_mar"; public static final String COL_MER="runs_mer"; public static final String COL_GIO="runs_gio"; public static final String COL_VEN="runs_ven"; public static final String COL_SAB="runs_sab"; public static final String COL_DOM="runs_dom"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION, COL_DIRECTION,COL_FESTIVO,COL_TYPE, COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM }; } } public static final class ConnectionsTable implements DataTables { public static final String TABLE_NAME = "connections"; public static final String COLUMN_BRANCH = "branchid"; public static final String COLUMN_STOP_ID = "stopid"; public static final String COLUMN_ORDER = "ordine"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER}; } } public static final class StopsTable implements DataTables { public static final String TABLE_NAME = "stops"; public static final String COL_ID = "stopid"; //integer public static final String COL_TYPE = "stop_type"; public static final String COL_NAME = "stop_name"; public static final String COL_GTFS_ID = "gtfs_id"; public static final String COL_LAT = "stop_latitude"; public static final String COL_LONG = "stop_longitude"; public static final String COL_LOCATION = "stop_location"; public static final String COL_PLACE = "stop_placeName"; public static final String COL_LINES_STOPPING = "stop_lines"; @Override public String getTableName() { return TABLE_NAME; } @Override public String[] getFields() { return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_GTFS_ID,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING}; } } } public static final class DBUpdatingException extends Exception{ public DBUpdatingException(String message) { super(message); } } } diff --git a/app/src/it/reyboz/bustorino/data/gtfs/Converters.kt b/app/src/it/reyboz/bustorino/data/gtfs/Converters.kt index 1440c25..243d887 100644 --- a/app/src/it/reyboz/bustorino/data/gtfs/Converters.kt +++ b/app/src/it/reyboz/bustorino/data/gtfs/Converters.kt @@ -1,103 +1,103 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data.gtfs import androidx.room.TypeConverter import java.text.SimpleDateFormat import java.util.* /** * Class to convert values for objects into * the needed columns * * handled automatically by Room with TypeConverter */ class Converters { @TypeConverter fun fromString(value: String?): Date? { return dateFromFmtString(value) } @TypeConverter fun dateToString(date: Date?): String? { return date?.let { stringFormat.format(it)} } @TypeConverter fun exceptionToInt(type: GtfsServiceDate.ExceptionType?): Int? { return type?.value } @TypeConverter fun fromInt(value: Int?): GtfsServiceDate.ExceptionType? { return value?.let { GtfsServiceDate.ExceptionType.getByValue(it) } } companion object{ const val DATE_FMT_STRING = "yyyyMMdd" val stringFormat = SimpleDateFormat(DATE_FMT_STRING, Locale.US) fun fromStringNum(string: String?): Boolean?{ string?.let { if (it.trim() == "1") return true else if(it.trim() == "0") return false else throw Exception("Cannot convert $string to numeric value") } return null } fun fromStringNum(string: String?, defaultVal: Boolean): Boolean{ string?.let { if (it.trim() == "1") return true else if(it.trim() == "0") return false else return defaultVal } return defaultVal } fun dateFromFmtString(value: String?): Date?{ return value?.let { stringFormat.parse(it) } } - 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 diff --git a/app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt b/app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt index a54be35..81d991d 100644 --- a/app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt +++ b/app/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt @@ -1,128 +1,144 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data.gtfs import androidx.lifecycle.LiveData import androidx.room.* @Dao interface 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 @Query("SELECT "+GtfsStop.COL_STOP_ID+" FROM "+GtfsStop.DB_TABLE) fun getAllStopsIDs() : List @Query("SELECT * FROM "+GtfsStop.DB_TABLE+" WHERE "+GtfsStop.COL_STOP_CODE+" LIKE :queryID") fun getStopByStopID(queryID: String): LiveData> @Query("SELECT * FROM "+GtfsShape.DB_TABLE+ " WHERE "+GtfsShape.COL_SHAPE_ID+" LIKE :shapeID"+ " ORDER BY "+GtfsShape.COL_POINT_SEQ+ " ASC" ) fun getShapeByID(shapeID: String) : LiveData> - @Query("SELECT * FROM ${GtfsRoute.DB_TABLE} WHERE ${GtfsRoute.COL_AGENCY_ID} LIKE :agencyID") fun getRoutesByAgency(agencyID:String) : LiveData> @Query("SELECT * FROM ${MatoPattern.TABLE_NAME} WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeID") - fun getPatternsByRouteID(routeID: String): LiveData> + 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> @Transaction @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) } @Transaction fun clearAndInsertRoutes(routes: List){ deleteAllRoutes() insertRoutes(routes) } @Transaction @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertRoutes(routes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStops(stops: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertCalendarServices(services: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertShapes(shapes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertDates(dates: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertServices(services: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertTrips(trips: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStopTimes(stopTimes: List) @Query("DELETE FROM "+GtfsRoute.DB_TABLE) fun deleteAllRoutes() @Query("DELETE FROM "+GtfsStop.DB_TABLE) fun deleteAllStops() @Query("DELETE FROM "+GtfsTrip.DB_TABLE) fun deleteAllTrips() @Update(onConflict = OnConflictStrategy.REPLACE) fun updateShapes(shapes: List) : Int @Transaction fun updateAllStops(stops: List){ deleteAllStops() insertStops(stops) } @Query("DELETE FROM "+GtfsStopTime.DB_TABLE) fun deleteAllStopTimes() @Query("DELETE FROM "+GtfsService.DB_TABLE) fun deleteAllServices() @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertFeeds(feeds: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAgencies(agencies: List) @Transaction fun insertAgenciesWithFeeds(feeds: List, agencies: List){ insertFeeds(feeds) insertAgencies(agencies) } //patterns @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertPatterns(patterns: List) - @Query("SELECT * FROM "+MatoPattern.TABLE_NAME+ - " WHERE ${MatoPattern.COL_ROUTE_ID} LIKE :routeGtfsId") - fun getPatternsForRouteID(routeGtfsId: String) : List @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertPatternStops(patternStops: List) } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt b/app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt index f02402a..f564428 100644 --- a/app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt +++ b/app/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt @@ -1,87 +1,90 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data.gtfs import android.content.Context import android.util.Log import androidx.room.* import androidx.room.migration.Migration @Database( entities = [ GtfsFeed::class, GtfsAgency::class, GtfsServiceDate::class, GtfsStop::class, GtfsService::class, GtfsRoute::class, GtfsStopTime::class, GtfsTrip::class, GtfsShape::class, MatoPattern::class, PatternStop::class ], version = GtfsDatabase.VERSION, + autoMigrations = [ + AutoMigration(from=2,to=3) + ] ) @TypeConverters(Converters::class) abstract class GtfsDatabase : RoomDatabase() { abstract fun gtfsDao() : GtfsDBDao companion object{ @Volatile private var INSTANCE: GtfsDatabase? =null const val DB_NAME="gtfs_database" fun getGtfsDatabase(context: Context): GtfsDatabase{ return INSTANCE ?: synchronized(this){ val instance = Room.databaseBuilder(context.applicationContext, GtfsDatabase::class.java, DB_NAME) .addMigrations(MIGRATION_1_2) .build() INSTANCE = instance instance } } - const val VERSION = 2 + const val VERSION = 3 const val FOREIGNKEY_ONDELETE = ForeignKey.CASCADE val MIGRATION_1_2 = Migration(1,2) { Log.d("BusTO-Database", "Upgrading from version 1 to version 2 the Room Database") //create table for feeds it.execSQL("CREATE TABLE IF NOT EXISTS `gtfs_feeds` (`feed_id` TEXT NOT NULL, PRIMARY KEY(`feed_id`))") //create table gtfs_agencies it.execSQL("CREATE TABLE IF NOT EXISTS `gtfs_agencies` (`gtfs_id` TEXT NOT NULL, `ag_name` TEXT NOT NULL, `ag_url` TEXT NOT NULL, `fare_url` TEXT, `phone` TEXT, `feed_id` TEXT, PRIMARY KEY(`gtfs_id`))") //recreate routes it.execSQL("DROP TABLE IF EXISTS `routes_table`") it.execSQL("CREATE TABLE IF NOT EXISTS `routes_table` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_mode` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, PRIMARY KEY(`route_id`))") //create patterns and stops it.execSQL("CREATE TABLE IF NOT EXISTS `mato_patterns` (`pattern_name` TEXT NOT NULL, `pattern_code` TEXT NOT NULL, `pattern_hash` TEXT NOT NULL, `pattern_direction_id` INTEGER NOT NULL, `pattern_route_id` TEXT NOT NULL, `pattern_headsign` TEXT, `pattern_polyline` TEXT NOT NULL, `pattern_polylength` INTEGER NOT NULL, PRIMARY KEY(`pattern_code`), FOREIGN KEY(`pattern_route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )") it.execSQL("CREATE TABLE IF NOT EXISTS `patterns_stops` (`pattern_gtfs_id` TEXT NOT NULL, `stop_gtfs_id` TEXT NOT NULL, `stop_order` INTEGER NOT NULL, PRIMARY KEY(`pattern_gtfs_id`, `stop_gtfs_id`, `stop_order`), FOREIGN KEY(`pattern_gtfs_id`) REFERENCES `mato_patterns`(`pattern_code`) ON UPDATE NO ACTION ON DELETE CASCADE )") } } } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt b/app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt index 11164fb..c19f696 100644 --- a/app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt +++ b/app/src/it/reyboz/bustorino/data/gtfs/GtfsStop.kt @@ -1,90 +1,80 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data.gtfs import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = GtfsStop.DB_TABLE) data class GtfsStop( @PrimaryKey @ColumnInfo(name= COL_STOP_ID) val internalID: Int, @ColumnInfo(name= COL_STOP_CODE) val gttStopID: String, @ColumnInfo(name= COL_STOP_NAME) val stopName: String, @ColumnInfo(name= COL_GTT_PLACE) val gttPlaceName: String, @ColumnInfo(name= COL_LATITUDE) val latitude: Double, @ColumnInfo(name= COL_LONGITUDE) val longitude: Double, //@ColumnInfo(name="zone_id") //val zoneID: Int, @ColumnInfo(name= COL_WHEELCHAIR) val wheelchair: WheelchairAccess, ): GtfsTable { constructor(valuesByColumn: Map) : this( valuesByColumn[COL_STOP_ID]?.toIntOrNull()!!, valuesByColumn[COL_STOP_CODE]!!, valuesByColumn[COL_STOP_NAME]!!, valuesByColumn[COL_GTT_PLACE]!!, 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" const val COL_STOP_CODE="stop_code" const val COL_STOP_ID = "stop_id" const val COL_GTT_PLACE="stop_desc" const val COL_STOP_NAME="stop_name" const val COL_LATITUDE="stop_lat" const val COL_LONGITUDE="stop_lon" const val COL_WHEELCHAIR="wheelchair_boarding" val COLUMNS = arrayOf( COL_STOP_CODE, COL_STOP_ID, COL_GTT_PLACE, COL_STOP_NAME, COL_LATITUDE, COL_LONGITUDE, //"zone_id", COL_WHEELCHAIR ) } override fun getColumns(): Array { 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 } - } - } } diff --git a/app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt b/app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt index daf58eb..4c2b51c 100644 --- a/app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt +++ b/app/src/it/reyboz/bustorino/data/gtfs/GtfsTrip.kt @@ -1,107 +1,136 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data.gtfs import androidx.room.* @Entity(tableName = GtfsTrip.DB_TABLE, foreignKeys=[ ForeignKey(entity = GtfsRoute::class, parentColumns = [GtfsRoute.COL_ROUTE_ID], childColumns = [GtfsTrip.COL_ROUTE_ID], onDelete = ForeignKey.CASCADE), // The service_id: ID referencing calendar.service_id or calendar_dates.service_id /* ForeignKey(entity = GtfsService::class, parentColumns = [GtfsService.COL_SERVICE_ID], childColumns = [GtfsTrips.COL_SERVICE_ID], 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 ) val routeID: String, @ColumnInfo(name = COL_SERVICE_ID) val serviceID: String, @PrimaryKey @ColumnInfo(name = COL_TRIP_ID) val tripID: String, @ColumnInfo(name = COL_HEADSIGN) val tripHeadsign: String, @ColumnInfo(name = COL_DIRECTION_ID) val directionID: Int, @ColumnInfo(name = COL_BLOCK_ID) val blockID: String, @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( valuesByColumn[COL_ROUTE_ID]!!, valuesByColumn[COL_SERVICE_ID]!!, valuesByColumn[COL_TRIP_ID]!!, valuesByColumn[COL_HEADSIGN]!!, 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{ const val DB_TABLE="gtfs_trips" const val COL_ROUTE_ID="route_id" const val COL_SERVICE_ID="service_id" const val COL_TRIP_ID = "trip_id" const val COL_HEADSIGN="trip_headsign" //const val COL_SHORT_NAME="trip_short_name", const val COL_DIRECTION_ID="direction_id" const val COL_BLOCK_ID="block_id" 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, COL_SERVICE_ID, COL_TRIP_ID, COL_HEADSIGN, COL_DIRECTION_ID, COL_BLOCK_ID, COL_SHAPE_ID, COL_WHEELCHAIR, COL_LIMITED_R ) /* open fun fromContentValues(values: ContentValues) { val tripItem = GtfsTrips(); } */ } 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 diff --git a/app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt b/app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt index 5e6e09b..9f4fa52 100644 --- a/app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt +++ b/app/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt @@ -1,140 +1,142 @@ /* BusTO - Data components Copyright (C) 2022 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data.gtfs import androidx.room.* import it.reyboz.bustorino.backend.Stop @Entity(tableName = MatoPattern.TABLE_NAME, foreignKeys = [ ForeignKey(entity = GtfsRoute::class, parentColumns = [GtfsRoute.COL_ROUTE_ID], childColumns = [MatoPattern.COL_ROUTE_ID], onDelete = ForeignKey.CASCADE, ) - ] + ], + indices = [Index(MatoPattern.COL_CODE), Index(MatoPattern.COL_ROUTE_ID)] ) data class MatoPattern( @ColumnInfo(name= COL_NAME) val name: String, @ColumnInfo(name= COL_CODE) @PrimaryKey val code: String, @ColumnInfo(name= COL_SEMANTIC_HASH) val semanticHash: String, @ColumnInfo(name= COL_DIRECTION_ID) val directionId: Int, @ColumnInfo(name= COL_ROUTE_ID) val routeGtfsId: String, @ColumnInfo(name= COL_HEADSIGN) var headsign: String?, @ColumnInfo(name= COL_GEOMETRY_POLY) val patternGeometryPoly: String, @ColumnInfo(name= COL_GEOMETRY_LENGTH) val patternGeometryLength: Int, @Ignore val stopsGtfsIDs: ArrayList ):GtfsTable{ @Ignore val servingStops= ArrayList(4) constructor( name: String, code:String, semanticHash: String, directionId: Int, routeGtfsId: String, headsign: String?, patternGeometryPoly: String, patternGeometryLength: Int ): this(name, code, semanticHash, directionId, routeGtfsId, headsign, patternGeometryPoly, patternGeometryLength, ArrayList(4)) companion object{ const val TABLE_NAME="mato_patterns" const val COL_NAME="pattern_name" const val COL_CODE="pattern_code" const val COL_ROUTE_ID="pattern_route_id" const val COL_SEMANTIC_HASH="pattern_hash" const val COL_DIRECTION_ID="pattern_direction_id" const val COL_HEADSIGN="pattern_headsign" const val COL_GEOMETRY_POLY="pattern_polyline" const val COL_GEOMETRY_LENGTH="pattern_polylength" val COLUMNS = arrayOf( COL_NAME, COL_CODE, COL_ROUTE_ID, COL_SEMANTIC_HASH, COL_DIRECTION_ID, COL_HEADSIGN, COL_GEOMETRY_POLY, COL_GEOMETRY_LENGTH ) } override fun getColumns(): Array { return COLUMNS } } //DO NOT USE EMBEDDED!!! -> copies all data @Entity(tableName=PatternStop.TABLE_NAME, primaryKeys = [ PatternStop.COL_PATTERN_ID, PatternStop.COL_STOP_GTFS, PatternStop.COL_ORDER ], foreignKeys = [ ForeignKey(entity = MatoPattern::class, parentColumns = [MatoPattern.COL_CODE], childColumns = [PatternStop.COL_PATTERN_ID], onDelete = ForeignKey.CASCADE ) - ] + ], + indices = [Index(PatternStop.COL_PATTERN_ID)] ) data class PatternStop( @ColumnInfo(name= COL_PATTERN_ID) val patternId: String, @ColumnInfo(name=COL_STOP_GTFS) val stopGtfsId: String, @ColumnInfo(name=COL_ORDER) val order: Int, ){ companion object{ const val TABLE_NAME="patterns_stops" const val COL_PATTERN_ID="pattern_gtfs_id" const val COL_STOP_GTFS="stop_gtfs_id" const val COL_ORDER="stop_order" } } data class MatoPatternWithStops( @Embedded val pattern: MatoPattern, @Relation( parentColumn = MatoPattern.COL_CODE, entityColumn = PatternStop.COL_PATTERN_ID, ) var stopsIndices: List) { init { stopsIndices = stopsIndices.sortedBy { p-> p.order } } } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/data/gtfs/WheelchairAccess.kt b/app/src/it/reyboz/bustorino/data/gtfs/WheelchairAccess.kt new file mode 100644 index 0000000..e26b677 --- /dev/null +++ b/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 diff --git a/app/src/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/it/reyboz/bustorino/fragments/MapFragment.java index 65d43fc..48c41a5 100644 --- a/app/src/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,664 +1,820 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.Manifest; +import android.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; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; +import androidx.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; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.lang.ref.WeakReference; import java.util.*; +import 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; public class MapFragment extends ScreenBaseFragment { private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String BUNDLE_ROUTES_STOPPING = "routesStopping"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; //the asynctask used to get the stops from the database private AsyncStopFetcher stopFetcher = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; + private boolean 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) { if (listenerMain!= null){ Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID); listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if (result == null){ Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?"); } else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(@NonNull Stop stop){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stop.getLatitude()); args.putDouble(BUNDLE_LONGIT, stop.getLongitude()); args.putString(BUNDLE_NAME, stop.getStopDisplayName()); args.putString(BUNDLE_ID, stop.ID); args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()); fragment.setArguments(args); return fragment; } //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.activity_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); btCenterMap = root.findViewById(R.id.icon_center_map); btFollowMe = root.findViewById(R.id.icon_follow); //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 if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.locationPermissionGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.locationPermissionGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); + mapViewModel = new ViewModelProvider(this).get(MapViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; + //stop animations + // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { 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); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); if (map == null){ //The map is null, it can happen? Log.e(DEBUG_TAG, "Cannot save map center, map is null"); return; } final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } @Override 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 public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == null || map ==null) //nothing else to do return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //clear previous overlays map.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; String routesStopping = ""; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, ""); } //ask for location permission if(!Permissions.locationPermissionGranted(activity)){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //TODO: show dialog for permission rationale Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show(); } positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.locationPermissionGranted(activity), map); // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); setLocationFollowing(false); // put the center a little bit off (animate later) startPoint = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.locationPermissionGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; setLocationFollowing(true); } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker //TODO: make Stop Bundle-able 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; } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); } + 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 */ protected void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); if(!map.isShown()){ if(good) - Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); + Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); good = false; continue; } else if(map.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } good=true; } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){ return makeMarker(geoPoint,stop.ID, stop.getStopDefaultName(), stop.routesThatStopHereToString(), isStartMarker); } public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, String routesStopping, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); - marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_marker, ctx.getTheme())); + 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 marker.setSnippet(stopID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return null; } /** * Simple asyncTask class to load the stops in the background * Holds a weak reference to the fragment to do callbacks */ static class AsyncStopFetcher extends AsyncTask>{ final WeakReference fragmentWeakReference; public AsyncStopFetcher(MapFragment fragment) { this.fragmentWeakReference = new WeakReference<>(fragment); } @Override protected List doInBackground(BoundingBoxLimit... limits) { if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){ Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return null; } final BoundingBoxLimit limit = limits[0]; //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); NextGenDB dbHelper = NextGenDB.getInstance(fragmentWeakReference.get().getContext()); ArrayList stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo, limit.longFrom, limit.latitTo); dbHelper.close(); return stops; } @Override protected void onPostExecute(List stops) { super.onPostExecute(stops); //Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working"); if(fragmentWeakReference.get()==null) { Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null"); return; } if (stops!=null) Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size()); fragmentWeakReference.get().showStopsMarkers(stops); } private static class BoundingBoxLimit{ final double longFrom, longTo, latitFrom, latitTo; public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) { this.longFrom = longFrom; this.longTo = longTo; this.latitFrom = latitFrom; this.latitTo = latitTo; } } } } diff --git a/app/src/it/reyboz/bustorino/fragments/MapViewModel.kt b/app/src/it/reyboz/bustorino/fragments/MapViewModel.kt new file mode 100644 index 0000000..64b4adf --- /dev/null +++ b/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 diff --git a/app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt b/app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt index 80238c0..e11de63 100644 --- a/app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt +++ b/app/src/it/reyboz/bustorino/fragments/TestRealtimeGtfsFragment.kt @@ -1,90 +1,93 @@ package it.reyboz.bustorino.fragments import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.TextView import android.widget.Toast import com.android.volley.Response 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 private const val ARG_PARAM1 = "param1" private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. * Use the [TestRealtimeGtfsFragment.newInstance] factory method to * create an instance of this fragment. */ class TestRealtimeGtfsFragment : Fragment() { 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}" } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val rootView= inflater.inflate(R.layout.fragment_test_realtime_gtfs, container, false) buttonLaunch = rootView.findViewById(R.id.btn_download_data) messageTextView = rootView.findViewById(R.id.gtfsMessageTextView) 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 ) NetworkVolleyManager.getInstance(cont).addToRequestQueue(req) } } return rootView } companion object { /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @return A new instance of fragment TestRealtimeGtfsFragment. */ @JvmStatic fun newInstance() = TestRealtimeGtfsFragment().apply { } } } \ No newline at end of file diff --git a/app/src/it/reyboz/bustorino/map/BusInfoWindow.kt b/app/src/it/reyboz/bustorino/map/BusInfoWindow.kt new file mode 100644 index 0000000..dced2ec --- /dev/null +++ b/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 diff --git a/app/src/it/reyboz/bustorino/map/GeoPointInterpolator.java b/app/src/it/reyboz/bustorino/map/GeoPointInterpolator.java new file mode 100644 index 0000000..94a8323 --- /dev/null +++ b/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))); + } + } +} diff --git a/app/src/it/reyboz/bustorino/map/MarkerAnimation.java b/app/src/it/reyboz/bustorino/map/MarkerAnimation.java new file mode 100644 index 0000000..7e7ebb9 --- /dev/null +++ b/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; + } +} diff --git a/app/src/it/reyboz/bustorino/util/DrawableUtils.kt b/app/src/it/reyboz/bustorino/util/DrawableUtils.kt new file mode 100644 index 0000000..1f0d712 --- /dev/null +++ b/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 diff --git a/app/src/test/java/it/reyboz/bustorino/util/GTFSRouteNameTest.java b/app/src/test/java/it/reyboz/bustorino/util/GTFSRouteNameTest.java new file mode 100644 index 0000000..7cf3502 --- /dev/null +++ b/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")); + } +} diff --git a/build.gradle b/build.gradle index 9af2fd8..68a2c66 100644 --- a/build.gradle +++ b/build.gradle @@ -1,47 +1,47 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { - jcenter() + mavenCentral() maven { url 'https://maven.google.com' } google() } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21" } } ext { androidXTestVersion = "1.4.0" //multidex multidex_version = "2.0.1" //libraries versions fragment_version = "1.4.1" activity_version = "1.4.0" appcompat_version = "1.4.1" preference_version = "1.2.0" work_version = "2.7.1" acra_version = "5.7.0" lifecycle_version = "2.4.1" arch_version = "2.1.0" room_version = "2.4.1" //kotlin kotlin_version = '1.6.0' coroutines_version = "1.5.0" } allprojects { repositories { - jcenter() maven { url 'https://maven.google.com' } google() mavenCentral() + //maven { url "https://jitpack.io" } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 555b3de..115a516 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Apr 24 16:03:07 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME