feat(server): better support for parent stops

- add datafixer to add parent stops for likely candidates
  - this is mainly for bus hubs, the heuristic is the existence of a
    platform code and missing parent
- use parent stops as default in route_stops
- support parent stops for stoptime querying
This commit is contained in:
Cilly Leang 2026-04-01 20:37:58 +11:00
parent 58649b6171
commit c55e3a3232
Signed by: cilly
GPG key ID: 6500251E087653C9
11 changed files with 616 additions and 13 deletions

View file

@ -8,7 +8,7 @@ data class Stop(
val id: String,
val name: String,
val pos: Point,
val parent: String,
val parent: String?,
val hasWheelChairBoarding: Boolean,
val level: String,
val platformCode: String,

View file

@ -3,7 +3,11 @@ package moe.lava.banksia.room
import androidx.room.AutoMigration
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.room.util.foreignKeyCheck
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.sqlite.execSQL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import moe.lava.banksia.room.converter.RouteTypeConverter
@ -26,7 +30,7 @@ import moe.lava.banksia.room.entity.VersionMetadataEntity
import androidx.room.Database as DatabaseAnnotation
@DatabaseAnnotation(
version = 10,
version = 11,
entities = [
RouteEntity::class,
ServiceEntity::class,
@ -59,7 +63,21 @@ abstract class Database : RoomDatabase() {
base.fallbackToDestructiveMigration(true)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.addMigrations(MIGRATION_10_11)
// .fallbackToDestructiveMigration(true)
.build()
}
}
val MIGRATION_10_11 = object : Migration(10, 11) {
override fun migrate(connection: SQLiteConnection) {
connection.execSQL("CREATE TABLE IF NOT EXISTS `_new_Stop` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`parent`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED)")
connection.execSQL("INSERT INTO `_new_Stop` (`id`,`name`,`lat`,`lng`,`parent`,`hasWheelChairBoarding`,`level`,`platformCode`) SELECT `id`,`name`,`lat`,`lng`,`parent`,`hasWheelChairBoarding`,`level`,`platformCode` FROM `Stop`")
connection.execSQL("UPDATE `_new_Stop` SET `parent` = NULL WHERE `parent` == \"\"")
connection.execSQL("DROP TABLE `Stop`")
connection.execSQL("ALTER TABLE `_new_Stop` RENAME TO `Stop`")
connection.execSQL("CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `Stop` (`parent`)")
connection.execSQL("CREATE INDEX IF NOT EXISTS `index_Trip_serviceId` ON `Trip` (`serviceId`)")
foreignKeyCheck(connection, "Stop")
}
}

View file

@ -37,13 +37,22 @@ interface RouteDao {
""")
suspend fun stops(id: String): List<StopEntity>
// I vibecoded this, sorry
@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
WITH Tree AS (
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
UNION ALL
SELECT s.*
FROM Stop s
INNER JOIN Tree t ON s.id = t.parent
)
SELECT DISTINCT * FROM Tree WHERE parent IS NULL;
""")
suspend fun stopsParent(id: String): List<StopEntity>
}

View file

@ -12,6 +12,13 @@ interface StopDao {
@Query("SELECT * FROM Stop")
suspend fun getAll(): List<StopEntity>
@Query("""
SELECT * FROM Stop
WHERE platformCode <> ""
AND parent == ""
""")
suspend fun getAllParentless(): List<StopEntity>
@Query("SELECT * FROM Stop WHERE id == :id")
suspend fun get(id: String): StopEntity?
@ -29,4 +36,7 @@ interface StopDao {
@Query("DELETE FROM Stop")
suspend fun deleteAll()
@Query("UPDATE Stop SET parent = :parent WHERE id IN (:ids)")
suspend fun updateParents(ids: List<String>, parent: String)
}

View file

@ -27,7 +27,7 @@ interface StopTimeDao {
INNER JOIN Trip ON Trip.serviceId == Service.id
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
WHERE StopTime.tripId == Trip.id
AND StopTime.stopId == :stopId
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL
""")
suspend fun getForStopDated(stopId: String, days: Int, date: Int): List<StopTimeEntity>

View file

@ -2,17 +2,30 @@ package moe.lava.banksia.room.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.Companion.SET_NULL
import androidx.room.PrimaryKey
import moe.lava.banksia.model.Stop
import moe.lava.banksia.util.Point
@Entity("Stop")
@Entity(
"Stop",
foreignKeys = [
ForeignKey(
StopEntity::class,
parentColumns = ["id"],
childColumns = ["parent"],
onDelete = SET_NULL,
deferred = true,
),
]
)
data class StopEntity(
@PrimaryKey val id: String,
val name: String,
val lat: Double,
val lng: Double,
@ColumnInfo(index = true) val parent: String,
@ColumnInfo(index = true) val parent: String?,
val hasWheelChairBoarding: Boolean,
val level: String,
val platformCode: String,

View file

@ -15,7 +15,7 @@ import moe.lava.banksia.model.Trip
ForeignKey(ServiceEntity::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = CASCADE),
ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE),
],
indices = [Index("shapeId")],
indices = [Index("shapeId"), Index("serviceId")],
)
data class TripEntity(
@PrimaryKey val id: String,