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

@ -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)