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

@ -54,6 +54,13 @@ fun Application.module() {
}
routing {
if (Constants.devMode) {
get("/fixup") {
call.respondText("received")
val fixer by inject<GtfsDataFixer>()
fixer.addParentsToStops()
}
}
get("/update") {
val key = call.parameters["key"]
if (key != Constants.updateKey) {
@ -66,8 +73,11 @@ fun Application.module() {
?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip"
call.respondText("received")
launch(context = Dispatchers.IO) {
val fixer by inject<GtfsDataFixer>()
val importer by inject<GtfsImporter>()
importer.import(datasetUrl)
fixer.addParentsToStops()
}
}
@ -123,7 +133,7 @@ fun Application.module() {
}
get("/route_stops/{route_id}") {
val routeId = call.parameters["route_id"]!!
val useParent = call.queryParameters["parent"] in listOf("true", "1")
val useParent = call.queryParameters["parent"] !in listOf("false", "0")
val stops = withContext(Dispatchers.IO) {
val routeDao by inject<RouteDao>()
if (useParent)

View file

@ -0,0 +1,43 @@
package moe.lava.banksia.server
import moe.lava.banksia.room.Database
import moe.lava.banksia.room.entity.StopEntity
import moe.lava.banksia.util.log
import java.security.MessageDigest
class GtfsDataFixer(
private val database: Database,
) {
suspend fun addParentsToStops() {
val dao = database.stopDao
val stops = dao.getAllParentless()
stops
.groupBy { it.name.split("/")[0] }
.filter { (_, stops) -> stops.size > 1 }
.forEach { (name, stops) ->
val avgLat = stops.map { it.lat }.average()
val avgLng = stops.map { it.lng }.average()
val hash = name.sha256().substring(0, 7)
val parentId = "bsia:df1:$hash"
val parent = StopEntity(
id = parentId,
name = name,
lat = avgLat,
lng = avgLng,
parent = "",
hasWheelChairBoarding = stops.all { it.hasWheelChairBoarding },
level = "",
platformCode = "",
)
log("datafixer", "inserting ${parentId} for ${stops.size} children")
dao.insertAll(parent)
database.stopDao.updateParents(stops.map { it.id }, parentId)
}
}
}
private fun String.sha256() =
MessageDigest
.getInstance("SHA-256")
.digest(this.toByteArray())
.joinToString("") { "%02x".format(it) }

View file

@ -1,6 +1,7 @@
package moe.lava.banksia.server.di
import io.ktor.client.HttpClient
import moe.lava.banksia.server.GtfsDataFixer
import moe.lava.banksia.server.GtfsImporter
import moe.lava.banksia.server.gtfs.GtfsParser
import moe.lava.banksia.server.gtfsrt.GtfsrtService
@ -12,5 +13,6 @@ val ServerModules = module {
singleOf(::GtfsParser)
singleOf(::GtfsrtService)
singleOf(::GtfsDataFixer)
singleOf(::GtfsImporter)
}