diff --git a/GTFS_changes.diff b/GTFS_changes.diff new file mode 100644 --- /dev/null +++ b/GTFS_changes.diff @@ -0,0 +1,501 @@ +diff --git a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt +index 453630e..5f6e054 100644 +--- a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt ++++ b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt +@@ -19,13 +19,12 @@ package it.reyboz.bustorino.data.gtfs + + import android.content.Context + import android.util.Log +-import java.util.ArrayList + + class CsvTableInserter( + val tableName: String, context: Context + ) { + private val database: GtfsDatabase = GtfsDatabase.getGtfsDatabase(context) +- private val dao: StaticGtfsDao = database.gtfsDao() ++ private val databaseDao: GtfsDBDao = database.gtfsDao() + + private val elementsList: MutableList< in GtfsTable> = mutableListOf() + +@@ -35,12 +34,12 @@ class CsvTableInserter( + private var countInsert = 0 + init { + if(tableName == "stop_times") { +- stopsIDsPresent = dao.getAllStopsIDs().toHashSet() +- tripsIDsPresent = dao.getAllTripsIDs().toHashSet() ++ stopsIDsPresent = databaseDao.getAllStopsIDs().toHashSet() ++ tripsIDsPresent = databaseDao.getAllTripsIDs().toHashSet() + Log.d(DEBUG_TAG, "num stop IDs present: "+ stopsIDsPresent!!.size) + Log.d(DEBUG_TAG, "num trips IDs present: "+ tripsIDsPresent!!.size) + } else if(tableName == "routes"){ +- dao.deleteAllRoutes() ++ databaseDao.deleteAllRoutes() + } + } + +@@ -77,7 +76,7 @@ class CsvTableInserter( + //have to insert + + if (tableName == "routes") +- dao.insertRoutes(elementsList.filterIsInstance()) ++ databaseDao.insertRoutes(elementsList.filterIsInstance()) + else + insertDataInDatabase() + +@@ -90,21 +89,21 @@ class CsvTableInserter( + countInsert += elementsList.size + when(tableName){ + "stops" -> { +- dao.insertStops(elementsList.filterIsInstance()) ++ databaseDao.insertStops(elementsList.filterIsInstance()) + } +- "routes" -> dao.insertRoutes(elementsList.filterIsInstance()) +- "calendar" -> dao.insertServices(elementsList.filterIsInstance()) +- "calendar_dates" -> dao.insertDates(elementsList.filterIsInstance()) +- "trips" -> dao.insertTrips(elementsList.filterIsInstance()) +- "stop_times"-> dao.insertStopTimes(elementsList.filterIsInstance()) +- "shapes" -> dao.insertShapes(elementsList.filterIsInstance()) ++ "routes" -> databaseDao.insertRoutes(elementsList.filterIsInstance()) ++ "calendar" -> databaseDao.insertServices(elementsList.filterIsInstance()) ++ "calendar_dates" -> databaseDao.insertDates(elementsList.filterIsInstance()) ++ "trips" -> databaseDao.insertTrips(elementsList.filterIsInstance()) ++ "stop_times"-> databaseDao.insertStopTimes(elementsList.filterIsInstance()) ++ "shapes" -> databaseDao.insertShapes(elementsList.filterIsInstance()) + + } + ///if(elementsList.size < MAX_ELEMENTS) + } + fun finishInsert(){ + insertDataInDatabase() +- Log.d(DEBUG_TAG, "Inserted "+countInsert+" elements from "+tableName); ++ Log.d(DEBUG_TAG, "Inserted $countInsert elements from $tableName") + } + + companion object{ +diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt +new file mode 100644 +index 0000000..e53729a +--- /dev/null ++++ b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt +@@ -0,0 +1,55 @@ ++package it.reyboz.bustorino.data.gtfs ++ ++import androidx.room.ColumnInfo ++import androidx.room.Embedded ++import androidx.room.Entity ++import androidx.room.PrimaryKey ++ ++@Entity(tableName = GtfsAgency.TABLE_NAME) ++data class GtfsAgency( ++ @PrimaryKey ++ @ColumnInfo(name = COL_GTFS_ID) ++ val gtfsId: String, ++ @ColumnInfo(name = COL_NAME) ++ val name: String, ++ @ColumnInfo(name = COL_URL) ++ val url: String, ++ @ColumnInfo(name = COL_FAREURL) ++ val fareUrl: String?, ++ @ColumnInfo(name = COL_PHONE) ++ val phone: String?, ++ @Embedded var feed: GtfsFeed? ++): GtfsTable{ ++ constructor(valuesByColumn: Map) : this( ++ valuesByColumn[COL_GTFS_ID]!!, ++ valuesByColumn[COL_NAME]!!, ++ valuesByColumn[COL_URL]!!, ++ valuesByColumn[COL_FAREURL], ++ valuesByColumn[COL_PHONE], ++ null ++ ) ++ ++ companion object{ ++ const val TABLE_NAME="gtfs_agencies" ++ ++ const val COL_GTFS_ID="gtfs_id" ++ const val COL_NAME="ag_name" ++ const val COL_URL="ag_url" ++ const val COL_FAREURL = "fare_url" ++ const val COL_PHONE = "phone" ++ ++ val COLUMNS = arrayOf( ++ COL_GTFS_ID, ++ COL_NAME, ++ COL_URL, ++ COL_FAREURL, ++ COL_PHONE ++ ) ++ const val CREATE_SQL = ++ "CREATE TABLE $TABLE_NAME ( $COL_GTFS_ID )" ++ } ++ ++ override fun getColumns(): Array { ++ return COLUMNS ++ } ++} +diff --git a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt +similarity index 80% +rename from src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt +rename to src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt +index 32f862c..5a00e74 100644 +--- a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt ++++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt +@@ -21,8 +21,8 @@ import androidx.lifecycle.LiveData + import androidx.room.* + + @Dao +-interface StaticGtfsDao { +- @Query("SELECT * FROM "+GtfsRoute.DB_TABLE+" ORDER BY "+GtfsRoute.COL_SORT_ORDER) ++interface GtfsDBDao { ++ @Query("SELECT * FROM "+GtfsRoute.DB_TABLE) + fun getAllRoutes() : LiveData> + + @Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE) +@@ -45,9 +45,9 @@ interface StaticGtfsDao { + deleteAllRoutes() + insertRoutes(routes) + } +- ++ @Transaction + @Insert(onConflict = OnConflictStrategy.REPLACE) +- fun insertRoutes(users: List) ++ fun insertRoutes(routes: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertStops(stops: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) +@@ -87,4 +87,21 @@ interface StaticGtfsDao { + @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) ++ } ++ ++ @Insert(onConflict = OnConflictStrategy.REPLACE) ++ fun insertPatterns(patterns: List) ++ ++ @Insert(onConflict = OnConflictStrategy.REPLACE) ++ fun insertPatternStops(patternStops: List) + } +\ No newline at end of file +diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt +index 986f01f..c6f19a7 100644 +--- a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt ++++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt +@@ -22,20 +22,24 @@ import androidx.room.* + + @Database( + entities = [ ++ GtfsFeed::class, ++ GtfsAgency::class, + GtfsServiceDate::class, + GtfsStop::class, + GtfsService::class, + GtfsRoute::class, + GtfsStopTime::class, + GtfsTrip::class, +- GtfsShape::class], ++ GtfsShape::class, ++ MatoPattern::class, ++ PatternStop::class ++ ], + version = GtfsDatabase.VERSION, +- exportSchema = false, + ) + @TypeConverters(Converters::class) +-public abstract class GtfsDatabase : RoomDatabase() { ++abstract class GtfsDatabase : RoomDatabase() { + +- abstract fun gtfsDao() : StaticGtfsDao ++ abstract fun gtfsDao() : GtfsDBDao + + companion object{ + @Volatile +@@ -51,7 +55,13 @@ public abstract class GtfsDatabase : RoomDatabase() { + } + } + +- const val VERSION = 1 ++ const val VERSION = 2 + const val FOREIGNKEY_ONDELETE = ForeignKey.CASCADE ++ ++ /*val MIGRATION_1_2 = Migration(1,2) { ++ TODO("Have to write it") //it.execSQL() ++ } ++ ++ */ + } + } +\ No newline at end of file +diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt +new file mode 100644 +index 0000000..8105fa6 +--- /dev/null ++++ b/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt +@@ -0,0 +1,50 @@ ++/* ++ BusTO - Data components ++ Copyright (C) 2022 Fabio Mazza ++ ++ This program is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ This program is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with this program. If not, see . ++ */ ++package it.reyboz.bustorino.data.gtfs ++ ++import androidx.room.ColumnInfo ++import androidx.room.Entity ++import androidx.room.PrimaryKey ++ ++@Entity(tableName = GtfsFeed.TABLE_NAME) ++data class GtfsFeed( ++ @PrimaryKey ++ @ColumnInfo(name = COL_GTFS_ID) ++ val gtfsId: String, ++): GtfsTable{ ++ constructor(valuesByColumn: Map) : this( ++ valuesByColumn[COL_GTFS_ID]!!, ++ ) ++ ++ companion object{ ++ const val TABLE_NAME="gtfs_feeds" ++ ++ const val COL_GTFS_ID="feed_id" ++ ++ ++ val COLUMNS = arrayOf( ++ COL_GTFS_ID, ++ ) ++ const val CREATE_SQL = ++ "CREATE TABLE $TABLE_NAME ( $COL_GTFS_ID )" ++ } ++ ++ override fun getColumns(): Array { ++ return COLUMNS ++ } ++} +diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt +new file mode 100644 +index 0000000..a9b7f2a +--- /dev/null ++++ b/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt +@@ -0,0 +1,19 @@ ++package it.reyboz.bustorino.data.gtfs ++ ++enum class GtfsMode(val intType: Int) { ++ TRAM(0), ++ SUBWAY(1), ++ RAIL(2), ++ BUS(3), ++ FERRY(4), ++ CABLE_TRAM(5), ++ GONDOLA(6), ++ FUNICULAR(7), ++ TROLLEYBUS(11), ++ MONORAIL(12); ++ ++ companion object { ++ private val VALUES = values() ++ fun getByValue(value: Int) = VALUES.firstOrNull { it.intType == value } ++ } ++} +\ No newline at end of file +diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt +index fef6167..fa39e16 100644 +--- a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt ++++ b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt +@@ -23,26 +23,25 @@ import androidx.room.PrimaryKey + + @Entity(tableName=GtfsRoute.DB_TABLE) + data class GtfsRoute( +- @PrimaryKey @ColumnInfo(name = COL_ROUTE_ID) +- val ID: String, +- @ColumnInfo(name = "agency_id") ++ @PrimaryKey @ColumnInfo(name = COL_ROUTE_ID) ++ val gtfsId: String, ++ @ColumnInfo(name = "agency_id") + val agencyID: String, +- @ColumnInfo(name = "route_short_name") ++ @ColumnInfo(name = "route_short_name") + val shortName: String, +- @ColumnInfo(name = "route_long_name") ++ @ColumnInfo(name = "route_long_name") + val longName: String, +- @ColumnInfo(name = "route_desc") ++ @ColumnInfo(name = "route_desc") + val description: String, +- @ColumnInfo(name ="route_type") +- val type: String, ++ @ColumnInfo(name = COL_MODE) ++ val mode: GtfsMode, + //@ColumnInfo(name ="route_url") + //val url: String, +- @ColumnInfo(name ="route_color") ++ @ColumnInfo(name = COL_COLOR) + val color: String, +- @ColumnInfo(name ="route_text_color") ++ @ColumnInfo(name = COL_TEXT_COLOR) + val textColor: String, +- @ColumnInfo(name = COL_SORT_ORDER) +- val sortOrder: Int ++ + ): GtfsTable { + + constructor(valuesByColumn: Map) : this( +@@ -51,15 +50,17 @@ data class GtfsRoute( + valuesByColumn["route_short_name"]!!, + valuesByColumn["route_long_name"]!!, + valuesByColumn["route_desc"]!!, +- valuesByColumn["route_type"]!!, +- valuesByColumn["route_color"]!!, +- valuesByColumn["route_text_color"]!!, +- valuesByColumn[COL_SORT_ORDER]?.toInt()!! ++ valuesByColumn["route_type"]?.toInt()?.let { GtfsMode.getByValue(it) }!!, ++ valuesByColumn[COL_COLOR]!!, ++ valuesByColumn[COL_TEXT_COLOR]!!, + ) + companion object { + const val DB_TABLE: String="routes_table" + const val COL_SORT_ORDER: String="route_sort_order" + const val COL_ROUTE_ID = "route_id" ++ const val COL_MODE ="route_mode" ++ const val COL_COLOR="route_color" ++ const val COL_TEXT_COLOR="route_text_color" + + val COLUMNS = arrayOf(COL_ROUTE_ID, + "agency_id", +diff --git a/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt +new file mode 100644 +index 0000000..83a0bb5 +--- /dev/null ++++ b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt +@@ -0,0 +1,111 @@ ++/* ++ BusTO - Data components ++ Copyright (C) 2022 Fabio Mazza ++ ++ This program is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ This program is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with this program. If not, see . ++ */ ++package it.reyboz.bustorino.data.gtfs ++ ++import androidx.room.ColumnInfo ++import androidx.room.Entity ++import androidx.room.Ignore ++import androidx.room.PrimaryKey ++import it.reyboz.bustorino.backend.Stop ++ ++@Entity(tableName = MatoPattern.TABLE_NAME) ++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 ++ ] ++) ++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" ++ } ++} +\ No newline at end of file diff --git a/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/1.json b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/1.json new file mode 100644 --- /dev/null +++ b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/1.json @@ -0,0 +1,464 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "5c633aea20ff416df784e00a939d7ae5", + "entities": [ + { + "tableName": "gtfs_calendar_dates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `date` TEXT NOT NULL, `exception_type` INTEGER NOT NULL, PRIMARY KEY(`service_id`, `date`), FOREIGN KEY(`service_id`) REFERENCES `gtfs_calendar`(`service_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exceptionType", + "columnName": "exception_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id", + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "gtfs_calendar", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "service_id" + ], + "referencedColumns": [ + "service_id" + ] + } + ] + }, + { + "tableName": "stops_gtfs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stop_id` INTEGER NOT NULL, `stop_code` TEXT NOT NULL, `stop_name` TEXT NOT NULL, `stop_desc` TEXT NOT NULL, `stop_lat` REAL NOT NULL, `stop_lon` REAL NOT NULL, `wheelchair_boarding` TEXT NOT NULL, PRIMARY KEY(`stop_id`))", + "fields": [ + { + "fieldPath": "internalID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gttStopID", + "columnName": "stop_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopName", + "columnName": "stop_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gttPlaceName", + "columnName": "stop_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "stop_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "stop_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wheelchair", + "columnName": "wheelchair_boarding", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stop_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `monday` INTEGER NOT NULL, `tuesday` INTEGER NOT NULL, `wednesday` INTEGER NOT NULL, `thursday` INTEGER NOT NULL, `friday` INTEGER NOT NULL, `saturday` INTEGER NOT NULL, `sunday` INTEGER NOT NULL, `start_date` TEXT NOT NULL, `end_date` TEXT NOT NULL, PRIMARY KEY(`service_id`))", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "onMonday", + "columnName": "monday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onTuesday", + "columnName": "tuesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onWednesday", + "columnName": "wednesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onThursday", + "columnName": "thursday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onFriday", + "columnName": "friday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSaturday", + "columnName": "saturday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSunday", + "columnName": "sunday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "end_date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "routes_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_type` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, `route_sort_order` INTEGER NOT NULL, PRIMARY KEY(`route_id`))", + "fields": [ + { + "fieldPath": "ID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agencyID", + "columnName": "agency_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "route_short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "route_long_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "route_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "route_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "route_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textColor", + "columnName": "route_text_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "route_sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "route_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_stop_times", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`trip_id` TEXT NOT NULL, `arrival_time` TEXT NOT NULL, `departure_time` TEXT NOT NULL, `stop_id` INTEGER NOT NULL, `stop_sequence` INTEGER NOT NULL, PRIMARY KEY(`trip_id`, `stop_id`), FOREIGN KEY(`stop_id`) REFERENCES `stops_gtfs`(`stop_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`trip_id`) REFERENCES `gtfs_trips`(`trip_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrival_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departure_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stopSequence", + "columnName": "stop_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id", + "stop_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_stop_times_stop_id", + "unique": false, + "columnNames": [ + "stop_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_stop_times_stop_id` ON `${TABLE_NAME}` (`stop_id`)" + } + ], + "foreignKeys": [ + { + "table": "stops_gtfs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stop_id" + ], + "referencedColumns": [ + "stop_id" + ] + }, + { + "table": "gtfs_trips", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "trip_id" + ], + "referencedColumns": [ + "trip_id" + ] + } + ] + }, + { + "tableName": "gtfs_trips", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `service_id` TEXT NOT NULL, `trip_id` TEXT NOT NULL, `trip_headsign` TEXT NOT NULL, `direction_id` INTEGER NOT NULL, `block_id` TEXT NOT NULL, `shape_id` TEXT NOT NULL, `wheelchair_accessible` INTEGER NOT NULL, `limited_route` INTEGER NOT NULL, PRIMARY KEY(`trip_id`), FOREIGN KEY(`route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "routeID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripHeadsign", + "columnName": "trip_headsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionID", + "columnName": "direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockID", + "columnName": "block_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isWheelchairAccess", + "columnName": "wheelchair_accessible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLimitedRoute", + "columnName": "limited_route", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_trips_route_id", + "unique": false, + "columnNames": [ + "route_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_route_id` ON `${TABLE_NAME}` (`route_id`)" + } + ], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "gtfs_shapes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shape_id` TEXT NOT NULL, `shape_pt_lat` REAL NOT NULL, `shape_pt_lon` REAL NOT NULL, `shape_pt_sequence` INTEGER NOT NULL, PRIMARY KEY(`shape_id`, `shape_pt_sequence`))", + "fields": [ + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointLat", + "columnName": "shape_pt_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointLon", + "columnName": "shape_pt_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointSequence", + "columnName": "shape_pt_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "shape_id", + "shape_pt_sequence" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5c633aea20ff416df784e00a939d7ae5')" + ] + } +} \ No newline at end of file diff --git a/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/2.json b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/2.json new file mode 100644 --- /dev/null +++ b/assets/schemas/it.reyboz.bustorino.data.gtfs.GtfsDatabase/2.json @@ -0,0 +1,648 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6d2aa826894d1e6b1429678e13b65433", + "entities": [ + { + "tableName": "gtfs_feeds", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feed_id` TEXT NOT NULL, PRIMARY KEY(`feed_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "feed_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_agencies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gtfs_id` TEXT NOT NULL, `ag_name` TEXT NOT NULL, `ag_url` TEXT NOT NULL, `fare_url` TEXT, `phone` TEXT, `feed_id` TEXT, PRIMARY KEY(`gtfs_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "ag_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "ag_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fareUrl", + "columnName": "fare_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feed.gtfsId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "gtfs_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_calendar_dates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `date` TEXT NOT NULL, `exception_type` INTEGER NOT NULL, PRIMARY KEY(`service_id`, `date`), FOREIGN KEY(`service_id`) REFERENCES `gtfs_calendar`(`service_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exceptionType", + "columnName": "exception_type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id", + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "gtfs_calendar", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "service_id" + ], + "referencedColumns": [ + "service_id" + ] + } + ] + }, + { + "tableName": "stops_gtfs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stop_id` INTEGER NOT NULL, `stop_code` TEXT NOT NULL, `stop_name` TEXT NOT NULL, `stop_desc` TEXT NOT NULL, `stop_lat` REAL NOT NULL, `stop_lon` REAL NOT NULL, `wheelchair_boarding` TEXT NOT NULL, PRIMARY KEY(`stop_id`))", + "fields": [ + { + "fieldPath": "internalID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gttStopID", + "columnName": "stop_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopName", + "columnName": "stop_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gttPlaceName", + "columnName": "stop_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "stop_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "stop_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wheelchair", + "columnName": "wheelchair_boarding", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "stop_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`service_id` TEXT NOT NULL, `monday` INTEGER NOT NULL, `tuesday` INTEGER NOT NULL, `wednesday` INTEGER NOT NULL, `thursday` INTEGER NOT NULL, `friday` INTEGER NOT NULL, `saturday` INTEGER NOT NULL, `sunday` INTEGER NOT NULL, `start_date` TEXT NOT NULL, `end_date` TEXT NOT NULL, PRIMARY KEY(`service_id`))", + "fields": [ + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "onMonday", + "columnName": "monday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onTuesday", + "columnName": "tuesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onWednesday", + "columnName": "wednesday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onThursday", + "columnName": "thursday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onFriday", + "columnName": "friday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSaturday", + "columnName": "saturday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onSunday", + "columnName": "sunday", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "start_date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "end_date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "service_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "routes_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_mode` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, PRIMARY KEY(`route_id`))", + "fields": [ + { + "fieldPath": "gtfsId", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agencyID", + "columnName": "agency_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "route_short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "route_long_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "route_desc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "route_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "route_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textColor", + "columnName": "route_text_color", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "route_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "gtfs_stop_times", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`trip_id` TEXT NOT NULL, `arrival_time` TEXT NOT NULL, `departure_time` TEXT NOT NULL, `stop_id` INTEGER NOT NULL, `stop_sequence` INTEGER NOT NULL, PRIMARY KEY(`trip_id`, `stop_id`), FOREIGN KEY(`stop_id`) REFERENCES `stops_gtfs`(`stop_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`trip_id`) REFERENCES `gtfs_trips`(`trip_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrival_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departure_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopID", + "columnName": "stop_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stopSequence", + "columnName": "stop_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id", + "stop_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_stop_times_stop_id", + "unique": false, + "columnNames": [ + "stop_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_stop_times_stop_id` ON `${TABLE_NAME}` (`stop_id`)" + } + ], + "foreignKeys": [ + { + "table": "stops_gtfs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stop_id" + ], + "referencedColumns": [ + "stop_id" + ] + }, + { + "table": "gtfs_trips", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "trip_id" + ], + "referencedColumns": [ + "trip_id" + ] + } + ] + }, + { + "tableName": "gtfs_trips", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route_id` TEXT NOT NULL, `service_id` TEXT NOT NULL, `trip_id` TEXT NOT NULL, `trip_headsign` TEXT NOT NULL, `direction_id` INTEGER NOT NULL, `block_id` TEXT NOT NULL, `shape_id` TEXT NOT NULL, `wheelchair_accessible` INTEGER NOT NULL, `limited_route` INTEGER NOT NULL, PRIMARY KEY(`trip_id`), FOREIGN KEY(`route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "routeID", + "columnName": "route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceID", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripID", + "columnName": "trip_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tripHeadsign", + "columnName": "trip_headsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionID", + "columnName": "direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockID", + "columnName": "block_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isWheelchairAccess", + "columnName": "wheelchair_accessible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLimitedRoute", + "columnName": "limited_route", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "trip_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_gtfs_trips_route_id", + "unique": false, + "columnNames": [ + "route_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_gtfs_trips_route_id` ON `${TABLE_NAME}` (`route_id`)" + } + ], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "gtfs_shapes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shape_id` TEXT NOT NULL, `shape_pt_lat` REAL NOT NULL, `shape_pt_lon` REAL NOT NULL, `shape_pt_sequence` INTEGER NOT NULL, PRIMARY KEY(`shape_id`, `shape_pt_sequence`))", + "fields": [ + { + "fieldPath": "shapeID", + "columnName": "shape_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointLat", + "columnName": "shape_pt_lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointLon", + "columnName": "shape_pt_lon", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pointSequence", + "columnName": "shape_pt_sequence", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "shape_id", + "shape_pt_sequence" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mato_patterns", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_name` TEXT NOT NULL, `pattern_code` TEXT NOT NULL, `pattern_hash` TEXT NOT NULL, `pattern_direction_id` INTEGER NOT NULL, `pattern_route_id` TEXT NOT NULL, `pattern_headsign` TEXT, `pattern_polyline` TEXT NOT NULL, `pattern_polylength` INTEGER NOT NULL, PRIMARY KEY(`pattern_code`), FOREIGN KEY(`pattern_route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "pattern_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "pattern_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "semanticHash", + "columnName": "pattern_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionId", + "columnName": "pattern_direction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "routeGtfsId", + "columnName": "pattern_route_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headsign", + "columnName": "pattern_headsign", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patternGeometryPoly", + "columnName": "pattern_polyline", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patternGeometryLength", + "columnName": "pattern_polylength", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "pattern_code" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "routes_table", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pattern_route_id" + ], + "referencedColumns": [ + "route_id" + ] + } + ] + }, + { + "tableName": "patterns_stops", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_gtfs_id` TEXT NOT NULL, `stop_gtfs_id` TEXT NOT NULL, `stop_order` INTEGER NOT NULL, PRIMARY KEY(`pattern_gtfs_id`, `stop_gtfs_id`, `stop_order`), FOREIGN KEY(`pattern_gtfs_id`) REFERENCES `mato_patterns`(`pattern_code`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "patternId", + "columnName": "pattern_gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopGtfsId", + "columnName": "stop_gtfs_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "stop_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "pattern_gtfs_id", + "stop_gtfs_id", + "stop_order" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "mato_patterns", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pattern_gtfs_id" + ], + "referencedColumns": [ + "pattern_code" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d2aa826894d1e6b1429678e13b65433')" + ] + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,8 @@ } ext { + androidXTestVersion = "1.4.0" + //multidex multidex_version = "2.0.1" //libraries versions @@ -57,6 +59,12 @@ versionName "1.16.3" vectorDrawables.useSupportLibrary = true multiDexEnabled true + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] + } + } + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { @@ -65,6 +73,8 @@ } sourceSets { + androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) + main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] @@ -128,17 +138,34 @@ // Room components implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" - androidTestImplementation "androidx.room:room-testing:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' + testImplementation 'junit:junit:4.12' + implementation 'junit:junit:4.12' + + implementation "androidx.test.ext:junit:1.1.3" + implementation "androidx.test:core:$androidXTestVersion" + implementation "androidx.test:runner:$androidXTestVersion" + implementation "androidx.room:room-testing:$room_version" + + androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test:core:$androidXTestVersion" + androidTestImplementation "androidx.test:runner:$androidXTestVersion" + androidTestImplementation "androidx.test:rules:$androidXTestVersion" + androidTestImplementation "androidx.room:room-testing:$room_version" + + + + } } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" diff --git a/res/layout/arrivals_nearby_card.xml b/res/layout/arrivals_nearby_card.xml --- a/res/layout/arrivals_nearby_card.xml +++ b/res/layout/arrivals_nearby_card.xml @@ -27,16 +27,23 @@ android:layout_margin="10dp" android:textStyle="normal" android:layout_marginRight="20dp" android:layout_marginEnd="20dp" android:layout_marginBottom="20dp"/> - + + Ricerca della posizione in corso… Nessuna fermata nei dintorni Preferenze - Aggiornamento del database… + Aggiornamento del database… + Aggiornamento del database + Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero @@ -140,6 +142,10 @@ Canale unico delle notifiche + Database + Informazioni sul database (aggiornamento) + + Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -126,7 +126,8 @@ Allow access to position to show it on the map Please enable GPS - Database update in progress… + Database update in progress… + Updating the database is arriving at at the stop %1$s - %2$s @@ -156,6 +157,9 @@ --> Default Default channel for notifications + Database + Notifications on the update of the database + Asked for %1$s permission too many times Cannot use the map with the storage permission! storage diff --git a/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java b/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java new file mode 100644 --- /dev/null +++ b/src/androidTest/java/it/reyboz/bustorino/data/gtfs/GtfsDBMigrationsTest.java @@ -0,0 +1,53 @@ +package it.reyboz.bustorino.data.gtfs; + +import androidx.room.Room; +import androidx.room.migration.Migration; +import androidx.room.testing.MigrationTestHelper; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +import it.reyboz.bustorino.data.gtfs.GtfsDatabase; + +//@RunWith(AndroidJUnit4.class) +public class GtfsDBMigrationsTest { + private static final String TEST_DB = "migration-test"; + + @Rule + public MigrationTestHelper helper; + + public GtfsDBMigrationsTest() { + helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), + GtfsDatabase.class.getCanonicalName(), + new FrameworkSQLiteOpenHelperFactory()); + } + + @Test + public void migrateAll() throws IOException { + // Create earliest version of the database. + SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); + db.close(); + + // Open latest version of the database. Room will validate the schema + // once all migrations execute. + GtfsDatabase appDb = Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().getTargetContext(), + GtfsDatabase.class, + TEST_DB) + .addMigrations(ALL_MIGRATIONS).build(); + appDb.getOpenHelper().getWritableDatabase(); + appDb.close(); + } + + // Array of all migrations + private static final Migration[] ALL_MIGRATIONS = new Migration[]{ + GtfsDatabase.Companion.getMIGRATION_1_2()}; +} + diff --git a/src/it/reyboz/bustorino/ActivityExperiments.java b/src/it/reyboz/bustorino/ActivityExperiments.java --- a/src/it/reyboz/bustorino/ActivityExperiments.java +++ b/src/it/reyboz/bustorino/ActivityExperiments.java @@ -18,21 +18,16 @@ package it.reyboz.bustorino; import android.content.Context; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.Toast; -import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.gtfs.GtfsDataParser; import it.reyboz.bustorino.backend.networkTools; -import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; -import it.reyboz.bustorino.data.gtfs.StaticGtfsDao; +import it.reyboz.bustorino.data.gtfs.GtfsDBDao; import it.reyboz.bustorino.middleware.GeneralActivity; import java.io.*; @@ -44,7 +39,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; public class ActivityExperiments extends GeneralActivity { @@ -91,7 +85,7 @@ "ExperimentGTFS", "Last update date is " + updateDate//utils.joinList(files, "\n") ); //Toast.makeText(v.getContext(), "Gtfs data already downloaded", Toast.LENGTH_SHORT).show(); - StaticGtfsDao dao = GtfsDatabase.Companion.getGtfsDatabase(appContext).gtfsDao(); + GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(appContext).gtfsDao(); Log.d(DEBUG_TAG, String.valueOf(dao)); dao.deleteAllStopTimes(); @@ -180,7 +174,7 @@ Runnable deleteDB = new Runnable() { @Override public void run() { - StaticGtfsDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); + GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); Log.d(DEBUG_TAG, String.valueOf(dao)); dao.deleteAllStopTimes(); dao.deleteAllTrips(); diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -41,11 +41,6 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; -import androidx.work.BackoffPolicy; -import androidx.work.Constraints; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.PeriodicWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; @@ -53,7 +48,6 @@ import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; -import java.util.concurrent.TimeUnit; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateWorker; @@ -389,7 +383,7 @@ } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); - snackbar = Snackbar.make(baseView, R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); + snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } diff --git a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java --- a/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/src/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -18,10 +18,12 @@ package it.reyboz.bustorino.adapters; import android.content.Context; +import android.content.SharedPreferences; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -37,15 +39,18 @@ import java.util.*; -public class ArrivalsStopAdapter extends RecyclerView.Adapter { +public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.arrivals_nearby_card; //private List stops; private @Nullable Location userPosition; private FragmentListenerMain listener; - private List< Pair > routesPairList = new ArrayList<>(); + private List< Pair > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable + private String KEY_CAPITALIZE; + private NameCapitalize capit; + public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @Nullable Location pos) { listener = fragmentListener; @@ -55,10 +60,16 @@ resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); + KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); + SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); + defSharPref.registerOnSharedPreferenceChangeListener(this); + String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); + this.capit = NameCapitalize.getCapitalize(capitalizeKey); } + @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); @@ -85,7 +96,7 @@ //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { holder.lineNameTextView.setText(r.getNameForDisplay()); - holder.lineDirectionTextView.setText(r.destinazione); + holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); @@ -117,6 +128,17 @@ return routesPairList.size(); } + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(KEY_CAPITALIZE)){ + String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); + capit = NameCapitalize.getCapitalize(k); + + notifyDataSetChanged(); + + } + } + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; diff --git a/src/it/reyboz/bustorino/adapters/NameCapitalize.java b/src/it/reyboz/bustorino/adapters/NameCapitalize.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/adapters/NameCapitalize.java @@ -0,0 +1,38 @@ +package it.reyboz.bustorino.adapters; + +import java.util.Locale; + +import it.reyboz.bustorino.backend.utils; + +public enum NameCapitalize { + DO_NOTHING, ALL, FIRST; + public static NameCapitalize getCapitalize(String capitalize){ + + switch (capitalize.trim()){ + case "KEEP": + return NameCapitalize.DO_NOTHING; + case "CAPITALIZE_ALL": + return NameCapitalize.ALL; + + case "CAPITALIZE_FIRST": + return NameCapitalize.FIRST; + } + return NameCapitalize.DO_NOTHING; + } + + public static String capitalizePass(String input, NameCapitalize capitalize){ + String dest = input; + switch (capitalize){ + case ALL: + dest = input.toUpperCase(Locale.ROOT); + break; + case FIRST: + dest = utils.toTitleCase(input, true); + break; + case DO_NOTHING: + default: + + } + return dest; + } +} diff --git a/src/it/reyboz/bustorino/backend/Notifications.java b/src/it/reyboz/bustorino/backend/Notifications.java --- a/src/it/reyboz/bustorino/backend/Notifications.java +++ b/src/it/reyboz/bustorino/backend/Notifications.java @@ -8,6 +8,7 @@ public class Notifications { public static final String DEFAULT_CHANNEL_ID ="Default"; + public static final String DB_UPDATE_CHANNELS_ID ="Database Update"; public static void createDefaultNotificationChannel(Context context) { // Create the NotificationChannel, but only on API 26+ because diff --git a/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java @@ -0,0 +1,48 @@ +package it.reyboz.bustorino.backend.gtfs; + +import org.osmdroid.util.GeoPoint; + +import java.util.ArrayList; + +public final class PolylineParser { + /** + * Decode a Google polyline + * Thanks to https://stackoverflow.com/questions/9341020/how-to-decode-googles-polyline-algorithm + * @param encodedPolyline the encoded polyline in a string + * @param initial_capacity for the list + * @return the list of points correspoding to the polyline + */ + public static ArrayList decodePolyline(String encodedPolyline, int initial_capacity) { + ArrayList points = new ArrayList<>(initial_capacity); + int truck = 0; + int carriage_q = 0; + int longit=0, latit=0; + boolean is_lat=true; + for (int x = 0, xx = encodedPolyline.length(); x < xx; ++x) { + int i = encodedPolyline.charAt(x); + i -= 63; + int _5_bits = i << (32 - 5) >>> (32 - 5); + truck |= _5_bits << carriage_q; + carriage_q += 5; + boolean is_last = (i & (1 << 5)) == 0; + if (is_last) { + boolean is_negative = (truck & 1) == 1; + truck >>>= 1; + if (is_negative) { + truck = ~truck; + } + if (is_lat){ + latit += truck; + is_lat = false; + } else{ + longit += truck; + points.add(new GeoPoint((double)latit/1e5,(double)longit/1e5)); + is_lat=true; + } + carriage_q = 0; + truck = 0; + } + } + return points; + } +} diff --git a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java --- a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -31,10 +31,7 @@ import org.json.JSONException; import org.json.JSONObject; -import java.nio.charset.StandardCharsets; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import it.reyboz.bustorino.backend.Fetcher; @@ -54,7 +51,7 @@ AtomicReference res, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { - super(MatoAPIFetcher.QueryType.ARRIVALS, listener, errorListener); + super(MatoQueries.QueryType.ARRIVALS, listener, errorListener); this.stopName = stopName; this.startingTime = startingTime; this.timeRange = timeRange; diff --git a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java --- a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java +++ b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java @@ -12,9 +12,9 @@ private static final String API_URL="https://mapi.5t.torino.it/routing/v1/routers/mat/index/graphql"; protected final Response.Listener listener; - private final MatoAPIFetcher.QueryType type; + protected final MatoQueries.QueryType type; public MapiVolleyRequest( - MatoAPIFetcher.QueryType type, + MatoQueries.QueryType type, Response.Listener listener, @Nullable Response.ErrorListener errorListener) { super(Method.POST, API_URL, errorListener); diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt --- a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -19,15 +19,23 @@ import android.content.Context import android.util.Log +import com.android.volley.DefaultRetryPolicy import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* +import it.reyboz.bustorino.data.gtfs.GtfsAgency +import it.reyboz.bustorino.data.gtfs.GtfsFeed +import it.reyboz.bustorino.data.gtfs.GtfsRoute +import it.reyboz.bustorino.data.gtfs.MatoPattern +import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference +import kotlin.collections.ArrayList open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher { @@ -58,7 +66,7 @@ return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue - request.setTag(getVolleyReqTag(QueryType.ARRIVALS)) + request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS)) requestQueue.add(request) try { @@ -105,10 +113,15 @@ "DNT" to "1", "Host" to "mapi.5t.torino.it") - fun getVolleyReqTag(type: QueryType): String{ + private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) + + fun getVolleyReqTag(type: MatoQueries.QueryType): String{ return when (type){ - QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" - QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" + MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" + MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" + MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds" + MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes" + MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute" } } @@ -120,14 +133,15 @@ val future = RequestFuture.newFuture>() val request = VolleyAllStopsRequest(future, future) - request.tag = getVolleyReqTag(QueryType.ALL_STOPS) + request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS) + request.retryPolicy = longRetryPolicy requestQueue.add(request) var palinaList:List = mutableListOf() try { - palinaList = future.get(60, TimeUnit.SECONDS) + palinaList = future.get(120, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { @@ -259,10 +273,159 @@ return data } - } - enum class QueryType { - ARRIVALS, ALL_STOPS + fun getFeedsAndAgencies(context: Context, res: AtomicReference?): + Pair, ArrayList> { + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + val future = RequestFuture.newFuture() + + val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future) + request.setRetryPolicy(longRetryPolicy) + request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS) + + requestQueue.add(request) + + val feeds = ArrayList() + val agencies = ArrayList() + var outObj = "" + try { + val resObj = future.get(120,TimeUnit.SECONDS) + outObj = resObj.toString(1) + val feedsJSON = resObj.getJSONArray("feeds") + for (i in 0 until feedsJSON.length()){ + val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i)) + feeds.add(resTup.first) + + agencies.addAll(resTup.second) + } + + + } catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } catch (e: JSONException){ + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + } + return Pair(feeds,agencies) + + } + fun getRoutes(context: Context, res: AtomicReference?): + ArrayList{ + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + val future = RequestFuture.newFuture() + + val params = JSONObject() + params.put("feeds","gtt") + + val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future) + request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES) + request.retryPolicy = longRetryPolicy + + requestQueue.add(request) + + val routes = ArrayList() + var outObj = "" + try { + val resObj = future.get(120,TimeUnit.SECONDS) + outObj = resObj.toString(1) + val routesJSON = resObj.getJSONArray("routes") + for (i in 0 until routesJSON.length()){ + val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i)) + routes.add(route) + } + + + } catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } catch (e: JSONException){ + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + } + return routes + + } + fun getPatternsWithStops(context: Context, routesGTFSIds: ArrayList, res: AtomicReference?): ArrayList{ + val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue + + val future = RequestFuture.newFuture() + + val params = JSONObject() + for (r in routesGTFSIds){ + if(r.isEmpty()) routesGTFSIds.remove(r) + } + val routes = JSONArray(routesGTFSIds) + + params.put("routes",routes) + + val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future) + request.retryPolicy = longRetryPolicy + request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES) + + requestQueue.add(request) + + val patterns = ArrayList() + //var outObj = "" + try { + val resObj = future.get(60,TimeUnit.SECONDS) + //outObj = resObj.toString(1) + val routesJSON = resObj.getJSONArray("routes") + for (i in 0 until routesJSON.length()){ + val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i)) + patterns.addAll(patternList) + } + + + } catch (e: InterruptedException) { + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + } catch (e: ExecutionException) { + e.printStackTrace() + res?.set(Fetcher.Result.SERVER_ERROR) + } catch (e: TimeoutException) { + res?.set(Fetcher.Result.CONNECTION_ERROR) + e.printStackTrace() + } catch (e: JSONException){ + e.printStackTrace() + res?.set(Fetcher.Result.PARSER_ERROR) + //Log.e(DEBUG_TAG, "Downloading feeds: $outObj") + } + /* + var numRequests = 0 + for(routeName in routesGTFSIds){ + if (!routeName.isEmpty()) numRequests++ + } + val countDownForRequests = CountDownLatch(numRequests) + val lockSave = ReentrantLock() + //val countDownFor + for (routeName in routesGTFSIds){ + val pars = JSONObject() + pars.put("") + + } + val goodResponseListener = Response.Listener { } + val errorResponseListener = Response.ErrorListener { } + */ + + return patterns + } + + } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt --- a/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoQueries.kt @@ -86,5 +86,81 @@ } } """ + + const val ALL_FEEDS=""" + query AllFeeds{ + feeds{ + feedId + agencies{ + gtfsId + name + url + fareUrl + phone + } + } + } + """ + + const val ROUTES_BY_FEED=""" + query AllRoutes(${'$'}feeds: [String]){ + routes(feeds: ${'$'}feeds) { + agency{ + gtfsId + } + gtfsId + shortName + longName + type + desc + color + textColor + } + } + """ + + const val ROUTES_WITH_PATTERNS=""" + query RoutesWithPatterns(${'$'}routes: [String]) { + routes(ids: ${'$'}routes) { + gtfsId + shortName + longName + type + + patterns{ + name + code + semanticHash + directionId + headsign + stops{ + gtfsId + lat + lon + } + patternGeometry{ + length + points + } + + } + } + } + """ + + fun getNameAndRequest(type: QueryType): Pair{ + return when (type){ + QueryType.FEEDS -> Pair("AllFeeds", ALL_FEEDS) + QueryType.ALL_STOPS -> Pair("AllStops", ALL_STOPS_BY_FEEDS) + QueryType.ARRIVALS -> Pair("AllStopsDirect", QUERY_ARRIVALS) + QueryType.ROUTES -> Pair("AllRoutes", ROUTES_BY_FEED) + QueryType.PATTERNS_FOR_ROUTES -> Pair("RoutesWithPatterns", ROUTES_WITH_PATTERNS) + } + } } + + enum class QueryType { + ARRIVALS, ALL_STOPS, FEEDS, ROUTES, PATTERNS_FOR_ROUTES + } + } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt b/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MatoVolleyJSONRequest.kt @@ -0,0 +1,48 @@ +package it.reyboz.bustorino.backend.mato + +import android.util.Log +import com.android.volley.NetworkResponse +import com.android.volley.Response +import com.android.volley.VolleyError +import com.android.volley.toolbox.HttpHeaderParser +import org.json.JSONException +import org.json.JSONObject + +class MatoVolleyJSONRequest(type: MatoQueries.QueryType, + val variables: JSONObject, + listener: Response.Listener, + errorListener: Response.ErrorListener?) + : MapiVolleyRequest(type, listener, errorListener) { + protected val requestName:String + protected val requestQuery:String + init { + val dd = MatoQueries.getNameAndRequest(type) + requestName = dd.first + requestQuery = dd.second + } + + override fun getBody(): ByteArray { + + val data = MatoAPIFetcher.makeRequestParameters(requestName, variables, requestQuery) + + return data.toString().toByteArray() + } + + override fun parseNetworkResponse(response: NetworkResponse?): Response { + if (response==null) + return Response.error(VolleyError("Null response")) + else if(response.statusCode != 200) + return Response.error(VolleyError("Response not ready, status "+response.statusCode)) + val obj:JSONObject + try { + obj = JSONObject(String(response.data)).getJSONObject("data") + }catch (ex: JSONException){ + Log.e("BusTO-VolleyJSON","Cannot parse response as JSON") + ex.printStackTrace() + return Response.error(VolleyError("Error parsing JSON")) + } + + return Response.success(obj, HttpHeaderParser.parseCacheHeaders(response)) + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt b/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/ResponseParsing.kt @@ -0,0 +1,120 @@ +/* + BusTO - Backend components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.backend.mato + +import it.reyboz.bustorino.data.gtfs.* +import org.json.JSONObject + +abstract class ResponseParsing{ + + companion object{ + fun parseAgencyJSON(jsonObject: JSONObject): GtfsAgency { + return GtfsAgency( + jsonObject.getString("gtfsId"), + jsonObject.getString("name"), + jsonObject.getString("url"), + jsonObject.getString("fareUrl"), + jsonObject.getString("phone"), + null + ) + } + + /** + * Parse a feed request json, containing the GTFS agencies it is served by + */ + fun parseFeedJSON(jsonObject: JSONObject): Pair> { + + val agencies = ArrayList() + val feed = GtfsFeed(jsonObject.getString("feedId")) + val oo = jsonObject.getJSONArray("agencies") + agencies.ensureCapacity(oo.length()) + for (i in 0 until oo.length()){ + val agObj = oo.getJSONObject(i) + + agencies.add( + GtfsAgency( + agObj.getString("gtfsId"), + agObj.getString("name"), + agObj.getString("url"), + agObj.getString("fareUrl"), + agObj.getString("phone"), + feed + ) + ) + } + return Pair(feed, agencies) + } + + fun parseRouteJSON(jsonObject: JSONObject): GtfsRoute { + + val agencyJSON = jsonObject.getJSONObject("agency") + val agencyId = agencyJSON.getString("gtfsId") + + + return GtfsRoute( + jsonObject.getString("gtfsId"), + agencyId, + jsonObject.getString("shortName"), + jsonObject.getString("longName"), + jsonObject.getString("desc"), + GtfsMode.getByValue(jsonObject.getInt("type"))!!, + jsonObject.getString("color"), + jsonObject.getString("textColor") + + ) + } + + /** + * Parse a route pattern from the JSON response of the MaTO server + */ + fun parseRoutePatternsStopsJSON(jsonObject: JSONObject) : ArrayList{ + val routeGtfsId = jsonObject.getString("gtfsId") + + val patternsJSON = jsonObject.getJSONArray("patterns") + val patternsOut = ArrayList(patternsJSON.length()) + var mPatternJSON: JSONObject + for(i in 0 until patternsJSON.length()){ + mPatternJSON = patternsJSON.getJSONObject(i) + + val stopsJSON = mPatternJSON.getJSONArray("stops") + + val stopsCodes = ArrayList(stopsJSON.length()) + for(k in 0 until stopsJSON.length()){ + stopsCodes.add( + stopsJSON.getJSONObject(k).getString("gtfsId") + ) + } + + val geometry = mPatternJSON.getJSONObject("patternGeometry") + val numGeo = geometry.getInt("length") + val polyline = geometry.getString("points") + + patternsOut.add( + MatoPattern( + mPatternJSON.getString("name"), mPatternJSON.getString("code"), + mPatternJSON.getString("semanticHash"), mPatternJSON.getInt("directionId"), + routeGtfsId,mPatternJSON.getString("headsign"), polyline, numGeo, stopsCodes + ) + ) + } + return patternsOut + } + + + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt --- a/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt +++ b/src/it/reyboz/bustorino/backend/mato/VolleyAllStopsRequest.kt @@ -31,7 +31,7 @@ listener: Response.Listener>, errorListener: Response.ErrorListener, ) : MapiVolleyRequest>( - MatoAPIFetcher.QueryType.ALL_STOPS,listener, errorListener) { + MatoQueries.QueryType.ALL_STOPS,listener, errorListener) { private val FEEDS = JSONArray() init { @@ -73,7 +73,7 @@ return Response.success(palinas, HttpHeaderParser.parseCacheHeaders(response)) } companion object{ - val FEEDS_STR = arrayOf("gtt") + //val FEEDS_STR = arrayOf("gtt") } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/DBUpdateWorker.java b/src/it/reyboz/bustorino/data/DBUpdateWorker.java --- a/src/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/src/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -47,13 +47,13 @@ public static final int SUCCESS_NO_ACTION_NEEDED = 9; public static final int SUCCESS_UPDATE_DONE = 1; - private final int notifi_ID=62341; + private final static int NOTIFIC_ID =32198; public static final String FORCED_UPDATE = "FORCED-UPDATE"; public static final String DEBUG_TAG = "Busto-UpdateWorker"; - private static final long UPDATE_MIN_DELAY= 3*7*24*3600; //3 weeks + private static final long UPDATE_MIN_DELAY= 9*24*3600; //9 days public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { @@ -66,7 +66,14 @@ public Result doWork() { //register Notification channel final Context con = getApplicationContext(); - Notifications.createDefaultNotificationChannel(con); + //Notifications.createDefaultNotificationChannel(con); + //Use the new notification channels + Notifications.createNotificationChannel(con,con.getString(R.string.database_notification_channel), + con.getString(R.string.database_notification_channel_desc), NotificationManagerCompat.IMPORTANCE_LOW, + Notifications.DB_UPDATE_CHANNELS_ID + ); + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); + final int notification_ID = 32198; final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); @@ -77,7 +84,17 @@ final long lastDBUpdateTime = shPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, 0); long currentTime = System.currentTimeMillis()/1000; - final int notificationID = showNotification(); + //showNotification(notificationManager, notification_ID); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(con, + Notifications.DB_UPDATE_CHANNELS_ID) + .setContentTitle(con.getString(R.string.database_update_msg_notif)) + .setProgress(0,0,true) + .setPriority(NotificationCompat.PRIORITY_LOW); + builder.setSmallIcon(R.drawable.ic_bus_orange); + + + notificationManager.notify(notification_ID,builder.build()); + Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); /* @@ -95,7 +112,7 @@ if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) && !isUpdateCompulsory) { //don't need to update - cancelNotification(notificationID); + cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } @@ -106,8 +123,7 @@ DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); if (resultUpdate != DatabaseUpdate.Result.DONE){ - Fetcher.Result result = resultAtomicReference.get(); - + //Fetcher.Result result = resultAtomicReference.get(); final Data.Builder dataBuilder = new Data.Builder(); switch (resultUpdate){ case ERROR_STOPS_DOWNLOAD: @@ -117,7 +133,7 @@ dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); break; } - cancelNotification(notificationID); + cancelNotification(notification_ID); return ListenableWorker.Result.failure(dataBuilder.build()); } Log.d(DEBUG_TAG, "Update finished successfully!"); @@ -127,7 +143,7 @@ currentTime = System.currentTimeMillis()/1000; editor.putLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, currentTime); editor.apply(); - cancelNotification(notificationID); + cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); } @@ -144,19 +160,21 @@ .build(); } - - private int showNotification(){ - final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), Notifications.DEFAULT_CHANNEL_ID) + /* + private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, + final String channel_ID){ + final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); - final NotificationManagerCompat notifcManager = NotificationManagerCompat.from(getApplicationContext()); - final int notification_ID = 32198; - notifcManager.notify(notification_ID,builder.build()); + + + notificManager.notify(notification_ID,builder.build()); return notification_ID; } + */ private void cancelNotification(int notificationID){ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); diff --git a/src/it/reyboz/bustorino/data/DatabaseUpdate.java b/src/it/reyboz/bustorino/data/DatabaseUpdate.java --- a/src/it/reyboz/bustorino/data/DatabaseUpdate.java +++ b/src/it/reyboz/bustorino/data/DatabaseUpdate.java @@ -22,22 +22,30 @@ import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; -import androidx.core.content.ContextCompat; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.Observer; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTAPIFetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; -import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; +import it.reyboz.bustorino.data.gtfs.GtfsAgency; +import it.reyboz.bustorino.data.gtfs.GtfsDatabase; +import it.reyboz.bustorino.data.gtfs.GtfsDBDao; +import it.reyboz.bustorino.data.gtfs.GtfsFeed; +import it.reyboz.bustorino.data.gtfs.GtfsRoute; +import it.reyboz.bustorino.data.gtfs.MatoPattern; +import it.reyboz.bustorino.data.gtfs.PatternStop; +import kotlin.Pair; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -59,130 +67,188 @@ DONE, ERROR_STOPS_DOWNLOAD, ERROR_LINES_DOWNLOAD } - /** - * Request the server the version of the database - * @return the version of the DB, or an error code - */ - public static int getNewVersion(){ - AtomicReference gres = new AtomicReference<>(); - String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); - if(networkRequest == null){ - return VERSION_UNAVAILABLE; - } + /** + * Request the server the version of the database + * @return the version of the DB, or an error code + */ + public static int getNewVersion(){ + AtomicReference gres = new AtomicReference<>(); + String networkRequest = FiveTAPIFetcher.performAPIRequest(FiveTAPIFetcher.QueryType.STOPS_VERSION,null,gres); + if(networkRequest == null){ + return VERSION_UNAVAILABLE; + } - try { - JSONObject resp = new JSONObject(networkRequest); - return resp.getInt("id"); - } catch (JSONException e) { - e.printStackTrace(); - Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); - return JSON_PARSING_ERROR; - } + try { + JSONObject resp = new JSONObject(networkRequest); + return resp.getInt("id"); + } catch (JSONException e) { + e.printStackTrace(); + Log.e(DEBUG_TAG,"Error: wrong JSON response\nResponse:\t"+networkRequest); + return JSON_PARSING_ERROR; } - /** - * Run the DB Update - * @param con a context - * @param gres a result reference - * @return result of the update - */ - public static Result performDBUpdate(Context con, AtomicReference gres) { + } + private static boolean updateGTFSAgencies(Context con, AtomicReference res){ - final FiveTAPIFetcher f = new FiveTAPIFetcher(); - /* - final ArrayList stops = f.getAllStopsFromGTT(gres); - //final ArrayList cpOp = new ArrayList<>(); + final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); - if (gres.get() != Fetcher.Result.OK) { - Log.w(DEBUG_TAG, "Something went wrong downloading"); - return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + final Pair, ArrayList> respair = MatoAPIFetcher.Companion.getFeedsAndAgencies( + con, res + ); - } + dao.insertAgenciesWithFeeds(respair.getFirst(), respair.getSecond()); - */ - final NextGenDB dbHelp = new NextGenDB(con.getApplicationContext()); - final SQLiteDatabase db = dbHelp.getWritableDatabase(); + return true; + } + private static boolean updateGTFSRoutes(Context con, AtomicReference res){ - final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); - if (gres.get() != Fetcher.Result.OK) { - Log.w(DEBUG_TAG, "Something went wrong downloading"); - return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + final GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(con).gtfsDao(); - } - //TODO: Get the type of stop from the lines - //Empty the needed tables - db.beginTransaction(); - //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); - //db.delete(LinesTable.TABLE_NAME,null,null); - - //put new data - long startTime = System.currentTimeMillis(); - - Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); - for (final Palina p : palinasMatoAPI) { - final ContentValues cv = new ContentValues(); - - cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); - cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); - if (p.location != null) - cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); - cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); - cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); - if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); - cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, p.routesThatStopHereToString()); - if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); - if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); - //Log.d(DEBUG_TAG,cv.toString()); - //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); - //valuesArr[i] = cv; - db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); + final List routes= MatoAPIFetcher.Companion.getRoutes( + con, res + ); + dao.insertRoutes(routes); + if(res.get()!= Fetcher.Result.OK){ + return false; + } + final ArrayList gtfsRoutesIDs = new ArrayList<>(routes.size()); + for(GtfsRoute r: routes){ + gtfsRoutesIDs.add(r.getGtfsId()); + } + long t0 = System.currentTimeMillis(); + final ArrayList patterns = MatoAPIFetcher.Companion.getPatternsWithStops(con,gtfsRoutesIDs,res); + long tend = System.currentTimeMillis() - t0; + Log.d(DEBUG_TAG, "Downloaded patterns in "+tend+" ms"); + if(res.get()!=Fetcher.Result.OK){ + Log.e(DEBUG_TAG, "Something went wrong downloading patterns"); + return false; + } + final ArrayList patternStops = new ArrayList<>(patterns.size()); + for(MatoPattern p: patterns){ + final ArrayList stopsIDs = p.getStopsGtfsIDs(); + for (int i=0; i routes = f.getAllLinesFromGTT(gres); + return true; + } - if (routes == null) { - Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); - dbHelp.close(); - return DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD; - } + /** + * Run the DB Update + * @param con a context + * @param gres a result reference + * @return result of the update + */ + public static Result performDBUpdate(Context con, AtomicReference gres) { - db.beginTransaction(); - startTime = System.currentTimeMillis(); - for (Route r : routes) { - final ContentValues cv = new ContentValues(); - cv.put(NextGenDB.Contract.LinesTable.COLUMN_NAME, r.getName()); - switch (r.type) { - case BUS: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "URBANO"); - break; - case RAILWAY: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "FERROVIA"); - break; - case LONG_DISTANCE_BUS: - cv.put(NextGenDB.Contract.LinesTable.COLUMN_TYPE, "EXTRA"); - break; - } - cv.put(NextGenDB.Contract.LinesTable.COLUMN_DESCRIPTION, r.description); - - //db.insert(LinesTable.TABLE_NAME,null,cv); - int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); - if (rows < 1) { //we haven't changed anything - db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - endTime = System.currentTimeMillis(); - Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); + final FiveTAPIFetcher f = new FiveTAPIFetcher(); + + final NextGenDB dbHelp = new NextGenDB(con.getApplicationContext()); + final SQLiteDatabase db = dbHelp.getWritableDatabase(); + + final List palinasMatoAPI = MatoAPIFetcher.Companion.getAllStopsGTT(con, gres); + if (gres.get() != Fetcher.Result.OK) { + Log.w(DEBUG_TAG, "Something went wrong downloading"); + return DatabaseUpdate.Result.ERROR_STOPS_DOWNLOAD; + + } + //TODO: Get the type of stop from the lines + //Empty the needed tables + db.beginTransaction(); + //db.execSQL("DELETE FROM "+StopsTable.TABLE_NAME); + //db.delete(LinesTable.TABLE_NAME,null,null); + + //put new data + long startTime = System.currentTimeMillis(); + + Log.d(DEBUG_TAG, "Inserting " + palinasMatoAPI.size() + " stops"); + for (final Palina p : palinasMatoAPI) { + final ContentValues cv = new ContentValues(); + + cv.put(NextGenDB.Contract.StopsTable.COL_ID, p.ID); + cv.put(NextGenDB.Contract.StopsTable.COL_NAME, p.getStopDefaultName()); + if (p.location != null) + cv.put(NextGenDB.Contract.StopsTable.COL_LOCATION, p.location); + cv.put(NextGenDB.Contract.StopsTable.COL_LAT, p.getLatitude()); + cv.put(NextGenDB.Contract.StopsTable.COL_LONG, p.getLongitude()); + if (p.getAbsurdGTTPlaceName() != null) cv.put(NextGenDB.Contract.StopsTable.COL_PLACE, p.getAbsurdGTTPlaceName()); + cv.put(NextGenDB.Contract.StopsTable.COL_LINES_STOPPING, p.routesThatStopHereToString()); + if (p.type != null) cv.put(NextGenDB.Contract.StopsTable.COL_TYPE, p.type.getCode()); + if (p.gtfsID != null) cv.put(NextGenDB.Contract.StopsTable.COL_GTFS_ID, p.gtfsID); + //Log.d(DEBUG_TAG,cv.toString()); + //cpOp.add(ContentProviderOperation.newInsert(uritobeused).withValues(cv).build()); + //valuesArr[i] = cv; + db.replace(NextGenDB.Contract.StopsTable.TABLE_NAME, null, cv); + + } + db.setTransactionSuccessful(); + db.endTransaction(); + long endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting stops took: " + ((double) (endTime - startTime) / 1000) + " s"); + + // GTFS data fetching + AtomicReference gtfsRes = new AtomicReference<>(Fetcher.Result.OK); + updateGTFSAgencies(con, gtfsRes); + if (gtfsRes.get()!= Fetcher.Result.OK){ + Log.w(DEBUG_TAG, "Could not insert the feeds and agencies stuff"); + } else{ + Log.d(DEBUG_TAG, "Done downloading agencies"); + } + gtfsRes.set(Fetcher.Result.OK); + updateGTFSRoutes(con,gtfsRes); + if (gtfsRes.get()!= Fetcher.Result.OK){ + Log.w(DEBUG_TAG, "Could not insert the routes into DB"); + } else{ + Log.d(DEBUG_TAG, "Done downloading routes from MaTO"); + } + /* + final ArrayList routes = f.getAllLinesFromGTT(gres); + + if (routes == null) { + Log.w(DEBUG_TAG, "Something went wrong downloading the lines"); dbHelp.close(); + return DatabaseUpdate.Result.ERROR_LINES_DOWNLOAD; + + } + + 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); - return DatabaseUpdate.Result.DONE; + //db.insert(LinesTable.TABLE_NAME,null,cv); + int rows = db.update(NextGenDB.Contract.LinesTable.TABLE_NAME, cv, NextGenDB.Contract.LinesTable.COLUMN_NAME + " = ?", new String[]{r.getName()}); + if (rows < 1) { //we haven't changed anything + db.insert(NextGenDB.Contract.LinesTable.TABLE_NAME, null, cv); + } } + db.setTransactionSuccessful(); + db.endTransaction(); + endTime = System.currentTimeMillis(); + Log.d(DEBUG_TAG, "Inserting lines took: " + ((double) (endTime - startTime) / 1000) + " s"); + + */ + dbHelp.close(); + + return DatabaseUpdate.Result.DONE; + } public static boolean setDBUpdatingFlag(Context con, boolean value){ final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); @@ -203,7 +269,7 @@ final SharedPreferences theShPr = PreferencesHolder.getMainSharedPreferences(con); final WorkManager workManager = WorkManager.getInstance(con); PeriodicWorkRequest wr = new PeriodicWorkRequest.Builder(DBUpdateWorker.class, 7, TimeUnit.DAYS) - .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .build()) .build(); @@ -221,4 +287,12 @@ TODO } */ + + public static void watchUpdateWorkStatus(Context context, @NonNull LifecycleOwner lifecycleOwner, + @NonNull Observer> observer) { + WorkManager workManager = WorkManager.getInstance(context); + workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe( + lifecycleOwner, observer + ); + } } diff --git a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt --- a/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt +++ b/src/it/reyboz/bustorino/data/gtfs/CsvTableInserter.kt @@ -19,13 +19,12 @@ import android.content.Context import android.util.Log -import java.util.ArrayList class CsvTableInserter( val tableName: String, context: Context ) { private val database: GtfsDatabase = GtfsDatabase.getGtfsDatabase(context) - private val dao: StaticGtfsDao = database.gtfsDao() + private val databaseDao: GtfsDBDao = database.gtfsDao() private val elementsList: MutableList< in GtfsTable> = mutableListOf() @@ -35,12 +34,12 @@ private var countInsert = 0 init { if(tableName == "stop_times") { - stopsIDsPresent = dao.getAllStopsIDs().toHashSet() - tripsIDsPresent = dao.getAllTripsIDs().toHashSet() + stopsIDsPresent = databaseDao.getAllStopsIDs().toHashSet() + tripsIDsPresent = databaseDao.getAllTripsIDs().toHashSet() Log.d(DEBUG_TAG, "num stop IDs present: "+ stopsIDsPresent!!.size) Log.d(DEBUG_TAG, "num trips IDs present: "+ tripsIDsPresent!!.size) } else if(tableName == "routes"){ - dao.deleteAllRoutes() + databaseDao.deleteAllRoutes() } } @@ -77,7 +76,7 @@ //have to insert if (tableName == "routes") - dao.insertRoutes(elementsList.filterIsInstance()) + databaseDao.insertRoutes(elementsList.filterIsInstance()) else insertDataInDatabase() @@ -90,21 +89,21 @@ countInsert += elementsList.size when(tableName){ "stops" -> { - dao.insertStops(elementsList.filterIsInstance()) + databaseDao.insertStops(elementsList.filterIsInstance()) } - "routes" -> dao.insertRoutes(elementsList.filterIsInstance()) - "calendar" -> dao.insertServices(elementsList.filterIsInstance()) - "calendar_dates" -> dao.insertDates(elementsList.filterIsInstance()) - "trips" -> dao.insertTrips(elementsList.filterIsInstance()) - "stop_times"-> dao.insertStopTimes(elementsList.filterIsInstance()) - "shapes" -> dao.insertShapes(elementsList.filterIsInstance()) + "routes" -> databaseDao.insertRoutes(elementsList.filterIsInstance()) + "calendar" -> databaseDao.insertServices(elementsList.filterIsInstance()) + "calendar_dates" -> databaseDao.insertDates(elementsList.filterIsInstance()) + "trips" -> databaseDao.insertTrips(elementsList.filterIsInstance()) + "stop_times"-> databaseDao.insertStopTimes(elementsList.filterIsInstance()) + "shapes" -> databaseDao.insertShapes(elementsList.filterIsInstance()) } ///if(elementsList.size < MAX_ELEMENTS) } fun finishInsert(){ insertDataInDatabase() - Log.d(DEBUG_TAG, "Inserted "+countInsert+" elements from "+tableName); + Log.d(DEBUG_TAG, "Inserted $countInsert elements from $tableName") } companion object{ diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsAgency.kt @@ -0,0 +1,55 @@ +package it.reyboz.bustorino.data.gtfs + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = GtfsAgency.TABLE_NAME) +data class GtfsAgency( + @PrimaryKey + @ColumnInfo(name = COL_GTFS_ID) + val gtfsId: String, + @ColumnInfo(name = COL_NAME) + val name: String, + @ColumnInfo(name = COL_URL) + val url: String, + @ColumnInfo(name = COL_FAREURL) + val fareUrl: String?, + @ColumnInfo(name = COL_PHONE) + val phone: String?, + @Embedded var feed: GtfsFeed? +): GtfsTable{ + constructor(valuesByColumn: Map) : this( + valuesByColumn[COL_GTFS_ID]!!, + valuesByColumn[COL_NAME]!!, + valuesByColumn[COL_URL]!!, + valuesByColumn[COL_FAREURL], + valuesByColumn[COL_PHONE], + null + ) + + companion object{ + const val TABLE_NAME="gtfs_agencies" + + const val COL_GTFS_ID="gtfs_id" + const val COL_NAME="ag_name" + const val COL_URL="ag_url" + const val COL_FAREURL = "fare_url" + const val COL_PHONE = "phone" + + val COLUMNS = arrayOf( + COL_GTFS_ID, + COL_NAME, + COL_URL, + COL_FAREURL, + COL_PHONE + ) + const val CREATE_SQL = + "CREATE TABLE $TABLE_NAME ( $COL_GTFS_ID )" + } + + override fun getColumns(): Array { + return COLUMNS + } +} diff --git a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt rename from src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt rename to src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt --- a/src/it/reyboz/bustorino/data/gtfs/StaticGtfsDao.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDBDao.kt @@ -21,8 +21,8 @@ import androidx.room.* @Dao -interface StaticGtfsDao { - @Query("SELECT * FROM "+GtfsRoute.DB_TABLE+" ORDER BY "+GtfsRoute.COL_SORT_ORDER) +interface GtfsDBDao { + @Query("SELECT * FROM "+GtfsRoute.DB_TABLE) fun getAllRoutes() : LiveData> @Query("SELECT "+GtfsTrip.COL_TRIP_ID+" FROM "+GtfsTrip.DB_TABLE) @@ -45,9 +45,9 @@ deleteAllRoutes() insertRoutes(routes) } - + @Transaction @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertRoutes(users: List) + fun insertRoutes(routes: List) @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertStops(stops: List) @Insert(onConflict = OnConflictStrategy.REPLACE) @@ -87,4 +87,21 @@ @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) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertPatterns(patterns: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertPatternStops(patternStops: List) } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt --- a/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsDatabase.kt @@ -18,24 +18,30 @@ package it.reyboz.bustorino.data.gtfs import android.content.Context +import android.util.Log import androidx.room.* +import androidx.room.migration.Migration @Database( entities = [ + GtfsFeed::class, + GtfsAgency::class, GtfsServiceDate::class, GtfsStop::class, GtfsService::class, GtfsRoute::class, GtfsStopTime::class, GtfsTrip::class, - GtfsShape::class], + GtfsShape::class, + MatoPattern::class, + PatternStop::class + ], version = GtfsDatabase.VERSION, - exportSchema = false, ) @TypeConverters(Converters::class) -public abstract class GtfsDatabase : RoomDatabase() { +abstract class GtfsDatabase : RoomDatabase() { - abstract fun gtfsDao() : StaticGtfsDao + abstract fun gtfsDao() : GtfsDBDao companion object{ @Volatile @@ -44,14 +50,36 @@ fun getGtfsDatabase(context: Context): GtfsDatabase{ return INSTANCE ?: synchronized(this){ val instance = Room.databaseBuilder(context.applicationContext, - GtfsDatabase::class.java, - "gtfs_database").build() + GtfsDatabase::class.java, + "gtfs_database") + .addMigrations(MIGRATION_1_2) + .build() INSTANCE = instance instance } } - const val VERSION = 1 + const val VERSION = 2 const val FOREIGNKEY_ONDELETE = ForeignKey.CASCADE + + val MIGRATION_1_2 = Migration(1,2) { + Log.d("BusTO-Database", "Upgrading from version 1 to version 2 the Room Database") + //create table for feeds + it.execSQL("CREATE TABLE IF NOT EXISTS `gtfs_feeds` (`feed_id` TEXT NOT NULL, PRIMARY KEY(`feed_id`))") + //create table gtfs_agencies + it.execSQL("CREATE TABLE IF NOT EXISTS `gtfs_agencies` (`gtfs_id` TEXT NOT NULL, `ag_name` TEXT NOT NULL, `ag_url` TEXT NOT NULL, `fare_url` TEXT, `phone` TEXT, `feed_id` TEXT, PRIMARY KEY(`gtfs_id`))") + + //recreate routes + it.execSQL("DROP TABLE IF EXISTS `routes_table`") + it.execSQL("CREATE TABLE IF NOT EXISTS `routes_table` (`route_id` TEXT NOT NULL, `agency_id` TEXT NOT NULL, `route_short_name` TEXT NOT NULL, `route_long_name` TEXT NOT NULL, `route_desc` TEXT NOT NULL, `route_mode` TEXT NOT NULL, `route_color` TEXT NOT NULL, `route_text_color` TEXT NOT NULL, PRIMARY KEY(`route_id`))") + + //create patterns and stops + it.execSQL("CREATE TABLE IF NOT EXISTS `mato_patterns` (`pattern_name` TEXT NOT NULL, `pattern_code` TEXT NOT NULL, `pattern_hash` TEXT NOT NULL, `pattern_direction_id` INTEGER NOT NULL, `pattern_route_id` TEXT NOT NULL, `pattern_headsign` TEXT, `pattern_polyline` TEXT NOT NULL, `pattern_polylength` INTEGER NOT NULL, PRIMARY KEY(`pattern_code`), FOREIGN KEY(`pattern_route_id`) REFERENCES `routes_table`(`route_id`) ON UPDATE NO ACTION ON DELETE CASCADE )") + it.execSQL("CREATE TABLE IF NOT EXISTS `patterns_stops` (`pattern_gtfs_id` TEXT NOT NULL, `stop_gtfs_id` TEXT NOT NULL, `stop_order` INTEGER NOT NULL, PRIMARY KEY(`pattern_gtfs_id`, `stop_gtfs_id`, `stop_order`), FOREIGN KEY(`pattern_gtfs_id`) REFERENCES `mato_patterns`(`pattern_code`) ON UPDATE NO ACTION ON DELETE CASCADE )") + + + } + + } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsFeed.kt @@ -0,0 +1,50 @@ +/* + BusTO - Data components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.data.gtfs + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = GtfsFeed.TABLE_NAME) +data class GtfsFeed( + @PrimaryKey + @ColumnInfo(name = COL_GTFS_ID) + val gtfsId: String, +): GtfsTable{ + constructor(valuesByColumn: Map) : this( + valuesByColumn[COL_GTFS_ID]!!, + ) + + companion object{ + const val TABLE_NAME="gtfs_feeds" + + const val COL_GTFS_ID="feed_id" + + + val COLUMNS = arrayOf( + COL_GTFS_ID, + ) + const val CREATE_SQL = + "CREATE TABLE $TABLE_NAME ( $COL_GTFS_ID )" + } + + override fun getColumns(): Array { + return COLUMNS + } +} diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsMode.kt @@ -0,0 +1,19 @@ +package it.reyboz.bustorino.data.gtfs + +enum class GtfsMode(val intType: Int) { + TRAM(0), + SUBWAY(1), + RAIL(2), + BUS(3), + FERRY(4), + CABLE_TRAM(5), + GONDOLA(6), + FUNICULAR(7), + TROLLEYBUS(11), + MONORAIL(12); + + companion object { + private val VALUES = values() + fun getByValue(value: Int) = VALUES.firstOrNull { it.intType == value } + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt --- a/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt +++ b/src/it/reyboz/bustorino/data/gtfs/GtfsRoute.kt @@ -23,26 +23,25 @@ @Entity(tableName=GtfsRoute.DB_TABLE) data class GtfsRoute( - @PrimaryKey @ColumnInfo(name = COL_ROUTE_ID) - val ID: String, - @ColumnInfo(name = "agency_id") + @PrimaryKey @ColumnInfo(name = COL_ROUTE_ID) + val gtfsId: String, + @ColumnInfo(name = "agency_id") val agencyID: String, - @ColumnInfo(name = "route_short_name") + @ColumnInfo(name = "route_short_name") val shortName: String, - @ColumnInfo(name = "route_long_name") + @ColumnInfo(name = "route_long_name") val longName: String, - @ColumnInfo(name = "route_desc") + @ColumnInfo(name = "route_desc") val description: String, - @ColumnInfo(name ="route_type") - val type: String, + @ColumnInfo(name = COL_MODE) + val mode: GtfsMode, //@ColumnInfo(name ="route_url") //val url: String, - @ColumnInfo(name ="route_color") + @ColumnInfo(name = COL_COLOR) val color: String, - @ColumnInfo(name ="route_text_color") + @ColumnInfo(name = COL_TEXT_COLOR) val textColor: String, - @ColumnInfo(name = COL_SORT_ORDER) - val sortOrder: Int + ): GtfsTable { constructor(valuesByColumn: Map) : this( @@ -51,15 +50,17 @@ valuesByColumn["route_short_name"]!!, valuesByColumn["route_long_name"]!!, valuesByColumn["route_desc"]!!, - valuesByColumn["route_type"]!!, - valuesByColumn["route_color"]!!, - valuesByColumn["route_text_color"]!!, - valuesByColumn[COL_SORT_ORDER]?.toInt()!! + valuesByColumn["route_type"]?.toInt()?.let { GtfsMode.getByValue(it) }!!, + valuesByColumn[COL_COLOR]!!, + valuesByColumn[COL_TEXT_COLOR]!!, ) companion object { const val DB_TABLE: String="routes_table" const val COL_SORT_ORDER: String="route_sort_order" const val COL_ROUTE_ID = "route_id" + const val COL_MODE ="route_mode" + const val COL_COLOR="route_color" + const val COL_TEXT_COLOR="route_text_color" val COLUMNS = arrayOf(COL_ROUTE_ID, "agency_id", @@ -71,6 +72,8 @@ "route_text_color", COL_SORT_ORDER ) + + //const val CREATE_SQL = "" } override fun getColumns(): Array { diff --git a/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/data/gtfs/MatoPattern.kt @@ -0,0 +1,123 @@ +/* + BusTO - Data components + Copyright (C) 2022 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.data.gtfs + +import androidx.room.* +import it.reyboz.bustorino.backend.Stop + +@Entity(tableName = MatoPattern.TABLE_NAME, + foreignKeys = [ + ForeignKey(entity = GtfsRoute::class, + parentColumns = [GtfsRoute.COL_ROUTE_ID], + childColumns = [MatoPattern.COL_ROUTE_ID], + onDelete = ForeignKey.CASCADE, + ) + ] +) +data class MatoPattern( + @ColumnInfo(name= COL_NAME) + val name: String, + @ColumnInfo(name= COL_CODE) + @PrimaryKey + val code: String, + @ColumnInfo(name= COL_SEMANTIC_HASH) + val semanticHash: String, + @ColumnInfo(name= COL_DIRECTION_ID) + val directionId: Int, + @ColumnInfo(name= COL_ROUTE_ID) + val routeGtfsId: String, + @ColumnInfo(name= COL_HEADSIGN) + var headsign: String?, + @ColumnInfo(name= COL_GEOMETRY_POLY) + val patternGeometryPoly: String, + @ColumnInfo(name= COL_GEOMETRY_LENGTH) + val patternGeometryLength: Int, + @Ignore + val stopsGtfsIDs: ArrayList + +):GtfsTable{ + + @Ignore + val servingStops= ArrayList(4) + constructor( + name: String, code:String, + semanticHash: String, directionId: Int, + routeGtfsId: String, headsign: String?, + patternGeometryPoly: String, patternGeometryLength: Int + ): this(name, code, semanticHash, directionId, routeGtfsId, headsign, patternGeometryPoly, patternGeometryLength, ArrayList(4)) + + companion object{ + const val TABLE_NAME="mato_patterns" + + const val COL_NAME="pattern_name" + const val COL_CODE="pattern_code" + const val COL_ROUTE_ID="pattern_route_id" + const val COL_SEMANTIC_HASH="pattern_hash" + const val COL_DIRECTION_ID="pattern_direction_id" + const val COL_HEADSIGN="pattern_headsign" + const val COL_GEOMETRY_POLY="pattern_polyline" + const val COL_GEOMETRY_LENGTH="pattern_polylength" + + val COLUMNS = arrayOf( + COL_NAME, + COL_CODE, + COL_ROUTE_ID, + COL_SEMANTIC_HASH, + COL_DIRECTION_ID, + COL_HEADSIGN, + COL_GEOMETRY_POLY, + COL_GEOMETRY_LENGTH + ) + } + override fun getColumns(): Array { + return COLUMNS + } +} + +//DO NOT USE EMBEDDED!!! -> copies all data + +@Entity(tableName=PatternStop.TABLE_NAME, + primaryKeys = [ + PatternStop.COL_PATTERN_ID, + PatternStop.COL_STOP_GTFS, + PatternStop.COL_ORDER + ], + foreignKeys = [ + ForeignKey(entity = MatoPattern::class, + parentColumns = [MatoPattern.COL_CODE], + childColumns = [PatternStop.COL_PATTERN_ID], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class PatternStop( + @ColumnInfo(name= COL_PATTERN_ID) + val patternId: String, + @ColumnInfo(name=COL_STOP_GTFS) + val stopGtfsId: String, + @ColumnInfo(name=COL_ORDER) + val order: Int, +){ + companion object{ + const val TABLE_NAME="patterns_stops" + + const val COL_PATTERN_ID="pattern_gtfs_id" + const val COL_STOP_GTFS="stop_gtfs_id" + const val COL_ORDER="stop_order" + } +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java --- a/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/src/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; @@ -36,6 +37,8 @@ import androidx.appcompat.widget.AppCompatButton; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.work.WorkInfo; + import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -49,6 +52,8 @@ import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType; +import it.reyboz.bustorino.backend.mato.MapiArrivalRequest; +import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB.Contract.*; @@ -78,7 +83,6 @@ private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; - boolean canStartDBQuery = true; private Location lastReceivedLocation = null; private ProgressBar circlingProgressBar,flatProgressBar; private int distance; @@ -100,6 +104,10 @@ private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; + private boolean dbUpdateRunning = false; + + private ArrayList currentNearbyStops = new ArrayList<>(); + public NearbyStopsFragment() { // Required empty public constructor } @@ -129,10 +137,10 @@ } locManager = AppLocationManager.getInstance(getContext()); fragmentLocationListener = new FragmentLocationListener(this); - globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences),Context.MODE_PRIVATE); - - - globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + if (getContext()!=null) { + globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); + globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); + } } @@ -153,27 +161,29 @@ titleTextView = root.findViewById(R.id.titleTextView); switchButton = root.findViewById(R.id.switchButton); - preferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { + scrollListener = new CommonScrollListener(mListener,false); + switchButton.setOnClickListener(v -> switchFragmentType()); + Log.d(DEBUG_TAG, "onCreateView"); + + DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Log.d(DEBUG_TAG,"Key "+key+" was changed"); - if(key.equals(getString(R.string.databaseUpdatingPref))){ - if(!sharedPreferences.getBoolean(getString(R.string.databaseUpdatingPref),true)){ - canStartDBQuery = true; - Log.d(DEBUG_TAG,"The database has finished updating, can start update now"); - } + public void onChanged(List workInfos) { + if(workInfos.isEmpty()) return; + + WorkInfo wi = workInfos.get(0); + if (wi.getState() == WorkInfo.State.RUNNING && locManager.isRequesterRegistered(fragmentLocationListener)) { + locManager.removeLocationRequestFor(fragmentLocationListener); + dbUpdateRunning = true; + } else if(!locManager.isRequesterRegistered(fragmentLocationListener)){ + locManager.addLocationRequestFor(fragmentLocationListener); + dbUpdateRunning = false; } } - }; - scrollListener = new CommonScrollListener(mListener,false); - switchButton.setOnClickListener(v -> { - switchFragmentType(); }); - Log.d(DEBUG_TAG, "onCreateView"); return root; } - protected ArrayList createStopListFromCursor(Cursor data){ + 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); @@ -220,7 +230,7 @@ @Override - public void onAttach(Context context) { + public void onAttach(@NonNull Context context) { super.onAttach(context); /// TODO: RISOLVERE PROBLEMA: il context qui e' l'Activity non il Fragment if (context instanceof FragmentListenerMain) { @@ -235,7 +245,6 @@ @Override public void onPause() { super.onPause(); - canStartDBQuery = false; gridRecyclerView.setAdapter(null); locManager.removeLocationRequestFor(fragmentLocationListener); @@ -245,9 +254,9 @@ @Override public void onResume() { super.onResume(); - canStartDBQuery = !globalSharedPref.getBoolean(getString(R.string.databaseUpdatingPref),false); try{ - if(canStartDBQuery) locManager.addLocationRequestFor(fragmentLocationListener); + if(!dbUpdateRunning && !locManager.isRequesterRegistered(fragmentLocationListener)) + locManager.addLocationRequestFor(fragmentLocationListener); } catch (SecurityException ex){ //ignored //try another location provider @@ -314,7 +323,8 @@ @Override public Loader onCreateLoader(int id, Bundle args) { //BUILD URI - lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); + if (args!=null) + lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); Uri.Builder builder = new Uri.Builder(); builder.scheme("content").authority(AppDataProvider.AUTHORITY) .appendPath("stops").appendPath("location") @@ -331,50 +341,58 @@ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { if (0 > MAX_DISTANCE) throw new AssertionError(); //Cursor might be null - if(cursor==null){ - Log.e(DEBUG_TAG,"Null cursor, something really wrong happened"); + if (cursor == null) { + Log.e(DEBUG_TAG, "Null cursor, something really wrong happened"); return; } - Log.d(DEBUG_TAG, "Num stops found: "+cursor.getCount()+", Current distance: "+distance); + Log.d(DEBUG_TAG, "Num stops found: " + cursor.getCount() + ", Current distance: " + distance); - if(!isDBUpdating() && (cursor.getCount()0) + currentNearbyStops = createStopListFromCursor(cursor); - if(cursor.getCount()>0) { - ArrayList stopList = createStopListFromCursor(cursor); - double minDistance = Double.POSITIVE_INFINITY; - for(Stop s: stopList){ - minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); - } + showCurrentStops(); + } + /** + * Display the stops, or run new set of requests for arrivals + */ + private void showCurrentStops(){ + if (currentNearbyStops.isEmpty()) { + setNoStopsLayout(); + return; + } - //quick trial to hopefully always get the stops in the correct order - Collections.sort(stopList,new StopSorterByDistance(lastReceivedLocation)); - switch (fragment_type){ - case TYPE_STOPS: - showStopsInRecycler(stopList); - break; - case TYPE_ARRIVALS: - arrivalsManager = new ArrivalsManager(stopList); - flatProgressBar.setVisibility(View.VISIBLE); - flatProgressBar.setProgress(0); - flatProgressBar.setIndeterminate(false); - //for the moment, be satisfied with only one location - //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); - break; - default: - } + double minDistance = Double.POSITIVE_INFINITY; + for(Stop s: currentNearbyStops){ + minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); + } - } else { - setNoStopsLayout(); + + //quick trial to hopefully always get the stops in the correct order + Collections.sort(currentNearbyStops,new StopSorterByDistance(lastReceivedLocation)); + switch (fragment_type){ + case TYPE_STOPS: + showStopsInRecycler(currentNearbyStops); + break; + case TYPE_ARRIVALS: + arrivalsManager = new ArrivalsManager(currentNearbyStops); + flatProgressBar.setVisibility(View.VISIBLE); + flatProgressBar.setProgress(0); + flatProgressBar.setIndeterminate(false); + //for the moment, be satisfied with only one location + //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); + break; + default: } } @@ -411,15 +429,12 @@ gridRecyclerView.setAdapter(arrivalsStopAdapter); } fragmentLocationListener.lastUpdateTime = -1; - locManager.removeLocationRequestFor(fragmentLocationListener); - locManager.addLocationRequestFor(fragmentLocationListener); + //locManager.removeLocationRequestFor(fragmentLocationListener); + //locManager.addLocationRequestFor(fragmentLocationListener); + showCurrentStops(); } //useful methods - protected boolean isDBUpdating(){ - return globalSharedPref.getBoolean(getString(R.string.databaseUpdatingPref),false); - } - /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ @@ -454,7 +469,8 @@ if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything - routesPairList.add(new Pair<>(p,r)); + if (r.passaggi != null && !r.passaggi.isEmpty()) + routesPairList.add(new Pair<>(p,r)); } } if (getContext()==null){ @@ -493,31 +509,30 @@ messageTextView.setVisibility(View.GONE); } - class ArrivalsManager implements FiveTAPIVolleyRequest.ResponseListener, Response.ErrorListener{ - final HashMap mStops; - final Map> routesToAdd = new HashMap<>(); + class ArrivalsManager implements Response.Listener, Response.ErrorListener{ + final HashMap palinasDone = new HashMap<>(); + //final Map> routesToAdd = new HashMap<>(); final static String REQUEST_TAG = "NearbyArrivals"; - private final QueryType[] types = {QueryType.ARRIVALS,QueryType.DETAILS}; final NetworkVolleyManager volleyManager; int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0; ArrivalsManager(List stops){ - mStops = new HashMap<>(); volleyManager = NetworkVolleyManager.getInstance(getContext()); int MAX_ARRIVAL_STOPS = 35; + Date currentDate = new Date(); + int timeRange = 3600; + int departures = 10; + int numreq = 0; for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){ - mStops.put(s.ID,new Palina(s)); - for(QueryType t: types) { - final FiveTAPIVolleyRequest req = FiveTAPIVolleyRequest.getNewRequest(t, s.ID, this, this); - if (req != null) { - req.setTag(REQUEST_TAG); - volleyManager.addToRequestQueue(req); - activeRequestCount++; - } - } + + final MapiArrivalRequest req = new MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this); + req.setTag(REQUEST_TAG); + volleyManager.addToRequestQueue(req); + activeRequestCount++; + numreq++; } - flatProgressBar.setMax(activeRequestCount); + flatProgressBar.setMax(numreq); } @@ -546,40 +561,19 @@ } @Override - public void onResponse(Palina result, QueryType type) { + public void onResponse(Palina result) { //counter for requests activeRequestCount--; reqSuccessCount++; - - - final Palina palinaInMap = mStops.get(result.ID); + //final Palina palinaInMap = palinasDone.get(result.ID); //palina cannot be null here //sorry for the brutal crash when it happens - if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); - //necessary to split the Arrivals and Details cases - switch (type){ - case ARRIVALS: - palinaInMap.addInfoFromRoutes(result.queryAllRoutes()); - final List possibleRoutes = routesToAdd.get(result.ID); - if(possibleRoutes!=null) { - palinaInMap.addInfoFromRoutes(possibleRoutes); - routesToAdd.remove(result.ID); - } - break; - case DETAILS: - if(palinaInMap.queryAllRoutes().size()>0){ - //merge the branches - palinaInMap.addInfoFromRoutes(result.queryAllRoutes()); - } else { - routesToAdd.put(result.ID,result.queryAllRoutes()); - } - break; - default: - throw new IllegalArgumentException("Wrong QueryType in onResponse"); - } - + //if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); + //add the palina to the successful one + //TODO: Avoid redoing everything every time a new Result arrives + palinasDone.put(result.ID, result); final ArrayList outList = new ArrayList<>(); - for(Palina p: mStops.values()){ + for(Palina p: palinasDone.values()){ final List routes = p.queryAllRoutes(); if(routes!=null && routes.size()>0) outList.add(p); } @@ -613,14 +607,14 @@ public void onLocationChanged(Location location) { //set adapter float accuracy = location.getAccuracy(); - if(accuracy<60 && canStartDBQuery) { + if(accuracy<60 && !dbUpdateRunning) { distance = 20; final Bundle msgBundle = new Bundle(); msgBundle.putParcelable(BUNDLE_LOCATION,location); getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); } lastUpdateTime = System.currentTimeMillis(); - Log.d("BusTO:NearPositListen","can start loader "+ canStartDBQuery); + Log.d("BusTO:NearPositListen","can start loader "+ !dbUpdateRunning); } @Override