feat: server-handled routes and stops

This commit is contained in:
Cilly Leang 2025-08-12 22:43:33 +10:00
parent efba64ea90
commit 58ee095522
Signed by: cilly
GPG key ID: 6500251E087653C9
61 changed files with 1634 additions and 349 deletions

View file

@ -37,6 +37,7 @@ kotlin {
sourceSets {
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
@ -48,6 +49,7 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
}

View file

@ -0,0 +1,315 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "83ece554400bb035c267dc2414c23293",
"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": "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"
]
},
"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_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"
]
}
]
}
],
"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, '83ece554400bb035c267dc2414c23293')"
]
}
}

View file

@ -6,6 +6,7 @@ import androidx.room.RoomDatabase
import moe.lava.banksia.room.Database
import org.koin.core.parameter.ParametersHolder
import org.koin.core.scope.Scope
import org.koin.dsl.module
class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
override fun getBuilder(): RoomDatabase.Builder<Database> {
@ -19,4 +20,6 @@ class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
}
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
AndroidDatabaseBuilder(p.get())
AndroidDatabaseBuilder(get())
internal actual val ExtPlatformModule = module { }

View file

@ -13,7 +13,6 @@ import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.http.appendPathSegments
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -22,9 +21,10 @@ import moe.lava.banksia.data.ptv.structures.PtvDeparture
import moe.lava.banksia.data.ptv.structures.PtvDirection
import moe.lava.banksia.data.ptv.structures.PtvRoute
import moe.lava.banksia.data.ptv.structures.PtvRouteType
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
import moe.lava.banksia.data.ptv.structures.PtvRun
import moe.lava.banksia.data.ptv.structures.PtvStop
import moe.lava.banksia.util.CacheMap
import moe.lava.banksia.model.RouteType
import moe.lava.banksia.util.LoopFlow.Companion.initWith
import moe.lava.banksia.util.error
import moe.lava.banksia.util.log
@ -59,16 +59,15 @@ suspend inline fun <K, V> MutableMap<K, V>.getOrPutSuspend(key: K, defaultValue:
return this[key]!!
}
class PtvService(coroutineScope: CoroutineScope) {
class PtvService() {
class PtvCache(
coroutineScope: CoroutineScope,
val directions: CacheMap<Pair<Int, Int>, PtvDirection> = CacheMap(coroutineScope),
val routes: CacheMap<Int, PtvRoute> = CacheMap(coroutineScope),
val runs: CacheMap<String, PtvRun> = CacheMap(coroutineScope),
val stops: CacheMap<Int, PtvStop> = CacheMap(coroutineScope),
val directions: MutableMap<Pair<Int, Int>, PtvDirection> = mutableMapOf(),
val routes: MutableMap<Int, PtvRoute> = mutableMapOf(),
val runs: MutableMap<String, PtvRun> = mutableMapOf(),
val stops: MutableMap<Int, PtvStop> = mutableMapOf(),
)
val cache = PtvCache(coroutineScope)
val cache = PtvCache()
private val client = HttpClient() {
install(ContentNegotiation) {
@ -227,6 +226,20 @@ class PtvService(coroutineScope: CoroutineScope) {
return cache.directions[directionId to routeId]!!
}
suspend fun departures(routeType: RouteType, stopId: String): Responses.PtvDeparturesResponse =
client
.safeGet ("departures") {
url {
appendPathSegments(
"route_type", routeType.asPtvType().ordinal.toString(),
"stop", stopId.toString(),
)
parameter("expand", "Route")
parameter("expand", "Direction")
parameter("gtfs", "true")
}
}.body()
suspend fun departures(routeType: PtvRouteType, stopId: Int): Responses.PtvDeparturesResponse =
client
.safeGet ("departures") {

View file

@ -7,6 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.lava.banksia.model.RouteType
private object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
@ -30,4 +31,20 @@ enum class PtvRouteType {
BUS,
VLINE,
NIGHT_BUS,
;
companion object {
fun fromModel(type: RouteType) = when (type) {
RouteType.MetroTrain -> TRAIN
RouteType.MetroTram -> TRAM
RouteType.MetroBus -> BUS
RouteType.RegionalTrain -> VLINE
RouteType.RegionalCoach -> BUS
RouteType.RegionalBus -> BUS
RouteType.SkyBus -> BUS
RouteType.Interstate -> TRAIN
}
fun RouteType.asPtvType() = fromModel(this)
}
}

View file

@ -7,6 +7,9 @@ val CommonModules = module {
includes(PlatformModule)
single { Database.build(get<PlatformDatabaseBuilder>().getBuilder()) }
single { get<Database>().getRouteDao() }
single { get<Database>().getShapeDao() }
single { get<Database>().routeDao }
single { get<Database>().shapeDao }
single { get<Database>().stopDao }
single { get<Database>().stopTimeDao }
single { get<Database>().tripDao }
}

View file

@ -2,6 +2,7 @@ package moe.lava.banksia.di
import androidx.room.RoomDatabase
import moe.lava.banksia.room.Database
import org.koin.core.module.Module
import org.koin.core.parameter.ParametersHolder
import org.koin.core.scope.Scope
import org.koin.dsl.module
@ -12,6 +13,9 @@ interface PlatformDatabaseBuilder {
expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder
internal expect val ExtPlatformModule: Module
internal val PlatformModule = module {
includes(ExtPlatformModule)
single { provideDatabaseBuilder(it) }
}

View file

@ -0,0 +1,50 @@
package moe.lava.banksia.model
import kotlinx.datetime.LocalTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.lava.banksia.model.FutureTime.Companion.asInt
@Serializable(FutureTimeSerialiser::class)
data class FutureTime(
val dayOffset: Boolean,
val time: LocalTime,
) {
companion object {
fun from(hour: Int, minute: Int, second: Int): FutureTime {
var nHour = hour
val nextDay = hour >= 24
if (nextDay)
nHour -= 24
val time = LocalTime(nHour, minute, second)
return FutureTime(nextDay, time)
}
fun FutureTime.asInt() =
trueHour * 3600 + minute * 60 + second
fun fromInt(int: Int) = FutureTime.from(
int / 3600,
(int / 60) % 60,
int % 60,
)
}
val hour = time.hour
val minute = time.minute
val second = time.second
val trueHour = time.hour + (if (dayOffset) 24 else 0)
}
object FutureTimeSerialiser: KSerializer<FutureTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(FutureTimeSerialiser::class.qualifiedName!!, PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: FutureTime) = encoder.encodeInt(value.asInt())
override fun deserialize(decoder: Decoder) = FutureTime.fromInt(decoder.decodeInt())
}

View file

@ -1,11 +1,10 @@
package moe.lava.banksia.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity
@Serializable
data class Route(
@PrimaryKey val id: String,
val id: String,
val type: RouteType,
val number: String?,
val name: String,

View file

@ -1,7 +1,8 @@
package moe.lava.banksia.model
import androidx.room.TypeConverter
import kotlinx.serialization.Serializable
@Serializable
enum class RouteType(val value: Int) {
MetroTrain(2),
MetroTram(3),
@ -12,12 +13,4 @@ enum class RouteType(val value: Int) {
SkyBus(11),
Interstate(10),
;
companion object {
@TypeConverter
fun from(value: Int) = RouteType.entries.first { it.value == value }
@TypeConverter
fun to(routeType: RouteType) = routeType.value
}
}

View file

@ -0,0 +1,5 @@
package moe.lava.banksia.model
data class Run(
val ref: String,
)

View file

@ -0,0 +1,13 @@
package moe.lava.banksia.model
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class Service(
val id: String,
val days: List<DayOfWeek>,
val start: LocalDate,
val end: LocalDate,
)

View file

@ -1,16 +1,12 @@
package moe.lava.banksia.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import moe.lava.banksia.room.converter.ShapeConverter
import kotlinx.serialization.Serializable
import moe.lava.banksia.util.Point
typealias ShapePath = List<Point>
@Entity
@TypeConverters(ShapeConverter::class)
@Serializable
data class Shape(
@PrimaryKey val id: String,
val id: String,
val path: ShapePath,
)

View file

@ -0,0 +1,15 @@
package moe.lava.banksia.model
import kotlinx.serialization.Serializable
import moe.lava.banksia.util.Point
@Serializable
data class Stop(
val id: String,
val name: String,
val pos: Point,
val parent: String,
val hasWheelChairBoarding: Boolean,
val level: String,
val platformCode: String,
)

View file

@ -0,0 +1,14 @@
package moe.lava.banksia.model
import kotlinx.serialization.Serializable
@Serializable
data class StopTime(
val tripId: String,
val stopId: String,
val arrivalTime: FutureTime,
val departureTime: FutureTime,
val headsign: String?,
val pickupType: Int,
val dropOffType: Int,
)

View file

@ -0,0 +1,15 @@
package moe.lava.banksia.model
import kotlinx.serialization.Serializable
@Serializable
data class Trip(
val id: String,
val routeId: String,
val serviceId: String,
val shapeId: String?,
val tripHeadsign: String,
val directionId: String,
val blockId: String,
val wheelchairAccessible: String,
)

View file

@ -1,28 +1,51 @@
package moe.lava.banksia.room
import androidx.room.AutoMigration
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import moe.lava.banksia.model.Route
import moe.lava.banksia.model.RouteType
import moe.lava.banksia.model.Shape
import moe.lava.banksia.room.converter.RouteTypeConverter
import moe.lava.banksia.room.dao.RouteDao
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.entity.RouteEntity
import moe.lava.banksia.room.entity.ShapeEntity
import moe.lava.banksia.room.entity.StopEntity
import moe.lava.banksia.room.entity.StopTimeEntity
import moe.lava.banksia.room.entity.TripEntity
import androidx.room.Database as DatabaseAnnotation
@DatabaseAnnotation(entities = [Route::class, Shape::class], version = 1)
@TypeConverters(RouteType.Companion::class)
@DatabaseAnnotation(
version = 2,
entities = [
RouteEntity::class,
ShapeEntity::class,
StopEntity::class,
StopTimeEntity::class,
TripEntity::class,
],
autoMigrations = [
AutoMigration(from = 1, to = 2),
]
)
@TypeConverters(RouteTypeConverter::class)
abstract class Database : RoomDatabase() {
abstract fun getRouteDao(): RouteDao
abstract fun getShapeDao(): ShapeDao
abstract val routeDao: RouteDao
abstract val shapeDao: ShapeDao
abstract val stopDao: StopDao
abstract val stopTimeDao: StopTimeDao
abstract val tripDao: TripDao
companion object {
fun build(base: Builder<Database>) =
base.fallbackToDestructiveMigrationOnDowngrade(true)
base.fallbackToDestructiveMigration(true)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
// .fallbackToDestructiveMigration(true)
.build()
}
}

View file

@ -0,0 +1,12 @@
package moe.lava.banksia.room.converter
import androidx.room.TypeConverter
import moe.lava.banksia.model.RouteType
object RouteTypeConverter {
@TypeConverter
fun from(value: Int) = RouteType.entries.first { it.value == value }
@TypeConverter
fun to(routeType: RouteType) = routeType.value
}

View file

@ -4,7 +4,7 @@ import androidx.room.TypeConverter
import moe.lava.banksia.model.ShapePath
import moe.lava.banksia.util.Point
object ShapeConverter {
object ShapePathConverter {
@TypeConverter
fun from(value: ByteArray): ShapePath {
return value

View file

@ -3,23 +3,47 @@ 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.model.Route
import moe.lava.banksia.room.entity.RouteEntity
import moe.lava.banksia.room.entity.StopEntity
@Dao
interface RouteDao {
@Query("SELECT * FROM Route")
suspend fun getAll(): List<Route>
suspend fun getAll(): List<RouteEntity>
@Query("SELECT * FROM Route WHERE id == :id")
suspend fun get(id: String): Route?
suspend fun get(id: String): RouteEntity?
@Insert
suspend fun insertAll(vararg routes: Route)
suspend fun insertAll(vararg routes: RouteEntity)
@Insert(onConflict = REPLACE)
suspend fun insertOrReplaceAll(vararg routes: RouteEntity)
@Delete
suspend fun delete(route: Route)
suspend fun delete(route: RouteEntity)
@Query("DELETE FROM Route")
suspend fun deleteAll()
@Query("""
SELECT Stop.* FROM Stop
INNER JOIN StopTime ON StopTime.stopId == Stop.id
INNER JOIN Trip ON Trip.id == StopTime.tripId
WHERE Trip.routeId == :id
GROUP BY Stop.id
""")
suspend fun stops(id: String): List<StopEntity>
@Query("""
SELECT Stop.* FROM Stop
INNER JOIN Stop Child ON Child.parent == Stop.id
INNER JOIN StopTime ON StopTime.stopId == Child.id
INNER JOIN Trip ON Trip.id == StopTime.tripId
WHERE Trip.routeId == :id
GROUP BY Stop.id
""")
suspend fun stopsParent(id: String): List<StopEntity>
}

View file

@ -4,18 +4,18 @@ import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import moe.lava.banksia.model.Shape
import moe.lava.banksia.room.entity.ShapeEntity
@Dao
interface ShapeDao {
@Query("SELECT * FROM Shape WHERE id == :id")
suspend fun get(id: String): Shape?
suspend fun get(id: String): ShapeEntity?
@Insert
suspend fun insertAll(vararg shapes: Shape)
suspend fun insertAll(vararg shapes: ShapeEntity)
@Delete
suspend fun delete(shape: Shape)
suspend fun delete(shape: ShapeEntity)
@Query("DELETE FROM Shape")
suspend fun deleteAll()

View file

@ -0,0 +1,32 @@
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.StopEntity
@Dao
interface StopDao {
@Query("SELECT * FROM Stop")
suspend fun getAll(): List<StopEntity>
@Query("SELECT * FROM Stop WHERE id == :id")
suspend fun get(id: String): StopEntity?
@Query("SELECT * FROM Stop WHERE id IN (:ids)")
suspend fun get(ids: List<String>): List<StopEntity>
@Insert
suspend fun insertAll(vararg stops: StopEntity)
@Insert(onConflict = REPLACE)
suspend fun insertOrReplaceAll(vararg stops: StopEntity)
@Delete
suspend fun delete(stop: StopEntity)
@Query("DELETE FROM Stop")
suspend fun deleteAll()
}

View file

@ -0,0 +1,32 @@
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.StopTimeEntity
@Dao
interface StopTimeDao {
@Query("SELECT * FROM StopTime")
suspend fun getAll(): List<StopTimeEntity>
@Query("SELECT * FROM StopTime WHERE tripId == :tripId")
suspend fun get(tripId: String): StopTimeEntity?
@Query("SELECT * FROM StopTime WHERE tripId IN (:tripIds)")
suspend fun get(tripIds: List<String>): List<StopTimeEntity>
@Insert
suspend fun insertAll(vararg stopTimes: StopTimeEntity)
@Insert(onConflict = REPLACE)
suspend fun insertOrReplaceAll(vararg stopTimes: StopTimeEntity)
@Delete
suspend fun delete(stopTime: StopTimeEntity)
@Query("DELETE FROM StopTime")
suspend fun deleteAll()
}

View file

@ -0,0 +1,32 @@
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.TripEntity
@Dao
interface TripDao {
@Query("SELECT * FROM Trip")
suspend fun getAll(): List<TripEntity>
@Query("SELECT * FROM Trip WHERE id == :id")
suspend fun get(id: String): TripEntity?
@Query("SELECT * FROM Trip WHERE routeId == :id")
suspend fun getByRoute(id: String): List<TripEntity>
@Insert
suspend fun insertAll(vararg trips: TripEntity)
@Insert(onConflict = REPLACE)
suspend fun insertOrReplaceAll(vararg trips: TripEntity)
@Delete
suspend fun delete(trip: TripEntity)
@Query("DELETE FROM Trip")
suspend fun deleteAll()
}

View file

@ -0,0 +1,18 @@
package moe.lava.banksia.room.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import moe.lava.banksia.model.Route
import moe.lava.banksia.model.RouteType
@Entity("Route")
data class RouteEntity(
@PrimaryKey val id: String,
val type: RouteType,
val number: String?,
val name: String,
) {
fun asModel() = Route(id, type, number, name)
}
fun Route.asEntity() = RouteEntity(id, type, number, name)

View file

@ -0,0 +1,58 @@
package moe.lava.banksia.room.entity
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import moe.lava.banksia.model.Service
data class ServiceEntity(
val id: String,
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<DayOfWeek> = 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<DayOfWeek>): 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),
LocalDate.fromEpochDays(start),
LocalDate.fromEpochDays(end),
)
}
fun Service.asEntity() = ServiceEntity(
id,
ServiceEntity.Parser.serialiseDays(days),
start.toEpochDays().toInt(),
end.toEpochDays().toInt(),
)

View file

@ -0,0 +1,19 @@
package moe.lava.banksia.room.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import moe.lava.banksia.model.Shape
import moe.lava.banksia.model.ShapePath
import moe.lava.banksia.room.converter.ShapePathConverter
@Entity("Shape")
@TypeConverters(ShapePathConverter::class)
data class ShapeEntity(
@PrimaryKey val id: String,
val path: ShapePath,
) {
fun asModel() = Shape(id, path)
}
fun Shape.asEntity() = ShapeEntity(id, path)

View file

@ -0,0 +1,23 @@
package moe.lava.banksia.room.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import moe.lava.banksia.model.Stop
import moe.lava.banksia.util.Point
@Entity("Stop")
data class StopEntity(
@PrimaryKey val id: String,
val name: String,
val lat: Double,
val lng: Double,
@ColumnInfo(index = true) val parent: String,
val hasWheelChairBoarding: Boolean,
val level: String,
val platformCode: String,
) {
fun asModel() = Stop(id, name, Point(lat, lng), parent, hasWheelChairBoarding, level, platformCode)
}
fun Stop.asEntity() = StopEntity(id, name, pos.lat, pos.lng, parent, hasWheelChairBoarding, level, platformCode)

View file

@ -0,0 +1,48 @@
package moe.lava.banksia.room.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.Companion.CASCADE
import kotlinx.serialization.ExperimentalSerializationApi
import moe.lava.banksia.model.FutureTime
import moe.lava.banksia.model.FutureTime.Companion.asInt
import moe.lava.banksia.model.StopTime
@Entity(
"StopTime",
primaryKeys = ["tripId", "stopId"],
foreignKeys = [
ForeignKey(TripEntity::class, parentColumns = ["id"], childColumns = ["tripId"], onDelete = CASCADE),
ForeignKey(StopEntity::class, parentColumns = ["id"], childColumns = ["stopId"], onDelete = CASCADE),
]
)
data class StopTimeEntity(
val tripId: String,
val stopId: String,
val arrivalTime: Int,
val departureTime: Int,
val headsign: String?,
val pickupType: Int,
val dropOffType: Int,
) {
fun asModel() = StopTime(
tripId,
stopId,
FutureTime.fromInt(arrivalTime),
FutureTime.fromInt(departureTime),
headsign,
pickupType,
dropOffType,
)
}
@OptIn(ExperimentalSerializationApi::class)
fun StopTime.asEntity() = StopTimeEntity(
tripId,
stopId,
arrivalTime.asInt(),
departureTime.asInt(),
headsign,
pickupType,
dropOffType,
)

View file

@ -0,0 +1,30 @@
package moe.lava.banksia.room.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.Companion.CASCADE
import androidx.room.PrimaryKey
import moe.lava.banksia.model.Trip
@Entity(
"Trip",
foreignKeys = [
ForeignKey(RouteEntity::class, parentColumns = ["id"], childColumns = ["routeId"], onDelete = CASCADE),
ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE),
],
)
data class TripEntity(
@PrimaryKey val id: String,
@ColumnInfo(index = true) val routeId: String,
val serviceId: String,
val shapeId: String?,
val tripHeadsign: String,
val directionId: String,
val blockId: String,
val wheelchairAccessible: String,
) {
fun asModel() = Trip(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible)
}
fun Trip.asEntity() = TripEntity(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible)

View file

@ -1,3 +1,6 @@
package moe.lava.banksia.util
import kotlinx.serialization.Serializable
@Serializable
data class Point(val lat: Double, val lng: Double)

View file

@ -4,6 +4,7 @@ import androidx.room.RoomDatabase
import moe.lava.banksia.room.Database
import org.koin.core.parameter.ParametersHolder
import org.koin.core.scope.Scope
import org.koin.dsl.module
class IosDatabaseBuilder() : PlatformDatabaseBuilder {
override fun getBuilder(): RoomDatabase.Builder<Database> {
@ -13,3 +14,5 @@ class IosDatabaseBuilder() : PlatformDatabaseBuilder {
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
IosDatabaseBuilder()
internal actual val ExtPlatformModule = module { }

View file

@ -5,6 +5,7 @@ import androidx.room.RoomDatabase
import moe.lava.banksia.room.Database
import org.koin.core.parameter.ParametersHolder
import org.koin.core.scope.Scope
import org.koin.dsl.module
import java.io.File
class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
@ -18,3 +19,5 @@ class JvmDatabaseBuilder() : PlatformDatabaseBuilder {
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
JvmDatabaseBuilder()
internal actual val ExtPlatformModule = module { }