feat: stop times/departures reimpl based on gtfs

This commit is contained in:
Cilly Leang 2026-03-31 20:09:48 +11:00
parent b5f2ec102d
commit 72b9fb2757
Signed by: cilly
GPG key ID: 6500251E087653C9
23 changed files with 1630 additions and 128 deletions

View file

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

View file

@ -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<FutureTime> {

View file

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

View file

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

View file

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

View file

@ -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<ServiceEntity>
@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()
}

View file

@ -13,10 +13,22 @@ interface StopTimeDao {
suspend fun getAll(): List<StopTimeEntity>
@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<String>): List<StopTimeEntity>
suspend fun getForTrips(tripIds: List<String>): List<StopTimeEntity>
@Query("SELECT * FROM StopTime WHERE stopId == :stopId")
suspend fun getForStop(stopId: String): List<StopTimeEntity>
@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<StopTimeEntity>
@Insert
suspend fun insertAll(vararg stopTimes: StopTimeEntity)

View file

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

View file

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

View file

@ -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<DayOfWeek> = 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<DayOfWeek>.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
}
}