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 { routing {
if (Constants.devMode) {
get("/fixup") {
call.respondText("received")
val fixer by inject<GtfsDataFixer>()
fixer.addParentsToStops()
}
}
get("/update") { get("/update") {
val key = call.parameters["key"] val key = call.parameters["key"]
if (key != Constants.updateKey) { 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" ?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip"
call.respondText("received") call.respondText("received")
launch(context = Dispatchers.IO) { launch(context = Dispatchers.IO) {
val fixer by inject<GtfsDataFixer>()
val importer by inject<GtfsImporter>() val importer by inject<GtfsImporter>()
importer.import(datasetUrl) importer.import(datasetUrl)
fixer.addParentsToStops()
} }
} }
@ -123,7 +133,7 @@ fun Application.module() {
} }
get("/route_stops/{route_id}") { get("/route_stops/{route_id}") {
val routeId = call.parameters["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 stops = withContext(Dispatchers.IO) {
val routeDao by inject<RouteDao>() val routeDao by inject<RouteDao>()
if (useParent) 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 package moe.lava.banksia.server.di
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import moe.lava.banksia.server.GtfsDataFixer
import moe.lava.banksia.server.GtfsImporter import moe.lava.banksia.server.GtfsImporter
import moe.lava.banksia.server.gtfs.GtfsParser import moe.lava.banksia.server.gtfs.GtfsParser
import moe.lava.banksia.server.gtfsrt.GtfsrtService import moe.lava.banksia.server.gtfsrt.GtfsrtService
@ -12,5 +13,6 @@ val ServerModules = module {
singleOf(::GtfsParser) singleOf(::GtfsParser)
singleOf(::GtfsrtService) singleOf(::GtfsrtService)
singleOf(::GtfsDataFixer)
singleOf(::GtfsImporter) singleOf(::GtfsImporter)
} }

View file

@ -0,0 +1,498 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "c4be3d0c2a25f8c5c33132646a070d0e",
"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": "Service",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "days",
"columnName": "days",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "start",
"columnName": "start",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "end",
"columnName": "end",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Service_days",
"unique": false,
"columnNames": [
"days"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)"
}
]
},
{
"tableName": "ServiceException",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serviceId` TEXT NOT NULL, `date` INTEGER NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`serviceId`, `date`))",
"fields": [
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"serviceId",
"date"
]
},
"indices": [
{
"name": "index_ServiceException_serviceId",
"unique": false,
"columnNames": [
"serviceId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ServiceException_serviceId` ON `${TABLE_NAME}` (`serviceId`)"
},
{
"name": "index_ServiceException_type",
"unique": false,
"columnNames": [
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ServiceException_type` ON `${TABLE_NAME}` (`type`)"
}
]
},
{
"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, `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)",
"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"
},
{
"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`)"
}
],
"foreignKeys": [
{
"table": "Stop",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"parent"
],
"referencedColumns": [
"id"
]
}
]
},
{
"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"
]
},
"indices": [
{
"name": "index_StopTime_tripId",
"unique": false,
"columnNames": [
"tripId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)"
},
{
"name": "index_StopTime_stopId",
"unique": false,
"columnNames": [
"stopId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`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(`serviceId`) REFERENCES `Service`(`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_shapeId",
"unique": false,
"columnNames": [
"shapeId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)"
},
{
"name": "index_Trip_serviceId",
"unique": false,
"columnNames": [
"serviceId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_serviceId` ON `${TABLE_NAME}` (`serviceId`)"
},
{
"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": "Service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Shape",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"shapeId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "VersionMetadata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "lastUpdated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"type"
]
}
}
],
"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, 'c4be3d0c2a25f8c5c33132646a070d0e')"
]
}
}

View file

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

View file

@ -3,7 +3,11 @@ package moe.lava.banksia.room
import androidx.room.AutoMigration import androidx.room.AutoMigration
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters 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.driver.bundled.BundledSQLiteDriver
import androidx.sqlite.execSQL
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import moe.lava.banksia.room.converter.RouteTypeConverter import moe.lava.banksia.room.converter.RouteTypeConverter
@ -26,7 +30,7 @@ import moe.lava.banksia.room.entity.VersionMetadataEntity
import androidx.room.Database as DatabaseAnnotation import androidx.room.Database as DatabaseAnnotation
@DatabaseAnnotation( @DatabaseAnnotation(
version = 10, version = 11,
entities = [ entities = [
RouteEntity::class, RouteEntity::class,
ServiceEntity::class, ServiceEntity::class,
@ -59,7 +63,21 @@ abstract class Database : RoomDatabase() {
base.fallbackToDestructiveMigration(true) base.fallbackToDestructiveMigration(true)
.setDriver(BundledSQLiteDriver()) .setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO) .setQueryCoroutineContext(Dispatchers.IO)
.addMigrations(MIGRATION_10_11)
// .fallbackToDestructiveMigration(true) // .fallbackToDestructiveMigration(true)
.build() .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> suspend fun stops(id: String): List<StopEntity>
// I vibecoded this, sorry
@Query(""" @Query("""
SELECT Stop.* FROM Stop WITH Tree AS (
INNER JOIN Stop Child ON Child.parent == Stop.id SELECT Stop.* FROM Stop
INNER JOIN StopTime ON StopTime.stopId == Child.id INNER JOIN StopTime ON StopTime.stopId == Stop.id
INNER JOIN Trip ON Trip.id == StopTime.tripId INNER JOIN Trip ON Trip.id == StopTime.tripId
WHERE Trip.routeId == :id WHERE Trip.routeId == :id
GROUP BY Stop.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> suspend fun stopsParent(id: String): List<StopEntity>
} }

View file

@ -12,6 +12,13 @@ interface StopDao {
@Query("SELECT * FROM Stop") @Query("SELECT * FROM Stop")
suspend fun getAll(): List<StopEntity> 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") @Query("SELECT * FROM Stop WHERE id == :id")
suspend fun get(id: String): StopEntity? suspend fun get(id: String): StopEntity?
@ -29,4 +36,7 @@ interface StopDao {
@Query("DELETE FROM Stop") @Query("DELETE FROM Stop")
suspend fun deleteAll() 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 INNER JOIN Trip ON Trip.serviceId == Service.id
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
WHERE StopTime.tripId == Trip.id 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 AND ServiceException.type IS NULL
""") """)
suspend fun getForStopDated(stopId: String, days: Int, date: Int): List<StopTimeEntity> 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.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.Companion.SET_NULL
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import moe.lava.banksia.model.Stop import moe.lava.banksia.model.Stop
import moe.lava.banksia.util.Point 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( data class StopEntity(
@PrimaryKey val id: String, @PrimaryKey val id: String,
val name: String, val name: String,
val lat: Double, val lat: Double,
val lng: Double, val lng: Double,
@ColumnInfo(index = true) val parent: String, @ColumnInfo(index = true) val parent: String?,
val hasWheelChairBoarding: Boolean, val hasWheelChairBoarding: Boolean,
val level: String, val level: String,
val platformCode: 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(ServiceEntity::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = CASCADE),
ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE), ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE),
], ],
indices = [Index("shapeId")], indices = [Index("shapeId"), Index("serviceId")],
) )
data class TripEntity( data class TripEntity(
@PrimaryKey val id: String, @PrimaryKey val id: String,