feat(server/gtfs): service exception support

This commit is contained in:
Cilly Leang 2026-04-01 19:31:31 +11:00
parent c9aeeb99c1
commit 58649b6171
Signed by: cilly
GPG key ID: 6500251E087653C9
10 changed files with 595 additions and 4 deletions

View file

@ -21,12 +21,14 @@ import moe.lava.banksia.Constants
import moe.lava.banksia.model.Route import moe.lava.banksia.model.Route
import moe.lava.banksia.model.RouteType import moe.lava.banksia.model.RouteType
import moe.lava.banksia.model.Service import moe.lava.banksia.model.Service
import moe.lava.banksia.model.ServiceException
import moe.lava.banksia.model.Shape import moe.lava.banksia.model.Shape
import moe.lava.banksia.model.Stop import moe.lava.banksia.model.Stop
import moe.lava.banksia.model.StopTime import moe.lava.banksia.model.StopTime
import moe.lava.banksia.model.Trip import moe.lava.banksia.model.Trip
import moe.lava.banksia.server.gtfs.structures.GtfsRoute import moe.lava.banksia.server.gtfs.structures.GtfsRoute
import moe.lava.banksia.server.gtfs.structures.GtfsService import moe.lava.banksia.server.gtfs.structures.GtfsService
import moe.lava.banksia.server.gtfs.structures.GtfsServiceException
import moe.lava.banksia.server.gtfs.structures.GtfsShape import moe.lava.banksia.server.gtfs.structures.GtfsShape
import moe.lava.banksia.server.gtfs.structures.GtfsStop import moe.lava.banksia.server.gtfs.structures.GtfsStop
import moe.lava.banksia.server.gtfs.structures.GtfsStopTime import moe.lava.banksia.server.gtfs.structures.GtfsStopTime
@ -39,6 +41,7 @@ import kotlin.time.ExperimentalTime
sealed class GtfsData { sealed class GtfsData {
data class RouteChunk(val routes: List<Route>) : GtfsData() data class RouteChunk(val routes: List<Route>) : GtfsData()
data class ServiceChunk(val services: List<Service>) : GtfsData() data class ServiceChunk(val services: List<Service>) : GtfsData()
data class ServiceExceptionChunk(val exceptions: List<ServiceException>) : GtfsData()
data class ShapeChunk(val shapes: List<Shape>) : GtfsData() data class ShapeChunk(val shapes: List<Shape>) : GtfsData()
data class StopChunk(val stops: List<Stop>) : GtfsData() data class StopChunk(val stops: List<Stop>) : GtfsData()
data class StopTimeChunk(val stopTimes: List<StopTime>) : GtfsData() data class StopTimeChunk(val stopTimes: List<StopTime>) : GtfsData()
@ -77,7 +80,7 @@ class GtfsParser(
.listFiles { it.isDirectory } .listFiles { it.isDirectory }
.flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() } .flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() }
.ifEmpty { extractAll(datasetPath) } .ifEmpty { extractAll(datasetPath) }
.filter { it.parentFile.name == "2" } // .filter { it.parentFile.name == "2" }
} else { } else {
extractAll(datasetPath) extractAll(datasetPath)
} }
@ -115,6 +118,10 @@ class GtfsParser(
} }
.associateBy { it.id } .associateBy { it.id }
files
.filter { it.name == "calendar_dates.txt" }
.forEach { emit(GtfsData.ServiceExceptionChunk(parseServiceExceptions(it))) }
val trips = files val trips = files
.filter { it.name == "trips.txt" } .filter { it.name == "trips.txt" }
.flatMap { fd -> .flatMap { fd ->
@ -207,6 +214,16 @@ class GtfsParser(
) )
} } } }
private fun parseServiceExceptions(fd: File) =
fd.parseCsv<GtfsServiceException>()
.map { with(it) {
ServiceException(
serviceId = service_id,
date = LocalDate.parse(date, LocalDate.Formats.ISO_BASIC),
type = exception_type,
)
} }
private fun parseTrips(fd: File, services: Map<String, Service>) = private fun parseTrips(fd: File, services: Map<String, Service>) =
fd.parseCsv<GtfsTrip>() fd.parseCsv<GtfsTrip>()
.map { with(it) { .map { with(it) {

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.server.gtfs.structures
import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
internal data class GtfsServiceException(
val service_id: String,
val date: String,
val exception_type: Int,
)

View file

@ -5,6 +5,7 @@ import androidx.room.useWriterConnection
import io.ktor.util.logging.Logger import io.ktor.util.logging.Logger
import moe.lava.banksia.model.Route import moe.lava.banksia.model.Route
import moe.lava.banksia.model.Service import moe.lava.banksia.model.Service
import moe.lava.banksia.model.ServiceException
import moe.lava.banksia.model.Shape import moe.lava.banksia.model.Shape
import moe.lava.banksia.model.Stop import moe.lava.banksia.model.Stop
import moe.lava.banksia.model.StopTime import moe.lava.banksia.model.StopTime
@ -25,6 +26,7 @@ class GtfsImporter(
transactor.immediateTransaction { transactor.immediateTransaction {
database.routeDao.deleteAll() database.routeDao.deleteAll()
database.serviceDao.deleteAll() database.serviceDao.deleteAll()
database.serviceExceptionDao.deleteAll()
database.shapeDao.deleteAll() database.shapeDao.deleteAll()
database.stopDao.deleteAll() database.stopDao.deleteAll()
database.stopTimeDao.deleteAll() database.stopTimeDao.deleteAll()
@ -34,6 +36,7 @@ class GtfsImporter(
when (chunk) { when (chunk) {
is GtfsData.RouteChunk -> addRoutes(chunk.routes) is GtfsData.RouteChunk -> addRoutes(chunk.routes)
is GtfsData.ServiceChunk -> addServices(chunk.services) is GtfsData.ServiceChunk -> addServices(chunk.services)
is GtfsData.ServiceExceptionChunk -> addServiceExceptions(chunk.exceptions)
is GtfsData.ShapeChunk -> addShapes(chunk.shapes) is GtfsData.ShapeChunk -> addShapes(chunk.shapes)
is GtfsData.StopChunk -> addStops(chunk.stops) is GtfsData.StopChunk -> addStops(chunk.stops)
is GtfsData.StopTimeChunk -> addStopTimes(chunk.stopTimes) is GtfsData.StopTimeChunk -> addStopTimes(chunk.stopTimes)
@ -67,6 +70,13 @@ class GtfsImporter(
log.info("done") log.info("done")
} }
private suspend fun addServiceExceptions(exceptions: List<ServiceException>) {
val dao = database.serviceExceptionDao
log.info("inserting exceptions...")
dao.insertOrReplaceAll(*exceptions.map { it.asEntity() }.toTypedArray())
log.info("done")
}
private suspend fun addShapes(shapes: List<Shape>) { private suspend fun addShapes(shapes: List<Shape>) {
val dao = database.shapeDao val dao = database.shapeDao
log.info("inserting shapes...") log.info("inserting shapes...")

View file

@ -0,0 +1,477 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "5b90bc800bfae6d22124ea0a6a906ca7",
"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 NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))",
"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",
"notNull": true
},
{
"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`)"
}
]
},
{
"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_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, '5b90bc800bfae6d22124ea0a6a906ca7')"
]
}
}

View file

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

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.model
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class ServiceException(
val serviceId: String,
val date: LocalDate,
val type: Int,
)

View file

@ -9,6 +9,7 @@ import kotlinx.coroutines.IO
import moe.lava.banksia.room.converter.RouteTypeConverter import moe.lava.banksia.room.converter.RouteTypeConverter
import moe.lava.banksia.room.dao.RouteDao import moe.lava.banksia.room.dao.RouteDao
import moe.lava.banksia.room.dao.ServiceDao import moe.lava.banksia.room.dao.ServiceDao
import moe.lava.banksia.room.dao.ServiceExceptionDao
import moe.lava.banksia.room.dao.ShapeDao import moe.lava.banksia.room.dao.ShapeDao
import moe.lava.banksia.room.dao.StopDao import moe.lava.banksia.room.dao.StopDao
import moe.lava.banksia.room.dao.StopTimeDao import moe.lava.banksia.room.dao.StopTimeDao
@ -16,6 +17,7 @@ import moe.lava.banksia.room.dao.TripDao
import moe.lava.banksia.room.dao.VersionMetadataDao import moe.lava.banksia.room.dao.VersionMetadataDao
import moe.lava.banksia.room.entity.RouteEntity import moe.lava.banksia.room.entity.RouteEntity
import moe.lava.banksia.room.entity.ServiceEntity import moe.lava.banksia.room.entity.ServiceEntity
import moe.lava.banksia.room.entity.ServiceExceptionEntity
import moe.lava.banksia.room.entity.ShapeEntity import moe.lava.banksia.room.entity.ShapeEntity
import moe.lava.banksia.room.entity.StopEntity import moe.lava.banksia.room.entity.StopEntity
import moe.lava.banksia.room.entity.StopTimeEntity import moe.lava.banksia.room.entity.StopTimeEntity
@ -24,10 +26,11 @@ import moe.lava.banksia.room.entity.VersionMetadataEntity
import androidx.room.Database as DatabaseAnnotation import androidx.room.Database as DatabaseAnnotation
@DatabaseAnnotation( @DatabaseAnnotation(
version = 9, version = 10,
entities = [ entities = [
RouteEntity::class, RouteEntity::class,
ServiceEntity::class, ServiceEntity::class,
ServiceExceptionEntity::class,
ShapeEntity::class, ShapeEntity::class,
StopEntity::class, StopEntity::class,
StopTimeEntity::class, StopTimeEntity::class,
@ -37,6 +40,7 @@ import androidx.room.Database as DatabaseAnnotation
autoMigrations = [ autoMigrations = [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3), AutoMigration(from = 2, to = 3),
AutoMigration(from = 9, to = 10),
] ]
) )
@TypeConverters(RouteTypeConverter::class) @TypeConverters(RouteTypeConverter::class)
@ -44,6 +48,7 @@ abstract class Database : RoomDatabase() {
abstract val versionMetadataDao: VersionMetadataDao abstract val versionMetadataDao: VersionMetadataDao
abstract val routeDao: RouteDao abstract val routeDao: RouteDao
abstract val serviceDao: ServiceDao abstract val serviceDao: ServiceDao
abstract val serviceExceptionDao: ServiceExceptionDao
abstract val shapeDao: ShapeDao abstract val shapeDao: ShapeDao
abstract val stopDao: StopDao abstract val stopDao: StopDao
abstract val stopTimeDao: StopTimeDao 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.ServiceExceptionEntity
@Dao
interface ServiceExceptionDao {
@Query("SELECT * FROM ServiceException")
suspend fun getAll(): List<ServiceExceptionEntity>
@Query("SELECT * FROM ServiceException WHERE serviceId == :id")
suspend fun get(id: String): List<ServiceExceptionEntity>
@Insert
suspend fun insertAll(vararg exceptions: ServiceExceptionEntity)
@Insert(onConflict = REPLACE)
suspend fun insertOrReplaceAll(vararg exceptions: ServiceExceptionEntity)
@Delete
suspend fun delete(service: ServiceExceptionEntity)
@Query("DELETE FROM ServiceException")
suspend fun deleteAll()
}

View file

@ -22,11 +22,13 @@ interface StopTimeDao {
suspend fun getForStop(stopId: String): List<StopTimeEntity> suspend fun getForStop(stopId: String): List<StopTimeEntity>
@Query(""" @Query("""
SELECT * FROM StopTime SELECT DISTINCT StopTime.* FROM StopTime
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end` INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
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
WHERE StopTime.tripId == Trip.id WHERE StopTime.tripId == Trip.id
AND StopTime.stopId == :stopId AND StopTime.stopId == :stopId
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

@ -0,0 +1,28 @@
package moe.lava.banksia.room.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import kotlinx.datetime.LocalDate
import moe.lava.banksia.model.ServiceException
@Entity(
"ServiceException",
primaryKeys = ["serviceId", "date"]
)
data class ServiceExceptionEntity(
@ColumnInfo(index = true) val serviceId: String,
val date: Int,
@ColumnInfo(index = true) val type: Int,
) {
fun asModel() = ServiceException(
serviceId,
LocalDate.fromEpochDays(date),
type,
)
}
fun ServiceException.asEntity() = ServiceExceptionEntity(
serviceId,
date.toEpochDays().toInt(),
type,
)