diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeLocalDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeLocalDataSource.kt new file mode 100644 index 0000000..3640b1f --- /dev/null +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeLocalDataSource.kt @@ -0,0 +1,28 @@ +package moe.lava.banksia.client.data.stoptime + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import moe.lava.banksia.model.StopTimeDated +import moe.lava.banksia.model.atDate +import moe.lava.banksia.room.dao.StopTimeDao +import moe.lava.banksia.util.serialise +import kotlin.time.Clock + +class StopTimeLocalDataSource( + private val stopTimeDao: StopTimeDao, +) { + suspend fun getAtStop( + stopId: String, + date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + ): List { + return stopTimeDao + .getForStopDated( + stopId, + listOf(date.dayOfWeek).serialise(), + date.toEpochDays().toInt(), + ) + .map { it.asModel().atDate(date) } + .sortedBy { it.departureTime } + } +} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimePtvDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimePtvDataSource.kt deleted file mode 100644 index 9f00b47..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimePtvDataSource.kt +++ /dev/null @@ -1,44 +0,0 @@ -package moe.lava.banksia.client.data.stoptime - -import moe.lava.banksia.data.ptv.PtvService -import moe.lava.banksia.model.RouteType -import moe.lava.banksia.model.StopTime - -class StopTimePtvDataSource( - private val ptvService: PtvService, -) { - suspend fun getForStop(type: RouteType, stopId: String): List { - return listOf() -// val res = ptvService.departures(type, stopId) -// // Map< -// // Pair, -// // Pair> -// // > -// val timetable = HashMap, Pair>>() -// res.departures.forEach { dep -> -// val key = Pair(dep.directionId, dep.routeId) -// val direction = ptvService.direction(dep.directionId, dep.routeId) -// val route = res.routes[dep.routeId.toString()] -// val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: "" -// val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second -// if (element.size >= 5) -// return@forEach -// -// val date = Instant.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc) -// val min = (date - Clock.System.now()).inWholeMinutes -// if (min <= -5) -// return@forEach -// if (min >= 65) -// element.add("${((min + 30.0) / 60.0).toInt()}hr") -// else -// element.add("${min}mn") -// } -// -// val departures = timetable.values.sortedBy { it.first }.map { (name, list) -> -// if (list.isEmpty()) -// InfoPanelState.Stop.Departure(name, "No departures") -// else -// InfoPanelState.Stop.Departure(name, list.joinToString(" | ")) -// } - } -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt new file mode 100644 index 0000000..baf26e7 --- /dev/null +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt @@ -0,0 +1,36 @@ +package moe.lava.banksia.client.data.stoptime + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import moe.lava.banksia.model.StopTimeDated +import kotlin.time.Clock + +class StopTimeRemoteDataSource( + private val client: HttpClient, +) { + suspend fun getAtStop( + stopId: String, + date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()), + ): List { + return client.get("stoptimes/by_stop/${stopId}") { + parameter("date", date) + }.body>() + } + + /*suspend fun get( + stop: String? = null, + trip: String? = null, + day: DayOfWeek? = Clock.System.todayIn(TimeZone.currentSystemDefault()).dayOfWeek, + ): List { + return client.get("stoptimes") { + stop?.let { parameter("stop", it) } + trip?.let { parameter("trip", it) } + day?.let { parameter("day", it) } + }.body>() + }*/ +} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt new file mode 100644 index 0000000..8b46fbd --- /dev/null +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt @@ -0,0 +1,18 @@ +package moe.lava.banksia.client.data.trip + +import io.ktor.client.HttpClient +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import moe.lava.banksia.model.Trip +import kotlin.time.Clock + +class TripRemoteDataSource( + private val client: HttpClient, +) { + suspend fun get( + day: DayOfWeek? = Clock.System.todayIn(TimeZone.currentSystemDefault()).dayOfWeek, + ): List { + return listOf() + } +} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt index 1507a94..f22c7db 100644 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt @@ -12,7 +12,8 @@ import moe.lava.banksia.client.data.route.RouteLocalDataSource import moe.lava.banksia.client.data.route.RouteRemoteDataSource import moe.lava.banksia.client.data.stop.StopLocalDataSource import moe.lava.banksia.client.data.stop.StopRemoteDataSource -import moe.lava.banksia.client.data.stoptime.StopTimePtvDataSource +import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource +import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource import moe.lava.banksia.client.repository.RouteRepository import moe.lava.banksia.client.repository.StopRepository import moe.lava.banksia.client.repository.StopTimeRepository @@ -48,7 +49,8 @@ val ClientModule = module { singleOf(::RouteRemoteDataSource) singleOf(::StopLocalDataSource) singleOf(::StopRemoteDataSource) - singleOf(::StopTimePtvDataSource) + singleOf(::StopTimeLocalDataSource) + singleOf(::StopTimeRemoteDataSource) // Repositories singleOf(::RouteRepository) diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt index 648f942..4f54840 100644 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt @@ -1,13 +1,16 @@ package moe.lava.banksia.client.repository -import moe.lava.banksia.client.data.stoptime.StopTimePtvDataSource -import moe.lava.banksia.model.StopTime +import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource +import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource +import moe.lava.banksia.model.StopTimeDated class StopTimeRepository( - private val ptv: StopTimePtvDataSource, + private val local: StopTimeLocalDataSource, + private val remote: StopTimeRemoteDataSource, ) { - // TODO - suspend fun getForStop(id: String): List { - return listOf() + suspend fun getForStop(id: String): List { + return local + .getAtStop(id) + .ifEmpty { remote.getAtStop(id) } } } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt index 4ae3398..76ee8ba 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt @@ -15,17 +15,24 @@ import io.ktor.server.routing.routing import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn import moe.lava.banksia.Constants import moe.lava.banksia.di.CommonModules +import moe.lava.banksia.model.atDate import moe.lava.banksia.room.dao.RouteDao import moe.lava.banksia.room.dao.StopDao +import moe.lava.banksia.room.dao.StopTimeDao import moe.lava.banksia.room.dao.VersionMetadataDao import moe.lava.banksia.server.di.ServerModules import moe.lava.banksia.server.gtfs.GtfsHandler import moe.lava.banksia.server.gtfsr.GtfsrService +import moe.lava.banksia.util.serialise import org.koin.dsl.module import org.koin.ktor.ext.inject import org.koin.ktor.plugin.Koin +import kotlin.time.Clock fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) @@ -41,8 +48,11 @@ fun Application.module() { modules(CommonModules, ServerModules) } - val gtfsr by inject() - launch { gtfsr.start() } + @Suppress("KotlinConstantConditions") + if (!Constants.devMode) { + val gtfsr by inject() + launch { gtfsr.start() } + } routing { get("/update") { @@ -137,6 +147,24 @@ fun Application.module() { // .map { it.asModel() } // } // call.respond(stops) + + } + get("/stoptimes/by_stop/{stop_id}") { + val stopId = call.parameters["stop_id"]!! + val date = call.queryParameters["date"] + ?.let { LocalDate.parse(it, LocalDate.Formats.ISO) } + ?: Clock.System.todayIn(TimeZone.currentSystemDefault()) + val times = withContext(context = Dispatchers.IO) { + inject().value + .getForStopDated( + stopId, + listOf(date.dayOfWeek).serialise(), + date.toEpochDays().toInt(), + ) + .map { it.asModel().atDate(date) } + .sortedBy { it.departureTime } + } + call.respond(times) } } } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt index d85d5df..28d50af 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt @@ -9,11 +9,14 @@ import io.ktor.client.statement.bodyAsChannel import io.ktor.util.cio.writeChannel import io.ktor.util.logging.Logger import io.ktor.utils.io.copyAndClose +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate import kotlinx.serialization.decodeFromString import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import moe.lava.banksia.Constants import moe.lava.banksia.model.Route +import moe.lava.banksia.model.Service import moe.lava.banksia.model.Shape import moe.lava.banksia.model.Stop import moe.lava.banksia.model.StopTime @@ -22,6 +25,7 @@ import moe.lava.banksia.room.Database import moe.lava.banksia.room.converter.RouteTypeConverter import moe.lava.banksia.room.entity.asEntity import moe.lava.banksia.server.gtfs.structures.GtfsRoute +import moe.lava.banksia.server.gtfs.structures.GtfsService import moe.lava.banksia.server.gtfs.structures.GtfsShape import moe.lava.banksia.server.gtfs.structures.GtfsStop import moe.lava.banksia.server.gtfs.structures.GtfsStopTime @@ -65,6 +69,7 @@ class GtfsHandler( .listFiles { it.isDirectory } .flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() } .ifEmpty { extractAll(datasetPath) } + .filter { it.parentFile.name == "2" } } else { extractAll(datasetPath) } @@ -72,8 +77,9 @@ class GtfsHandler( addRoutes(files) addStops(files) addShapes(files) - addTrips(files) - addStopTimes(files) + val services = addServices(files) + val trips = addTrips(files, services.associateBy { it.id }) + addStopTimes(files, trips.associateBy { it.id }) updateMetadata(date ?: Clock.System.now().epochSeconds) @@ -174,7 +180,7 @@ class GtfsHandler( ) } } - private suspend fun addStopTimes(files: List) { + private suspend fun addStopTimes(files: List, trips: Map) { val dao = db.stopTimeDao dao.deleteAll() log.info("parsing stop times...") @@ -182,7 +188,7 @@ class GtfsHandler( .filter { it.name == "stop_times.txt" } .forEach { fd -> log.info("parsing stop times for ${fd.parent}...") - parseStopTimes(fd) { seq -> + parseStopTimes(fd, trips) { seq -> seq.chunked(1000000) .forEach { queue -> log.info("converting stop times (${queue.size}) for ${fd.parent}...") @@ -194,7 +200,7 @@ class GtfsHandler( } } - private inline fun parseStopTimes(fd: File, block: (Sequence) -> Unit) = + private inline fun parseStopTimes(fd: File, trips: Map, block: (Sequence) -> Unit) = fd.parseCsvSequence { seq -> seq .map { with(it) { @@ -203,7 +209,7 @@ class GtfsHandler( stopId = stop_id, arrivalTime = GtfsStopTime.parseGtfsTime(arrival_time), departureTime = GtfsStopTime.parseGtfsTime(departure_time), - headsign = stop_headsign, + headsign = stop_headsign.ifEmpty { trips[trip_id]!!.tripHeadsign }, pickupType = pickup_type, dropOffType = drop_off_type, ) @@ -211,25 +217,61 @@ class GtfsHandler( .let { block(it) } } - private suspend fun addTrips(files: List) { + private suspend fun addServices(files: List): List { + val dao = db.serviceDao + log.info("parsing services...") + val services = files + .filter { it.name == "calendar.txt" } + .flatMap { fd -> parseServices(fd) } + + log.info("inserting services...") + dao.deleteAll() + dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray()) + + return services + } + + private fun parseServices(fd: File) = + fd.parseCsv() + .map { with(it) { + val days = buildList { + if (monday == 1) add(DayOfWeek.MONDAY) + if (tuesday == 1) add(DayOfWeek.TUESDAY) + if (wednesday == 1) add(DayOfWeek.WEDNESDAY) + if (thursday == 1) add(DayOfWeek.THURSDAY) + if (friday == 1) add(DayOfWeek.FRIDAY) + if (saturday == 1) add(DayOfWeek.SATURDAY) + if (sunday == 1) add(DayOfWeek.SUNDAY) + } + Service( + id = service_id, + days = days, + start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC), + end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC), + ) + } } + + private suspend fun addTrips(files: List, services: Map): List { val dao = db.tripDao log.info("parsing trips...") val trips = files .filter { it.name == "trips.txt" } - .flatMap { fd -> parseTrips(fd) } + .flatMap { fd -> parseTrips(fd, services) } log.info("inserting trips...") dao.deleteAll() dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) + + return trips } - private fun parseTrips(fd: File) = + private fun parseTrips(fd: File, services: Map) = fd.parseCsv() .map { with(it) { Trip( id = trip_id, routeId = route_id, - serviceId = service_id, + service = services[service_id]!!, shapeId = shape_id.ifEmpty { null }, tripHeadsign = trip_headsign, directionId = direction_id, diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt new file mode 100644 index 0000000..9347b5e --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt @@ -0,0 +1,18 @@ +package moe.lava.banksia.server.gtfs.structures + +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +data class GtfsService( + val service_id: String, + val monday: Int, + val tuesday: Int, + val wednesday: Int, + val thursday: Int, + val friday: Int, + val saturday: Int, + val sunday: Int, + val start_date: String, + val end_date: String, +) diff --git a/shared/schemas/moe.lava.banksia.room.Database/7.json b/shared/schemas/moe.lava.banksia.room.Database/7.json new file mode 100644 index 0000000..d4c62b2 --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/7.json @@ -0,0 +1,415 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "15c94df0a62438ff28d451c074c94c59", + "entities": [ + { + "tableName": "Route", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "days", + "columnName": "days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Service_days", + "unique": false, + "columnNames": [ + "days" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)" + } + ] + }, + { + "tableName": "Shape", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Stop", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lat", + "columnName": "lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lng", + "columnName": "lng", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasWheelChairBoarding", + "columnName": "hasWheelChairBoarding", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platformCode", + "columnName": "platformCode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Stop_parent", + "unique": false, + "columnNames": [ + "parent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" + } + ] + }, + { + "tableName": "StopTime", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripId", + "columnName": "tripId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopId", + "columnName": "stopId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrivalTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departureTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "headsign", + "columnName": "headsign", + "affinity": "TEXT" + }, + { + "fieldPath": "pickupType", + "columnName": "pickupType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dropOffType", + "columnName": "dropOffType", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tripId", + "stopId" + ] + }, + "indices": [ + { + "name": "index_StopTime_tripId", + "unique": false, + "columnNames": [ + "tripId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" + }, + { + "name": "index_StopTime_stopId", + "unique": false, + "columnNames": [ + "stopId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" + } + ], + "foreignKeys": [ + { + "table": "Trip", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "tripId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Stop", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stopId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Trip", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "routeId", + "columnName": "routeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeId", + "columnName": "shapeId", + "affinity": "TEXT" + }, + { + "fieldPath": "tripHeadsign", + "columnName": "tripHeadsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionId", + "columnName": "directionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockId", + "columnName": "blockId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wheelchairAccessible", + "columnName": "wheelchairAccessible", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Trip_shapeId", + "unique": false, + "columnNames": [ + "shapeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" + }, + { + "name": "index_Trip_routeId", + "unique": false, + "columnNames": [ + "routeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" + } + ], + "foreignKeys": [ + { + "table": "Route", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "routeId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Shape", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "shapeId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "VersionMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type" + ] + } + } + ], + "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, '15c94df0a62438ff28d451c074c94c59')" + ] + } +} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/8.json b/shared/schemas/moe.lava.banksia.room.Database/8.json new file mode 100644 index 0000000..9240dd5 --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/8.json @@ -0,0 +1,426 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "6e0f07bf1af88b2e37b5ad7c38a3fb2a", + "entities": [ + { + "tableName": "Route", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "days", + "columnName": "days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Service_days", + "unique": false, + "columnNames": [ + "days" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)" + } + ] + }, + { + "tableName": "Shape", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Stop", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lat", + "columnName": "lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lng", + "columnName": "lng", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasWheelChairBoarding", + "columnName": "hasWheelChairBoarding", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platformCode", + "columnName": "platformCode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Stop_parent", + "unique": false, + "columnNames": [ + "parent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" + } + ] + }, + { + "tableName": "StopTime", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripId", + "columnName": "tripId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopId", + "columnName": "stopId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrivalTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departureTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "headsign", + "columnName": "headsign", + "affinity": "TEXT" + }, + { + "fieldPath": "pickupType", + "columnName": "pickupType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dropOffType", + "columnName": "dropOffType", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tripId", + "stopId" + ] + }, + "indices": [ + { + "name": "index_StopTime_tripId", + "unique": false, + "columnNames": [ + "tripId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" + }, + { + "name": "index_StopTime_stopId", + "unique": false, + "columnNames": [ + "stopId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" + } + ], + "foreignKeys": [ + { + "table": "Trip", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "tripId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Stop", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stopId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Trip", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serviceId`) REFERENCES `Service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "routeId", + "columnName": "routeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeId", + "columnName": "shapeId", + "affinity": "TEXT" + }, + { + "fieldPath": "tripHeadsign", + "columnName": "tripHeadsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionId", + "columnName": "directionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockId", + "columnName": "blockId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wheelchairAccessible", + "columnName": "wheelchairAccessible", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Trip_shapeId", + "unique": false, + "columnNames": [ + "shapeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" + }, + { + "name": "index_Trip_routeId", + "unique": false, + "columnNames": [ + "routeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" + } + ], + "foreignKeys": [ + { + "table": "Route", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "routeId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Shape", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "shapeId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "VersionMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type" + ] + } + } + ], + "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, '6e0f07bf1af88b2e37b5ad7c38a3fb2a')" + ] + } +} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/9.json b/shared/schemas/moe.lava.banksia.room.Database/9.json new file mode 100644 index 0000000..2359dbd --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/9.json @@ -0,0 +1,426 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "6e0f07bf1af88b2e37b5ad7c38a3fb2a", + "entities": [ + { + "tableName": "Route", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Service", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "days", + "columnName": "days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Service_days", + "unique": false, + "columnNames": [ + "days" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)" + } + ] + }, + { + "tableName": "Shape", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Stop", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lat", + "columnName": "lat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lng", + "columnName": "lng", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasWheelChairBoarding", + "columnName": "hasWheelChairBoarding", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platformCode", + "columnName": "platformCode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Stop_parent", + "unique": false, + "columnNames": [ + "parent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" + } + ] + }, + { + "tableName": "StopTime", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tripId", + "columnName": "tripId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stopId", + "columnName": "stopId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrivalTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "departureTime", + "columnName": "departureTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "headsign", + "columnName": "headsign", + "affinity": "TEXT" + }, + { + "fieldPath": "pickupType", + "columnName": "pickupType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dropOffType", + "columnName": "dropOffType", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tripId", + "stopId" + ] + }, + "indices": [ + { + "name": "index_StopTime_tripId", + "unique": false, + "columnNames": [ + "tripId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" + }, + { + "name": "index_StopTime_stopId", + "unique": false, + "columnNames": [ + "stopId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" + } + ], + "foreignKeys": [ + { + "table": "Trip", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "tripId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Stop", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "stopId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Trip", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serviceId`) REFERENCES `Service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "routeId", + "columnName": "routeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shapeId", + "columnName": "shapeId", + "affinity": "TEXT" + }, + { + "fieldPath": "tripHeadsign", + "columnName": "tripHeadsign", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directionId", + "columnName": "directionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockId", + "columnName": "blockId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wheelchairAccessible", + "columnName": "wheelchairAccessible", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Trip_shapeId", + "unique": false, + "columnNames": [ + "shapeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" + }, + { + "name": "index_Trip_routeId", + "unique": false, + "columnNames": [ + "routeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" + } + ], + "foreignKeys": [ + { + "table": "Route", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "routeId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Service", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serviceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Shape", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "shapeId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "VersionMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type" + ] + } + } + ], + "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, '6e0f07bf1af88b2e37b5ad7c38a3fb2a')" + ] + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt index 823174b..8658342 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt @@ -9,6 +9,7 @@ val CommonModules = module { single { Database.build(get().getBuilder()) } single { get().versionMetadataDao } single { get().routeDao } + single { get().serviceDao } single { get().shapeDao } single { get().stopDao } single { get().stopTimeDao } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt index c1853a9..91c5c77 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt @@ -1,6 +1,10 @@ package moe.lava.banksia.model +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime +import kotlinx.datetime.atTime +import kotlinx.datetime.plus import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -39,6 +43,10 @@ data class FutureTime( val minute = time.minute val second = time.second val trueHour = time.hour + (if (dayOffset) 24 else 0) + + fun atDate(date: LocalDate) = date + .let { if (dayOffset) date.plus(1, DateTimeUnit.DAY) else date } + .atTime(time) } object FutureTimeSerialiser: KSerializer { diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt new file mode 100644 index 0000000..55288fa --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt @@ -0,0 +1,26 @@ +package moe.lava.banksia.model + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class StopTimeDated( + val tripId: String, + val stopId: String, + val arrivalTime: LocalDateTime, + val departureTime: LocalDateTime, + val headsign: String?, + val pickupType: Int, + val dropOffType: Int, +) + +fun StopTime.atDate(date: LocalDate) = StopTimeDated( + tripId = tripId, + stopId = stopId, + arrivalTime = arrivalTime.atDate(date), + departureTime = departureTime.atDate(date), + headsign = headsign, + pickupType = pickupType, + dropOffType = dropOffType, +) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt index ef95eea..81d3f8d 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class Trip( val id: String, val routeId: String, - val serviceId: String, + val service: Service, val shapeId: String?, val tripHeadsign: String, val directionId: String, diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt index 90a577f..89bc24a 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt @@ -8,12 +8,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import moe.lava.banksia.room.converter.RouteTypeConverter import moe.lava.banksia.room.dao.RouteDao +import moe.lava.banksia.room.dao.ServiceDao import moe.lava.banksia.room.dao.ShapeDao import moe.lava.banksia.room.dao.StopDao import moe.lava.banksia.room.dao.StopTimeDao import moe.lava.banksia.room.dao.TripDao import moe.lava.banksia.room.dao.VersionMetadataDao import moe.lava.banksia.room.entity.RouteEntity +import moe.lava.banksia.room.entity.ServiceEntity import moe.lava.banksia.room.entity.ShapeEntity import moe.lava.banksia.room.entity.StopEntity import moe.lava.banksia.room.entity.StopTimeEntity @@ -22,9 +24,10 @@ import moe.lava.banksia.room.entity.VersionMetadataEntity import androidx.room.Database as DatabaseAnnotation @DatabaseAnnotation( - version = 6, + version = 9, entities = [ RouteEntity::class, + ServiceEntity::class, ShapeEntity::class, StopEntity::class, StopTimeEntity::class, @@ -40,6 +43,7 @@ import androidx.room.Database as DatabaseAnnotation abstract class Database : RoomDatabase() { abstract val versionMetadataDao: VersionMetadataDao abstract val routeDao: RouteDao + abstract val serviceDao: ServiceDao abstract val shapeDao: ShapeDao abstract val stopDao: StopDao abstract val stopTimeDao: StopTimeDao diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt new file mode 100644 index 0000000..6fc2906 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt @@ -0,0 +1,29 @@ +package moe.lava.banksia.room.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import moe.lava.banksia.room.entity.ServiceEntity + +@Dao +interface ServiceDao { + @Query("SELECT * FROM Service") + suspend fun getAll(): List + + @Query("SELECT * FROM Service WHERE id == :id") + suspend fun get(id: String): ServiceEntity? + + @Insert + suspend fun insertAll(vararg services: ServiceEntity) + + @Insert(onConflict = REPLACE) + suspend fun insertOrReplaceAll(vararg services: ServiceEntity) + + @Delete + suspend fun delete(service: ServiceEntity) + + @Query("DELETE FROM Service") + suspend fun deleteAll() +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt index 88485f4..d5e1744 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt @@ -13,10 +13,22 @@ interface StopTimeDao { suspend fun getAll(): List @Query("SELECT * FROM StopTime WHERE tripId == :tripId") - suspend fun get(tripId: String): StopTimeEntity? + suspend fun getForTrip(tripId: String): StopTimeEntity? @Query("SELECT * FROM StopTime WHERE tripId IN (:tripIds)") - suspend fun get(tripIds: List): List + suspend fun getForTrips(tripIds: List): List + + @Query("SELECT * FROM StopTime WHERE stopId == :stopId") + suspend fun getForStop(stopId: String): List + + @Query(""" + SELECT * FROM StopTime + INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end` + INNER JOIN Trip ON Trip.serviceId == Service.id + WHERE StopTime.tripId == Trip.id + AND StopTime.stopId == :stopId + """) + suspend fun getForStopDated(stopId: String, days: Int, date: Int): List @Insert suspend fun insertAll(vararg stopTimes: StopTimeEntity) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt index 4b14a95..027aaa8 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt @@ -1,50 +1,23 @@ package moe.lava.banksia.room.entity -import kotlinx.datetime.DayOfWeek +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey import kotlinx.datetime.LocalDate import moe.lava.banksia.model.Service +import moe.lava.banksia.util.deserialiseDaysBitflag +import moe.lava.banksia.util.serialise +@Entity("Service") data class ServiceEntity( - val id: String, - val days: Int, + @PrimaryKey val id: String, + @ColumnInfo(index = true) val days: Int, val start: Int, val end: Int, ) { - object Parser { - private fun Int.check(other: Int) = (this and other) != 0 - - fun deserialiseDays(days: Int): List = buildList { - if (days.check(1)) - add(DayOfWeek.MONDAY) - if (days.check(1 shl 1)) - add(DayOfWeek.TUESDAY) - if (days.check(1 shl 2)) - add(DayOfWeek.WEDNESDAY) - if (days.check(1 shl 3)) - add(DayOfWeek.THURSDAY) - if (days.check(1 shl 4)) - add(DayOfWeek.FRIDAY) - if (days.check(1 shl 5)) - add(DayOfWeek.SATURDAY) - if (days.check(1 shl 6)) - add(DayOfWeek.SUNDAY) - } - fun serialiseDays(days: List): Int = - days.fold(0) { vl, n -> - vl + when (n) { - DayOfWeek.MONDAY -> 1 - DayOfWeek.TUESDAY -> 1 shl 1 - DayOfWeek.WEDNESDAY -> 1 shl 2 - DayOfWeek.THURSDAY -> 1 shl 3 - DayOfWeek.FRIDAY -> 1 shl 4 - DayOfWeek.SATURDAY -> 1 shl 5 - DayOfWeek.SUNDAY -> 1 shl 6 - } - } - } fun asModel() = Service( id, - Parser.deserialiseDays(days), + days.deserialiseDaysBitflag(), LocalDate.fromEpochDays(start), LocalDate.fromEpochDays(end), ) @@ -52,7 +25,7 @@ data class ServiceEntity( fun Service.asEntity() = ServiceEntity( id, - ServiceEntity.Parser.serialiseDays(days), + days.serialise(), start.toEpochDays().toInt(), end.toEpochDays().toInt(), ) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt index fc30e4e..3753d44 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt @@ -12,6 +12,7 @@ import moe.lava.banksia.model.Trip "Trip", foreignKeys = [ ForeignKey(RouteEntity::class, parentColumns = ["id"], childColumns = ["routeId"], onDelete = CASCADE), + ForeignKey(ServiceEntity::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = CASCADE), ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE), ], indices = [Index("shapeId")], @@ -25,8 +26,24 @@ data class TripEntity( val directionId: String, val blockId: String, val wheelchairAccessible: String, -) { - fun asModel() = Trip(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) +) + +fun Trip.Companion.from(tripEntity: TripEntity, serviceEntity: ServiceEntity): Trip { + if (tripEntity.serviceId != serviceEntity.id) { + throw IllegalArgumentException("trip and service id mismatch (${tripEntity.serviceId} != ${serviceEntity.id})") + } + return with(tripEntity) { + Trip( + id = id, + routeId = routeId, + service = serviceEntity.asModel(), + shapeId = shapeId, + tripHeadsign = tripHeadsign, + directionId = directionId, + blockId = blockId, + wheelchairAccessible = wheelchairAccessible + ) + } } -fun Trip.asEntity() = TripEntity(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) +fun Trip.asEntity() = TripEntity(id, routeId, service.id, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt new file mode 100644 index 0000000..87d3244 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt @@ -0,0 +1,36 @@ +package moe.lava.banksia.util + +import kotlinx.datetime.DayOfWeek + +private fun Int.check(other: Int) = (this and other) != 0 + +fun Int.deserialiseDaysBitflag(): List = buildList { + val days = this@deserialiseDaysBitflag + if (days.check(1)) + add(DayOfWeek.MONDAY) + if (days.check(1 shl 1)) + add(DayOfWeek.TUESDAY) + if (days.check(1 shl 2)) + add(DayOfWeek.WEDNESDAY) + if (days.check(1 shl 3)) + add(DayOfWeek.THURSDAY) + if (days.check(1 shl 4)) + add(DayOfWeek.FRIDAY) + if (days.check(1 shl 5)) + add(DayOfWeek.SATURDAY) + if (days.check(1 shl 6)) + add(DayOfWeek.SUNDAY) +} + +fun List.serialise(): Int = + this.fold(0) { vl, n -> + vl + when (n) { + DayOfWeek.MONDAY -> 1 + DayOfWeek.TUESDAY -> 1 shl 1 + DayOfWeek.WEDNESDAY -> 1 shl 2 + DayOfWeek.THURSDAY -> 1 shl 3 + DayOfWeek.FRIDAY -> 1 shl 4 + DayOfWeek.SATURDAY -> 1 shl 5 + DayOfWeek.SUNDAY -> 1 shl 6 + } + } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt index a65b52f..82e9ecc 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant import moe.lava.banksia.client.repository.RouteRepository import moe.lava.banksia.client.repository.StopRepository import moe.lava.banksia.client.repository.StopTimeRepository @@ -30,6 +32,8 @@ import moe.lava.banksia.util.BoxedValue.Companion.box import moe.lava.banksia.util.LoopFlow.Companion.waitUntilSubscribed import moe.lava.banksia.util.Point import moe.lava.banksia.util.log +import kotlin.time.Clock +import kotlin.time.Duration.Companion.minutes sealed class MapScreenEvent { data object DismissState : MapScreenEvent() @@ -223,28 +227,22 @@ class MapScreenViewModel( } val departures = stopTimeRepository.getForStop(id) - .filter { it.headsign != null } + .filter { !it.headsign.isNullOrBlank() } .groupBy { it.headsign!! } .map { (headsign, stopTimes) -> - InfoPanelState.Stop.Departure(headsign, "...") - // TODO -// val tmsF = stopTimes.map { time -> -// val key = Pair(dep.directionId, dep.routeId) -// val direction = ptvService.direction(dep.directionId, dep.routeId) -// val route = res.routes[dep.routeId.toString()] -// val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: "" -// val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second -// if (element.size >= 5) -// return@forEach -// -// val min = (time.departureTime.time - Clock.System.now()).inWholeMinutes -// if (min <= -5) -// return@forEach -// if (min >= 65) -// element.add("${((min + 30.0) / 60.0).toInt()}hr") -// else -// element.add("${min}mn") -// } + val now = Clock.System.now() + val times = stopTimes + .map { it.arrivalTime.toInstant(TimeZone.currentSystemDefault()) } + .filter { it >= (now - 1.minutes) } + .joinToString(" | ") { + val diff = (it - now).inWholeMinutes.coerceAtLeast(0) + if (diff >= 65) { + "${((diff + 30.0) / 60.0).toInt()}hr" + } else { + "${diff}mn" + } + } + InfoPanelState.Stop.Departure(headsign, times) } iInfoState.update { if (it !is InfoPanelState.Stop)