Compare commits

..

No commits in common. "master" and "refactor/huge" have entirely different histories.

177 changed files with 4896 additions and 2635 deletions

2
.gitignore vendored
View file

@ -18,6 +18,6 @@ captures
**/xcshareddata/WorkspaceSettings.xcsettings
secrets.properties
/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt
shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt
/data/
/data

View file

@ -7,7 +7,6 @@ plugins {
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.sqldelight) apply false
alias(libs.plugins.wire) apply false
}

View file

@ -4,12 +4,11 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.sqldelight)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.sqld"
namespace = "moe.lava.banksia.client"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
@ -17,37 +16,28 @@ kotlin {
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
androidMain.dependencies {
implementation(libs.sqldelight.driver.android)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.play.services.location)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(projects.core)
}
nativeMain.dependencies {
implementation(libs.sqldelight.driver.native)
}
jvmMain.dependencies {
implementation(libs.sqldelight.driver.jvm)
}
}
}
sqldelight {
databases {
register("BanksiaDatabase") {
packageName.set("moe.lava.banksia.core.sqld")
schemaOutputDirectory.set(file("src/commonMain/sqldelight/schema"))
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(projects.shared)
}
}
}

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.client.data.route
import moe.lava.banksia.model.Route
import moe.lava.banksia.room.dao.RouteDao
import moe.lava.banksia.room.entity.asEntity
class RouteLocalDataSource(private val dao: RouteDao) {
suspend fun get(id: String) = dao.get(id)
suspend fun getAll() = dao.getAll()
suspend fun save(vararg routes: Route) = dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray())
}

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.client.data.route
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import moe.lava.banksia.model.Route
class RouteRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("routes/${id}").body<Route>()
suspend fun getAll() = client.get("routes").body<List<Route>>()
}

View file

@ -0,0 +1,12 @@
package moe.lava.banksia.client.data.stop
import moe.lava.banksia.model.Stop
import moe.lava.banksia.room.dao.RouteDao
import moe.lava.banksia.room.dao.StopDao
import moe.lava.banksia.room.entity.asEntity
class StopLocalDataSource(private val dao: StopDao, private val routeDao: RouteDao) {
suspend fun get(id: String) = dao.get(id)
suspend fun getByRoute(id: String) = routeDao.stops(id)
suspend fun save(vararg stops: Stop) = dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
}

View file

@ -1,11 +1,11 @@
package moe.lava.banksia.core.data.sources.stop
package moe.lava.banksia.client.data.stop
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.model.Stop
internal class StopRemoteDataSource(val client: HttpClient) {
class StopRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("stops/${id}").body<Stop>()
suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body<List<Stop>>()
}

View file

@ -0,0 +1,28 @@
package moe.lava.banksia.client.data.stoptime
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.model.StopTimeDated
import moe.lava.banksia.model.atDate
import moe.lava.banksia.room.dao.StopTimeDao
import moe.lava.banksia.util.serialise
import kotlin.time.Clock
class StopTimeLocalDataSource(
private val stopTimeDao: StopTimeDao,
) {
suspend fun getAtStop(
stopId: String,
date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
): List<StopTimeDated> {
return stopTimeDao
.getForStopDated(
stopId,
listOf(date.dayOfWeek).serialise(),
date.toEpochDays().toInt(),
)
.map { it.asModel().atDate(date) }
.sortedBy { it.departureTime }
}
}

View file

@ -0,0 +1,36 @@
package moe.lava.banksia.client.data.stoptime
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.model.StopTimeDated
import kotlin.time.Clock
class StopTimeRemoteDataSource(
private val client: HttpClient,
) {
suspend fun getAtStop(
stopId: String,
date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()),
): List<StopTimeDated> {
return client.get("stoptimes/by_stop/${stopId}") {
parameter("date", date)
}.body<List<StopTimeDated>>()
}
/*suspend fun get(
stop: String? = null,
trip: String? = null,
day: DayOfWeek? = Clock.System.todayIn(TimeZone.currentSystemDefault()).dayOfWeek,
): List<StopTime> {
return client.get("stoptimes") {
stop?.let { parameter("stop", it) }
trip?.let { parameter("trip", it) }
day?.let { parameter("day", it) }
}.body<List<StopTime>>()
}*/
}

View file

@ -0,0 +1,18 @@
package moe.lava.banksia.client.data.trip
import io.ktor.client.HttpClient
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.model.Trip
import kotlin.time.Clock
class TripRemoteDataSource(
private val client: HttpClient,
) {
suspend fun get(
day: DayOfWeek? = Clock.System.todayIn(TimeZone.currentSystemDefault()).dayOfWeek,
): List<Trip> {
return listOf()
}
}

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.core.data
package moe.lava.banksia.client.di
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend
@ -7,22 +7,22 @@ import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.plugin
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.data.repositories.ClientRouteRepository
import moe.lava.banksia.core.data.repositories.ClientStopRepository
import moe.lava.banksia.core.data.repositories.RouteRepository
import moe.lava.banksia.core.data.repositories.StopRepository
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
import moe.lava.banksia.core.util.log
import moe.lava.banksia.Constants
import moe.lava.banksia.client.data.route.RouteLocalDataSource
import moe.lava.banksia.client.data.route.RouteRemoteDataSource
import moe.lava.banksia.client.data.stop.StopLocalDataSource
import moe.lava.banksia.client.data.stop.StopRemoteDataSource
import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource
import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource
import moe.lava.banksia.client.repository.RouteRepository
import moe.lava.banksia.client.repository.StopRepository
import moe.lava.banksia.client.repository.StopTimeRepository
import moe.lava.banksia.data.ptv.PtvService
import moe.lava.banksia.util.log
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
actual val platformModule = module {
val ClientModule = module {
// HTTP Clients
singleOf(::PtvService)
single {
@ -49,8 +49,11 @@ actual val platformModule = module {
singleOf(::RouteRemoteDataSource)
singleOf(::StopLocalDataSource)
singleOf(::StopRemoteDataSource)
singleOf(::StopTimeLocalDataSource)
singleOf(::StopTimeRemoteDataSource)
// Repositories
singleOf(::ClientRouteRepository) bind RouteRepository::class
singleOf(::ClientStopRepository) bind StopRepository::class
singleOf(::RouteRepository)
singleOf(::StopRepository)
singleOf(::StopTimeRepository)
}

View file

@ -0,0 +1,25 @@
package moe.lava.banksia.client.repository
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.client.data.route.RouteLocalDataSource
import moe.lava.banksia.client.data.route.RouteRemoteDataSource
class RouteRepository(
private val local: RouteLocalDataSource,
private val remote: RouteRemoteDataSource,
) {
private val mutex = Mutex()
suspend fun getAll() = mutex.withLock {
local
.getAll()
.map { it.asModel() }
.ifEmpty {
remote
.getAll()
.also { local.save(*it.toTypedArray()) }
}
}
suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
}

View file

@ -0,0 +1,22 @@
package moe.lava.banksia.client.repository
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.client.data.stop.StopLocalDataSource
import moe.lava.banksia.client.data.stop.StopRemoteDataSource
class StopRepository(
private val local: StopLocalDataSource,
private val remote: StopRemoteDataSource,
) {
private val mutex = Mutex()
suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
suspend fun getByRoute(id: String) = mutex.withLock {
local
.getByRoute(id)
.map { it.asModel() }
.ifEmpty { null }
?: remote.getByRoute(id)
}
}

View file

@ -0,0 +1,16 @@
package moe.lava.banksia.client.repository
import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource
import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource
import moe.lava.banksia.model.StopTimeDated
class StopTimeRepository(
private val local: StopTimeLocalDataSource,
private val remote: StopTimeRemoteDataSource,
) {
suspend fun getForStop(id: String): List<StopTimeDated> {
return local
.getAtStop(id)
.ifEmpty { remote.getAtStop(id) }
}
}

View file

@ -1,64 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.data"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
val clientMain by creating {
dependsOn(commonMain.get())
}
androidMain.get().dependsOn(clientMain)
iosArm64Main.get().dependsOn(clientMain)
iosSimulatorArm64Main.get().dependsOn(clientMain)
commonMain.dependencies {
implementation(libs.koin.core)
implementation(projects.core)
api(projects.core.stoptime)
}
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.sqld)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}

View file

@ -1,36 +0,0 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.sqld.mappers.asModel
internal class ClientRouteRepository internal constructor(
private val local: RouteLocalDataSource,
private val remote: RouteRemoteDataSource,
) : RouteRepository {
private val mutex = Mutex()
override suspend fun getAll() = mutex.withLock {
local
.getAll()
.map { it.asModel() }
.ifEmpty {
remote
.getAll()
.also { local.save(*it.toTypedArray()) }
}
}
private val tripRouteMap = mutableMapOf<Long, Route>()
override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
override suspend fun getByPattern(patternId: Long) = mutex.withLock {
tripRouteMap[patternId]
?: remote.getByPattern(patternId).also {
local.save(it)
tripRouteMap[patternId] = it
}
}
}

View file

@ -1,23 +0,0 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
import moe.lava.banksia.core.sqld.mappers.asModel
internal class ClientStopRepository internal constructor(
private val local: StopLocalDataSource,
private val remote: StopRemoteDataSource,
) : StopRepository {
private val mutex = Mutex()
override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
override suspend fun getByRoute(id: String) = mutex.withLock {
local
.getByRoute(id)
.map { it.asModel() }
.ifEmpty { null }
?: remote.getByRoute(id)
}
}

View file

@ -1,23 +0,0 @@
package moe.lava.banksia.core.data.sources.route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.sqld.RouteQueries
import moe.lava.banksia.core.sqld.mappers.asDb
internal class RouteLocalDataSource(private val queries: RouteQueries) {
suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
suspend fun getAll() = withContext(Dispatchers.IO) { queries.getAll().executeAsList() }
// suspend fun getByTrip(tripId: String) = dao.getByTrip(tripId)
suspend fun save(vararg routes: Route) {
withContext(Dispatchers.IO) {
queries.transaction {
routes.forEach {
queries.insert(it.asDb())
}
}
}
}
}

View file

@ -1,12 +0,0 @@
package moe.lava.banksia.core.data.sources.route
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import moe.lava.banksia.core.model.Route
internal class RouteRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("routes/${id}").body<Route>()
suspend fun getByPattern(patternId: Long) = client.get("routes/by_pattern/${patternId}").body<Route>()
suspend fun getAll() = client.get("routes").body<List<Route>>()
}

View file

@ -1,22 +0,0 @@
package moe.lava.banksia.core.data.sources.stop
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.sqld.StopQueries
import moe.lava.banksia.core.sqld.mappers.asDb
internal class StopLocalDataSource(private val queries: StopQueries) {
suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
suspend fun getByRoute(id: String) = withContext(Dispatchers.IO) { queries.getByRoute(id).executeAsList() }
suspend fun save(vararg stops: Stop) {
withContext(Dispatchers.IO) {
queries.transaction {
stops.forEach {
queries.insert(it.asDb())
}
}
}
}
}

View file

@ -1,13 +0,0 @@
package moe.lava.banksia.core.data
import moe.lava.banksia.core.sqld.sqldDiModule
import org.koin.core.module.Module
import org.koin.dsl.module
internal expect val platformModule: Module
val dataDiModule = module {
includes(platformModule)
includes(sqldDiModule)
includes(stopTimeDataDiModule)
}

View file

@ -1,9 +0,0 @@
package moe.lava.banksia.core.data.repositories
import moe.lava.banksia.core.model.Route
interface RouteRepository {
suspend fun get(id: String): Route?
suspend fun getByPattern(patternId: Long): Route?
suspend fun getAll(): List<Route>
}

View file

@ -1,8 +0,0 @@
package moe.lava.banksia.core.data.repositories
import moe.lava.banksia.core.model.Stop
interface StopRepository {
suspend fun get(id: String): Stop
suspend fun getByRoute(id: String): List<Stop>
}

View file

@ -1,7 +0,0 @@
package moe.lava.banksia.core.data
import org.koin.dsl.module
internal actual val platformModule = module {
}

View file

@ -1,14 +0,0 @@
package moe.lava.banksia.core.sqld
import android.content.Context
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
actual class DatabaseManager : KoinComponent {
actual val database by lazy {
val ctx = get<Context>().applicationContext
val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "${DBNAME}.db")
BanksiaDatabase(driver)
}
}

View file

@ -1,7 +0,0 @@
package moe.lava.banksia.core.sqld
internal const val DBNAME = "timetable"
expect class DatabaseManager() {
val database: BanksiaDatabase
}

View file

@ -1,17 +0,0 @@
package moe.lava.banksia.core.sqld
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val sqldDiModule = module {
singleOf(::DatabaseManager)
factory { get<DatabaseManager>().database }
factory { get<BanksiaDatabase>().routeQueries }
factory { get<BanksiaDatabase>().serviceQueries }
factory { get<BanksiaDatabase>().serviceExceptionQueries }
factory { get<BanksiaDatabase>().shapeQueries }
factory { get<BanksiaDatabase>().stopQueries }
factory { get<BanksiaDatabase>().stoppingPatternQueries }
factory { get<BanksiaDatabase>().stopTimeQueries }
factory { get<BanksiaDatabase>().tripQueries }
}

View file

@ -1,14 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.model.RouteType
import moe.lava.banksia.core.sqld.Route as DbRoute
fun DbRoute.asModel() = Route(
id = id,
type = RouteType.from(type.toInt()),
number = number,
name = name,
)
fun Route.asDb() = DbRoute(id, type.value.toLong(), number, name)

View file

@ -1,21 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.util.deserialiseDaysBitflag
import moe.lava.banksia.core.util.serialise
import moe.lava.banksia.core.sqld.Service as DbService
fun DbService.asModel() = Service(
id = id,
days = days.toInt().deserialiseDaysBitflag(),
start = LocalDate.fromEpochDays(start),
end = LocalDate.fromEpochDays(end),
)
fun Service.asDb() = DbService(
id = id,
days = days.serialise().toLong(),
start = start.toEpochDays(),
end = end.toEpochDays(),
)

View file

@ -1,17 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.model.ServiceException
import moe.lava.banksia.core.sqld.ServiceException as DbServiceException
fun DbServiceException.asModel() = ServiceException(
serviceId = serviceId,
date = LocalDate.fromEpochDays(date),
type = type.toInt(),
)
fun ServiceException.asDb() = DbServiceException(
serviceId = serviceId,
type = date.toEpochDays(),
date = type.toLong(),
)

View file

@ -1,52 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Shape
import moe.lava.banksia.core.model.ShapePath
import moe.lava.banksia.core.util.Point
import moe.lava.banksia.core.sqld.Shape as DbShape
fun DbShape.asModel() = Shape(
id = id,
path = bytesToPath(path),
)
fun Shape.asDb() = DbShape(
id = id,
path = bytesFromPath(path),
)
private fun bytesToPath(value: ByteArray): ShapePath {
return value
.asSequence()
.asIterable()
.chunked(8) {
(it[0].toLong() and 0xFF) or
(it[1].toLong() and 0xFF shl 8) or
(it[2].toLong() and 0xFF shl 16) or
(it[3].toLong() and 0xFF shl 24) or
(it[4].toLong() and 0xFF shl 32) or
(it[5].toLong() and 0xFF shl 40) or
(it[6].toLong() and 0xFF shl 48) or
(it[7].toLong() and 0xFF shl 56)
}
.map { Double.fromBits(it) }
.chunked(2)
.map { (lat, lng) -> Point(lat, lng) }
.toList()
}
private fun bytesFromPath(path: ShapePath): ByteArray {
return path
.flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) }
.flatMap { i -> listOf(
i.toByte(),
(i shr 8).toByte(),
(i shr 16).toByte(),
(i shr 24).toByte(),
(i shr 32).toByte(),
(i shr 40).toByte(),
(i shr 48).toByte(),
(i shr 56).toByte(),
) }
.toByteArray()
}

View file

@ -1,26 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.util.Point
import moe.lava.banksia.core.sqld.Stop as DbStop
fun DbStop.asModel() = Stop(
id = id,
name = name,
pos = Point(lat, lng),
parent = parent,
hasWheelChairBoarding = hasWheelChairBoarding == 1L,
level = level,
platformCode = platformCode,
)
fun Stop.asDb() = DbStop(
id = id,
name = name,
lat = pos.lat,
lng = pos.lng,
parent = parent,
hasWheelChairBoarding = if (hasWheelChairBoarding) 1L else 0L,
level = level,
platformCode = platformCode
)

View file

@ -1,27 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.FutureTime
import moe.lava.banksia.core.model.FutureTime.Companion.asInt
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.sqld.StopTime as DbStopTime
fun DbStopTime.asModel() = StopTime(
patternId = patternId,
stopId = stopId,
time = TimeType.Undated(
arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()),
departure = FutureTime.fromInt(departureTime.toInt()),
),
pickupType = pickupType.toInt(),
dropOffType = dropOffType.toInt(),
)
fun StopTime.Undated.asDb() = DbStopTime(
patternId = patternId,
stopId = stopId,
arrivalDelta = (time.arrival.asInt() - time.departure.asInt()).toLong(),
departureTime = time.departure.asInt().toLong(),
pickupType = pickupType.toLong(),
dropOffType = dropOffType.toLong(),
)

View file

@ -1,23 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.sqld.StoppingPattern as DbStoppingPattern
fun <T: TimeType> DbStoppingPattern.asModel(stoptimes: List<StopTime<T>>) = StoppingPattern(
id = id,
routeId = routeId,
shapeId = shapeId,
headsign = headsign,
wheelchairAccessible = wheelchairAccessible == 1L,
stoptimes = stoptimes,
)
fun StoppingPattern<*>.asDb() = DbStoppingPattern(
id = id,
routeId = routeId,
shapeId = shapeId,
headsign = headsign,
wheelchairAccessible = if (wheelchairAccessible) 1L else 0L,
)

View file

@ -1,27 +0,0 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.Trip
import moe.lava.banksia.core.sqld.Trip as DbTrip
fun DbTrip.asModel(pattern: StoppingPattern.Undated, service: Service): Trip.Undated {
if (serviceId != service.id) {
throw IllegalArgumentException("trip and service id mismatch (${serviceId} != ${service.id})")
}
return Trip(
id = gtfsId,
pattern = pattern,
service = service,
directionId = directionId.toInt(),
blockId = blockId.toString(),
)
}
fun Trip.Undated.asDb() = DbTrip(
gtfsId = id,
patternId = pattern.id,
serviceId = service.id,
directionId = directionId.toLong(),
blockId = blockId?.toLong(),
)

View file

@ -1,20 +0,0 @@
CREATE TABLE Route (
id TEXT PRIMARY KEY NOT NULL,
type INTEGER NOT NULL,
number TEXT,
name TEXT NOT NULL
);
getAll:
SELECT * FROM Route;
get:
SELECT * FROM Route WHERE id == ?;
getByPattern:
SELECT Route.* FROM Route
INNER JOIN StoppingPattern ON Route.id == StoppingPattern.routeId
WHERE StoppingPattern.id == :patternId;
insert:
INSERT OR REPLACE INTO Route VALUES ?;

View file

@ -1,11 +0,0 @@
CREATE TABLE Service (
id TEXT PRIMARY KEY NOT NULL,
days INTEGER NOT NULL,
start INTEGER NOT NULL,
end INTEGER NOT NULL
);
CREATE INDEX idx_Service_days ON Service (days);
insert:
INSERT INTO Service VALUES ?;

View file

@ -1,9 +0,0 @@
CREATE TABLE ServiceException (
serviceId TEXT NOT NULL,
type INTEGER NOT NULL,
date INTEGER NOT NULL,
PRIMARY KEY (serviceId, type)
);
insert:
INSERT INTO ServiceException VALUES ?;

View file

@ -1,7 +0,0 @@
CREATE TABLE Shape (
id TEXT PRIMARY KEY NOT NULL,
path BLOB NOT NULL
);
insert:
INSERT INTO Shape VALUES ?;

View file

@ -1,54 +0,0 @@
CREATE TABLE Stop (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
lat REAL NOT NULL,
lng REAL NOT NULL,
parent TEXT REFERENCES Stop(id),
hasWheelChairBoarding INTEGER NOT NULL,
level TEXT,
platformCode TEXT
);
CREATE INDEX idx_Stop_parent ON Stop (parent);
getAll:
SELECT * FROM Stop;
getAllParentless:
SELECT * FROM Stop WHERE platformCode IS NOT NULL AND parent IS NULL;
get:
SELECT * FROM Stop WHERE id == ?;
getMany:
SELECT * FROM Stop WHERE id IN ?;
insert:
INSERT INTO Stop VALUES ?;
updateParents:
UPDATE Stop SET parent = ? WHERE id IN ?;
getByRoute:
SELECT Stop.* FROM Stop
INNER JOIN StopTime ON StopTime.stopId == Stop.id
INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
WHERE StoppingPattern.routeId == :id
GROUP BY Stop.id;
-- I vibecoded this, sorry
getParentsByRoute:
WITH RECURSIVE Tree AS (
SELECT Stop.* FROM Stop
INNER JOIN StopTime ON StopTime.stopId == Stop.id
INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
WHERE StoppingPattern.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;

View file

@ -1,45 +0,0 @@
CREATE TABLE StopTime (
patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
stopId TEXT NOT NULL REFERENCES Stop (id),
arrivalDelta INTEGER NOT NULL,
departureTime INTEGER NOT NULL,
pickupType INTEGER NOT NULL,
dropOffType INTEGER NOT NULL,
PRIMARY KEY (patternId, stopId)
) WITHOUT ROWID;
CREATE INDEX idx_StopTime_stopId ON StopTime (stopId);
insert:
INSERT OR REPLACE INTO StopTime VALUES ?;
getForStopDated:
SELECT DISTINCT StopTime.* FROM StopTime
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
INNER JOIN Trip ON Trip.serviceId == Service.id
INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;
getExtendedForStop:
SELECT DISTINCT
StopTime.patternId,
StopTime.arrivalDelta,
StopTime.departureTime,
StoppingPattern.headsign,
Route.type AS routeType,
Route.number AS routeNumber,
Route.name AS routeName,
Stop.platformCode AS stopPlatformCode
FROM StopTime
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
INNER JOIN Trip ON Trip.serviceId == Service.id
INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
INNER JOIN Route ON Route.id == StoppingPattern.routeId
INNER JOIN Stop ON Stop.id == StopTime.stopId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;

View file

@ -1,13 +0,0 @@
CREATE TABLE StoppingPattern (
id INTEGER PRIMARY KEY NOT NULL,
routeId TEXT NOT NULL REFERENCES Route (id),
shapeId TEXT NOT NULL REFERENCES Shape (id),
headsign TEXT NOT NULL,
wheelchairAccessible INTEGER NOT NULL
);
insert:
INSERT OR REPLACE INTO StoppingPattern VALUES ?;
get:
SELECT * FROM StoppingPattern WHERE id == :id;

View file

@ -1,13 +0,0 @@
CREATE TABLE Trip (
gtfsId TEXT PRIMARY KEY NOT NULL,
patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
serviceId TEXT NOT NULL REFERENCES Service (id),
blockId INTEGER,
directionId INTEGER NOT NULL
);
CREATE INDEX idx_Trip_patternId ON Trip (patternId);
CREATE INDEX idx_Trip_serviceId ON Trip (serviceId);
insert:
INSERT OR REPLACE INTO Trip VALUES ?;

View file

@ -1,11 +0,0 @@
package moe.lava.banksia.core.sqld
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import org.koin.core.component.KoinComponent
actual class DatabaseManager : KoinComponent {
actual val database by lazy {
val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "${DBNAME}.db")
BanksiaDatabase(driver)
}
}

View file

@ -1,56 +0,0 @@
package moe.lava.banksia.core.sqld
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import moe.lava.banksia.core.util.error
import org.koin.core.component.KoinComponent
import java.io.File
import java.util.Properties
import kotlin.system.exitProcess
actual class DatabaseManager : KoinComponent {
private var driver = connect()
actual val database get() = BanksiaDatabase(driver)
private fun connect(path: String = "./data/${DBNAME}.db") =
JdbcSqliteDriver("jdbc:sqlite:${path}", Properties(), BanksiaDatabase.Schema)
.apply { execute(null, "PRAGMA journal_mode = OFF;", 0) }
fun makeAlt() = run {
File("./data/${DBNAME}_alt.db").takeIf { it.exists() }?.delete()
val driver = connect("./data/${DBNAME}_alt.db")
BanksiaDatabase(driver) to { driver.close() }
}
fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) {
val live = File("./data/${DBNAME}.db")
val alt = File("./data/${DBNAME}_alt.db")
val old = File("./data/${DBNAME}_old.db")
if (live.takeIf { it.exists() }?.renameTo(old) == false) {
error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})")
return
}
if (alt.takeIf { it.exists() }?.renameTo(live) == false) {
error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})")
if (!live.renameTo(old)) {
error("DatabaseManager", "Failed to undo, critical failure, exiting..")
exitProcess(1)
}
return
}
val oldDriver = driver
driver = connect()
scope.launch {
delay(5000)
if (old.takeIf { it.exists() }?.delete() == false) {
error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})")
}
oldDriver.close()
}
}
}

View file

@ -1,3 +0,0 @@
package moe.lava.banksia.core.endpoints
object Endpoint

View file

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

View file

@ -1,45 +0,0 @@
package moe.lava.banksia.core.model
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
data class StopTime<T: TimeType>(
val patternId: Long,
val stopId: String,
val time: T,
val pickupType: Int,
val dropOffType: Int,
) {
typealias Dated = StopTime<TimeType.Dated>
typealias Undated = StopTime<TimeType.Undated>
}
@Serializable
sealed class TimeType {
@Serializable
data class Undated(
val arrival: FutureTime,
val departure: FutureTime,
) : TimeType()
@Serializable
data class Dated(
val arrival: LocalDateTime,
val departure: LocalDateTime,
) : TimeType()
}
fun TimeType.Undated.atDate(date: LocalDate) = TimeType.Dated(
arrival = arrival.atDate(date),
departure = departure.atDate(date),
)
fun StopTime<TimeType.Undated>.atDate(date: LocalDate) = StopTime(
patternId = patternId,
stopId = stopId,
time = time.atDate(date),
pickupType = pickupType,
dropOffType = dropOffType,
)

View file

@ -1,16 +0,0 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
data class StoppingPattern<T: TimeType>(
val id: Long,
val routeId: String,
val shapeId: String,
val headsign: String,
val wheelchairAccessible: Boolean,
val stoptimes: List<StopTime<T>>,
) {
typealias Dated = StoppingPattern<TimeType.Dated>
typealias Undated = StoppingPattern<TimeType.Undated>
}

View file

@ -1,15 +0,0 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
data class Trip<T: TimeType>(
val id: String,
val pattern: StoppingPattern<T>,
val service: Service,
val directionId: Int,
val blockId: String?,
) {
typealias Dated = Trip<TimeType.Dated>
typealias Undated = Trip<TimeType.Undated>
}

View file

@ -1,64 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.stoptime"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
freeCompilerArgs.add("-Xexpect-actual-classes")
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
val clientMain by creating {
dependsOn(commonMain.get())
}
androidMain.get().dependsOn(clientMain)
iosArm64Main.get().dependsOn(clientMain)
iosSimulatorArm64Main.get().dependsOn(clientMain)
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.sqld)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
jvmMain.dependencies {
implementation(libs.koin.ktor)
implementation(libs.ktor.server.core)
}
}
}

View file

@ -1,11 +0,0 @@
package moe.lava.banksia.core.data
import moe.lava.banksia.core.data.repositories.StopTimeRepository
import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
internal actual val platformModule = module {
singleOf(::StopTimeRepository)
singleOf(::StopTimeRemoteDataSource)
}

View file

@ -1,19 +0,0 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.flow.flow
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
actual class StopTimeRepository internal constructor(
private val local: StopTimeLocalDataSource,
private val remote: StopTimeRemoteDataSource,
) {
actual suspend fun getForStop(id: String, date: LocalDate) = flow {
emit(local.getAtStop(id, date))
remote.getAtStop(id, date)
.takeIf { it.isNotEmpty() }
?.let { emit(it) }
}
}

View file

@ -1,26 +0,0 @@
package moe.lava.banksia.core.data.sources.stoptime
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.core.data.dto.ExtendedStopTime
import moe.lava.banksia.core.endpoints.Endpoint
import moe.lava.banksia.core.endpoints.stopTimeByStop
import kotlin.time.Clock
internal class StopTimeRemoteDataSource(
private val client: HttpClient,
) {
suspend fun getAtStop(
stopId: String,
date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()),
): List<ExtendedStopTime> {
return client.get(Endpoint.stopTimeByStop(stopId)) {
parameter("date", date)
}.body<List<ExtendedStopTime>>()
}
}

View file

@ -1,13 +0,0 @@
package moe.lava.banksia.core.data
import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
import org.koin.core.module.Module
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
internal expect val platformModule: Module;
val stopTimeDataDiModule = module {
includes(platformModule)
singleOf(::StopTimeLocalDataSource)
}

View file

@ -1,34 +0,0 @@
package moe.lava.banksia.core.data.dto
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import moe.lava.banksia.core.model.FutureTime
import moe.lava.banksia.core.model.RouteType
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.model.atDate
import moe.lava.banksia.core.sqld.GetExtendedForStop
@Serializable
data class ExtendedStopTime(
val patternId: Long,
val stopPlatformCode: String?,
val time: TimeType.Dated,
val headsign: String?,
val routeType: RouteType,
val routeNumber: String?,
val routeName: String,
)
// TODO: This probably doesn't belong here
fun GetExtendedForStop.asModel(date: LocalDate) = ExtendedStopTime(
patternId = patternId,
stopPlatformCode = stopPlatformCode,
time = TimeType.Undated(
arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()),
departure = FutureTime.fromInt(departureTime.toInt()),
).atDate(date),
headsign = headsign,
routeType = RouteType.from(routeType.toInt()),
routeNumber = routeNumber,
routeName = routeName,
)

View file

@ -1,15 +0,0 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.core.data.dto.ExtendedStopTime
import kotlin.time.Clock
expect class StopTimeRepository {
suspend fun getForStop(
id: String,
date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
): Flow<List<ExtendedStopTime>>
}

View file

@ -1,30 +0,0 @@
package moe.lava.banksia.core.data.sources.stoptime
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.data.dto.ExtendedStopTime
import moe.lava.banksia.core.data.dto.asModel
import moe.lava.banksia.core.sqld.StopTimeQueries
import moe.lava.banksia.core.util.serialise
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
internal class StopTimeLocalDataSource : KoinComponent {
private val queries get() = get<StopTimeQueries>()
suspend fun getAtStop(stopId: String, date: LocalDate): List<ExtendedStopTime> {
return withContext(context = Dispatchers.IO) {
queries
.getExtendedForStop(
listOf(date.dayOfWeek).serialise().toLong(),
date.toEpochDays(),
stopId,
)
.executeAsList()
.map { it.asModel(date) }
.sortedBy { it.time.departure }
}
}
}

View file

@ -1,3 +0,0 @@
package moe.lava.banksia.core.endpoints
fun Endpoint.stopTimeByStop(stopId: String) = "stoptimes/by_stop/${stopId}"

View file

@ -1,9 +0,0 @@
package moe.lava.banksia.core.data
import moe.lava.banksia.core.data.repositories.StopTimeRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
internal actual val platformModule = module {
singleOf(::StopTimeRepository)
}

View file

@ -1,13 +0,0 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.flow.flow
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
actual class StopTimeRepository internal constructor(
private val local: StopTimeLocalDataSource,
) {
actual suspend fun getForStop(id: String, date: LocalDate) = flow {
emit(local.getAtStop(id, date))
}
}

View file

@ -1,27 +0,0 @@
package moe.lava.banksia.server.routes
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import kotlinx.coroutines.flow.first
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.core.data.repositories.StopTimeRepository
import moe.lava.banksia.core.endpoints.Endpoint
import moe.lava.banksia.core.endpoints.stopTimeByStop
import org.koin.ktor.ext.inject
import kotlin.time.Clock
fun Route.stopTimeRoutes() {
val repo by inject<StopTimeRepository>()
get(Endpoint.stopTimeByStop("{stop_id}")) {
val stopId = call.parameters["stop_id"]!!
val date = call.queryParameters["date"]
?.let { LocalDate.parse(it, LocalDate.Formats.ISO) }
?: Clock.System.todayIn(TimeZone.currentSystemDefault())
val data = repo.getForStop(stopId, date).first()
call.respond(data)
}
}

View file

@ -1,36 +1,40 @@
[versions]
agp = "9.1.0"
android-compileSdk = "37"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "37"
androidx-activity= "1.13.0"
androidx-lifecycle = "2.10.0"
compose-multiplatform = "1.12.0-alpha02"
android-targetSdk = "36"
androidx-activityCompose = "1.12.4"
androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.2.1"
androidx-core-ktx = "1.15.0"
androidx-espresso-core = "3.6.1"
androidx-lifecycle = "2.9.6"
androidx-material = "1.12.0"
androidx-test-junit = "1.2.1"
compose-multiplatform = "1.11.0-alpha02"
composeunstyled = "1.49.6"
coroutines = "1.10.2"
geo = "0.8.0"
koin = "4.2.0"
kotlin = "2.3.20"
junit = "4.13.2"
koin = "4.1.1"
kotlin = "2.3.10"
kotlinxDatetime = "0.7.1"
kotlinxSerializationCsv = "0.2.18"
kotlinxSerialization = "1.10.0"
ksp = "2.3.4"
ktor = "3.4.1"
ktor = "3.4.0"
logback = "1.5.32"
maplibre = "0.12.1"
material = "1.7.3"
material3 = "1.11.0-alpha07"
okio = "3.17.0"
material3 = "1.11.0-alpha02"
okio = "3.16.4"
playServicesLocation = "21.3.0"
sqlite = "2.6.2"
room = "2.8.4"
secretsGradlePlugin = "2.0.1"
sqldelight = "2.3.2"
wire = "6.1.0"
wire = "5.5.0"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" }
compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material" }
@ -40,11 +44,18 @@ compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-mu
compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" }
compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" }
composeunstyled = { module = "com.composables:composeunstyled", version.ref = "composeunstyled" }
moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
@ -62,14 +73,14 @@ ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "k
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre" }
moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
room-common = { group = "androidx.room", name = "room-common", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" }
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" }
[plugins]
@ -82,6 +93,6 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref =
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
room = { id = "androidx.room", version.ref = "room" }
secretsGradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
wire = { id = "com.squareup.wire", version.ref = "wire" }

View file

@ -5,27 +5,15 @@ plugins {
application
}
group = "moe.lava.banksia.server"
group = "moe.lava.banksia"
version = "1.0.0"
application {
mainClass.set("moe.lava.banksia.server.ApplicationKt")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}
dependencies {
implementation(projects.core)
implementation(projects.core.data)
implementation(projects.core.sqld)
implementation(projects.core.stoptime)
implementation(projects.server.gtfs)
implementation(projects.server.gtfsRt)
implementation(projects.shared)
implementation(libs.logback)
implementation(libs.koin.core)
implementation(libs.koin.ktor)
@ -38,6 +26,8 @@ dependencies {
implementation(libs.ktor.server.contentnegotiation)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
}

View file

@ -1,20 +0,0 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}
dependencies {
implementation(projects.core)
implementation(libs.kotlinx.serialization.csv)
implementation(libs.kotlinx.datetime)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
}

View file

@ -1,388 +0,0 @@
package moe.lava.banksia.server.gtfs
import com.lightningkite.kotlinx.serialization.csv.CsvFormat
import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig
import io.ktor.client.HttpClient
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsChannel
import io.ktor.util.cio.writeChannel
import io.ktor.util.logging.Logger
import io.ktor.utils.io.copyAndClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.serializer
import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.model.FutureTime.Companion.asInt
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.model.RouteType
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.model.ServiceException
import moe.lava.banksia.core.model.Shape
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.model.Trip
import moe.lava.banksia.core.util.Point
import moe.lava.banksia.server.gtfs.structures.GtfsRoute
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.GtfsStop
import moe.lava.banksia.server.gtfs.structures.GtfsStopTime
import moe.lava.banksia.server.gtfs.structures.GtfsTrip
import java.io.File
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.zip.ZipFile
import kotlin.time.ExperimentalTime
private typealias StopWithSource = Pair<String, Stop>
sealed class GtfsData {
data class RouteChunk(val routes: List<Route>) : 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 StopChunk(val stops: List<Stop>) : GtfsData()
data class TripChunk(val trips: List<Trip.Undated>) : GtfsData()
}
class GtfsParser(
private val log: Logger,
private val client: HttpClient,
) {
private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
private val datasetPath = File("/tmp/banksia", "dataset.zip")
@OptIn(ExperimentalTime::class)
suspend fun update(datasetUrl: String): Flow<GtfsData> {
val parentDir = datasetPath.parentFile
@Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions")
if (parentDir.exists() && !Constants.devMode)
parentDir.deleteRecursively()
parentDir.mkdirs()
log.info("fetching..")
client.prepareRequest {
url(datasetUrl)
}.execute { resp ->
if (!datasetPath.exists())
resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel())
log.info("fetched!")
}
log.info("extracting...")
@Suppress("KotlinConstantConditions")
val files = if (Constants.devMode) {
datasetPath.parentFile
.listFiles { it.isDirectory }
.flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() }
.ifEmpty { extractAll(datasetPath) }
// .filter { it.parentFile.name == "2" }
} else {
extractAll(datasetPath)
}
log.info("parsing...")
return parse(files)
.onCompletion {
@Suppress("KotlinConstantConditions")
if (!Constants.devMode) {
parentDir.deleteRecursively()
}
log.info("done!")
}
}
private fun parse(files: List<File>) = flow {
files
.filter { it.name == "routes.txt" }
.forEach { emit(GtfsData.RouteChunk(parseRoutes(it))) }
files
.filter { it.name == "stops.txt" }
.flatMap { parseStops(it) }
.let { emit(GtfsData.StopChunk(fixupDuplicateStops(it))) }
files
.filter { it.name == "shapes.txt" }
.forEach { emit(GtfsData.ShapeChunk(parseShapes(it))) }
val services = files
.filter { it.name == "calendar.txt" }
.flatMap { fd ->
parseServices(fd)
.also { emit(GtfsData.ServiceChunk(it)) }
}
.associateBy { it.id }
files
.filter { it.name == "calendar_dates.txt" }
.forEach { emit(GtfsData.ServiceExceptionChunk(parseServiceExceptions(it))) }
val trips = files
.filter { it.name == "trips.txt" }
.flatMap { fd ->
parseTrips(fd, services)
}
.associateBy { it.id }
files
.filter { it.name == "stop_times.txt" }
.forEach { fd ->
log.info("parsing stop times for ${fd.parent}...")
parseStopTimes(fd) { seq ->
val times = ArrayList<Pair<String, StopTime.Undated>>(1000100)
seq.forEach { pair ->
val (_, stoptime) = pair
if (times.size > 1000000 && stoptime.patternId == 1L) {
emit(GtfsData.TripChunk(processStoptimes(trips, times)))
times.clear()
}
times.add(pair)
}
emit(GtfsData.TripChunk(processStoptimes(trips, times)))
}
}
}
private fun hashCalc(headsign: String, stops: List<StopTime.Undated>): Long {
val inst = MessageDigest.getInstance("SHA-256")
inst.update(headsign.toByteArray())
stops.forEach {
inst.update(it.stopId.toByteArray())
val dint = it.time.departure.asInt()
inst.update((dint).toByte())
inst.update((dint shr 8).toByte())
val aint = it.time.arrival.asInt()
inst.update((aint).toByte())
inst.update((aint shr 8).toByte())
}
val buf = inst.digest().slice(0..7).toByteArray()
buf[0] = 0
buf[1] = 0
return ByteBuffer.wrap(buf).long
}
private fun processStoptimes(trips: Map<String, Trip.Undated>, times: ArrayList<Pair<String, StopTime.Undated>>) =
times.groupBy { it.first }
.map { (tripId, pairs) ->
val trip = trips[tripId]!!
val stoptimes = pairs.map { it.second }
val hash = hashCalc(trip.pattern.headsign, stoptimes)
trip.copy(pattern = trip.pattern.copy(
id = hash,
stoptimes = stoptimes.map { it.copy(patternId = hash) }
))
}
private fun parseRoutes(fd: File) =
fd.parseCsv<GtfsRoute>()
.map { with(it) {
Route(
id = route_id,
type = RouteType.from(fd.parentFile.name.toInt()),
number = route_short_name,
name = route_long_name,
)
} }
private fun parseShapes(fd: File) =
fd.parseCsv<GtfsShape>()
.groupBy { it.shape_id }
.map { (id, group) ->
val points = group
.sortedBy { it.shape_pt_sequence }
.map { Point(it.shape_pt_lat, it.shape_pt_lon) }
Shape(id, points)
}
private fun parseStops(fd: File): List<StopWithSource> =
fd.parseCsv<GtfsStop>()
.map { with(it) {
fd.parentFile.name to Stop(
id = stop_id,
name = stop_name,
pos = Point(stop_lat, stop_lon),
parent = parent_station.ifEmpty { null },
hasWheelChairBoarding = wheelchair_boarding == "1",
level = level_id.ifEmpty { null },
platformCode = platform_code.ifEmpty { null },
)
} }
private inline fun parseStopTimes(fd: File, block: (Sequence<Pair<String, StopTime.Undated>>) -> Unit) =
fd.parseCsvSequence<GtfsStopTime> { seq ->
seq
.map { with(it) {
it.trip_id to StopTime(
patternId = stop_sequence,
stopId = stop_id,
time = TimeType.Undated(
arrival = GtfsStopTime.parseGtfsTime(arrival_time),
departure = GtfsStopTime.parseGtfsTime(departure_time),
),
pickupType = pickup_type,
dropOffType = drop_off_type,
)
} }
.let { block(it) }
}
private fun parseServices(fd: File) =
fd.parseCsv<GtfsService>()
.map { with(it) {
val days = buildList {
if (monday == 1) add(DayOfWeek.MONDAY)
if (tuesday == 1) add(DayOfWeek.TUESDAY)
if (wednesday == 1) add(DayOfWeek.WEDNESDAY)
if (thursday == 1) add(DayOfWeek.THURSDAY)
if (friday == 1) add(DayOfWeek.FRIDAY)
if (saturday == 1) add(DayOfWeek.SATURDAY)
if (sunday == 1) add(DayOfWeek.SUNDAY)
}
Service(
id = "${fd.parentFile.name}_${service_id}",
days = days,
start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC),
end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC),
)
} }
private fun parseServiceExceptions(fd: File) =
fd.parseCsv<GtfsServiceException>()
.map { with(it) {
ServiceException(
serviceId = "${fd.parentFile.name}_${service_id}",
date = LocalDate.parse(date, LocalDate.Formats.ISO_BASIC),
type = exception_type,
)
} }
private fun parseTrips(fd: File, services: Map<String, Service>) =
fd.parseCsv<GtfsTrip>()
.map { with(it) {
Trip.Undated(
id = trip_id,
pattern = StoppingPattern(
id = 0,
routeId = route_id,
shapeId = shape_id,
headsign = trip_headsign,
wheelchairAccessible = wheelchair_accessible == "1",
stoptimes = listOf()
),
service = services["${fd.parentFile.name}_${service_id}"]!!,
directionId = direction_id.toInt(),
blockId = block_id.ifEmpty { null },
)
} }
private fun extract(fd: File): List<File> {
val outputs = mutableListOf<File>()
ZipFile(fd).use { zip ->
for (entry in zip.entries()) {
zip.getInputStream(entry).use { input ->
val out = File(fd.parentFile, entry.name)
out.parentFile.mkdirs()
out.outputStream().use { output ->
input.copyTo(output)
}
outputs.add(out)
}
}
}
return outputs
}
private fun extractAll(fd: File) = extract(fd).flatMap(::extract)
private inline fun <reified T> File.parseCsv(): List<T> = this
.readText()
.replace("\uFEFF", "") // remove bom
.replace("\r\n", "\n") // crlf -> lf
.let { csv.decodeFromString(it) }
private inline fun <reified T> File.parseCsvSequence(block: (Sequence<T>) -> Unit) = this
.bufferedReader()
.use { reader ->
val iter = object : CharIterator() {
var next: Char? = null
override fun nextChar(): Char {
if (!hasNext()) {
throw NoSuchElementException()
}
val ret = next!!
next = null
return ret
}
override fun hasNext(): Boolean {
if (next == null) {
do {
next = null
val new = reader.read()
if (new != -1) {
next = new.toChar()
}
} while (next == '\uFEFF' || next == '\r')
}
return next != null
}
}
block(csv.decodeToSequence(iter, csv.serializersModule.serializer()))
}
// Type priority used to resolve duplicates, preferring the first one in the chain
private val typePriorityRanking = listOf(
RouteType.MetroTrain,
RouteType.RegionalTrain,
RouteType.MetroTram,
RouteType.MetroBus,
RouteType.RegionalBus,
RouteType.SkyBus,
).map { it.value.toString() }
@Suppress("LoggingStringTemplateAsArgument") // ?
private fun fixupDuplicateStops(stops: List<StopWithSource>): List<Stop> {
return stops
.groupBy { (_, stops) -> stops.id }
.map { (id, stops) ->
// Just return it if no duplicate
if (stops.size == 1) return@map stops[0].second
// Just return the first one if all the stops' data match
if (stops.withIndex().all { (idx, stop) -> idx == 0 || stop.second == stops[idx - 1].second })
return@map stops[0].second
// Find first stop ordered by the types
val res = typePriorityRanking
.firstNotNullOfOrNull { type ->
stops.find { it.first == type }
}
val (_, stop) = if (res == null) {
log.warn("Cannot resolve duplicate stop ${id}, using first one")
stops.forEach { (type, stop) -> log.warn(" - ($type): $stop") }
stops[0]
} else {
log.debug("Resolving $id for type ${res.first}")
stops.forEach { (type, stop) -> log.debug("${if (res.first == type) "*" else " "} - ($type): $stop") }
res
}
stop
}
}
}

View file

@ -1,11 +0,0 @@
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

@ -1,32 +0,0 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.wire)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}
dependencies {
implementation(projects.core)
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
}
wire {
sourcePath {
srcDir("src/main/proto")
}
kotlin {}
}

View file

@ -1,116 +0,0 @@
package moe.lava.banksia.server.gtfsrt
import com.google.transit.realtime.FeedMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import moe.lava.banksia.core.util.log
import java.io.File
import kotlin.time.Instant
private const val BASE_DIR = "./data/gtfsr-archive/"
internal class GtfsrtArchiver {
private var started = false
suspend fun start(flow: SharedFlow<Pair<String, ByteArray>>) {
if (started) {
log("GtfsrtArchiver", "Tried to start when already started")
return
}
started = true
coroutineScope {
launch { compressJob() }
flow.collect { (type, rawData) ->
val data = try {
FeedMessage.ADAPTER.decode(rawData)
} catch (e: Throwable) {
log("gtfsr $type", "Failed to parse proto: $e")
return@collect
}
val timestamp = data.header_.timestamp
?: return@collect log("gtfsr $type", "Failed to read proto timestamp")
val time = Instant.fromEpochSeconds(timestamp).toLocalDateTime(TimeZone.currentSystemDefault())
val (prevWeek, prevDay) = (time.dayOfYear - 1) / 7 to (time.dayOfYear - 1) % 7
val (nextWeek, nextDay) = time.dayOfYear / 7 to time.dayOfYear % 7
val base = File(BASE_DIR, type)
val previousParent = File(base, "${time.year}-${prevWeek.toString().padStart(2, '0')}/${prevDay}")
val currentParent = File(base, "${time.year}-${nextWeek.toString().padStart(2, '0')}/${nextDay}")
val target = File(currentParent, "${timestamp}.proto")
if (previousParent.isDirectory) {
enqueueCompression(previousParent)
if (prevWeek != nextWeek) {
enqueueCompression(previousParent.parentFile)
}
}
if (!target.exists()) {
try {
if (!target.parentFile.isDirectory) {
target.parentFile.mkdirs()
}
target.writeBytes(rawData)
} catch (e: Throwable) {
log("gtfsr $type", "Failed to write ${target}: $e")
}
}
}
}
}
private val cqueue = mutableSetOf<File>()
private val ignore = mutableSetOf<File>()
private val cmut = Mutex()
private suspend fun enqueueCompression(fd: File) {
cmut.withLock { cqueue.add(fd) }
}
private suspend fun compressJob() {
while(true) {
while(true) {
val next = cmut.withLock { cqueue.firstOrNull() }
?: break
if (!next.isDirectory) {
cmut.withLock { cqueue.remove(next) }
continue
}
if (next in ignore) continue
withContext(Dispatchers.IO) {
val proc = ProcessBuilder(
"tar", "-acf",
"${next.absolutePath}.tar.zst",
next.absolutePath
).start()
val exitCode = proc.waitFor()
if (exitCode == 0) {
log("CompressJob", "Compressed ${next.absolutePath} to ${next.absolutePath}.tar.zst")
if (next.deleteRecursively()) {
cmut.withLock { cqueue.remove(next) }
} else {
log("CompressJob", "Failed to delete $next")
ignore.add(next)
}
} else {
val msg = proc.errorStream.readAllBytes().decodeToString()
log("CompressJob", "Failed to delete $next (exit code $exitCode")
log("CompressJob", msg)
}
}
}
delay(30000)
}
}
}

View file

@ -1,87 +0,0 @@
package moe.lava.banksia.server.gtfsrt
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.readRawBytes
import io.ktor.http.isSuccess
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.util.LogScope
import moe.lava.banksia.core.util.log
private val types = arrayOf(
"metro/trip-updates",
"metro/vehicle-positions",
"metro/service-alerts",
"tram/trip-updates",
"tram/vehicle-positions",
"tram/service-alerts",
"bus/trip-updates",
"bus/vehicle-positions",
"vline/trip-updates",
"vline/vehicle-positions",
)
class GtfsrtService(
private val client: HttpClient,
) {
private val archiver = GtfsrtArchiver()
private var started = false
internal val rawMessages: SharedFlow<Pair<String, ByteArray>>
field = MutableSharedFlow<Pair<String, ByteArray>>()
fun start(
scope: CoroutineScope,
enableArchiving: Boolean = false,
) {
if (started) {
log("GtfsrtService", "Tried to start when already started")
return
}
if (enableArchiving) {
scope.launch { archiver.start(rawMessages) }
}
scope.launch { fetch() }
}
private suspend fun fetch() {
coroutineScope {
types.map { type ->
launch(context = Dispatchers.IO) {
val logger = LogScope("gtfsr $type")
try {
val res = client.get {
url("https://api.opendata.transport.vic.gov.au/opendata/public-transport/gtfs/realtime/v1/${type}")
header("KeyId", Constants.opendataKey)
}
if (!res.status.isSuccess()) {
logger.log("${res.status} | ${res.bodyAsText()}")
} else {
val bytes = res.readRawBytes()
rawMessages.emit(type to bytes)
}
} catch (e: Throwable) {
logger.log("$e")
logger.log(e.stackTraceToString())
}
}
}.joinAll()
}
delay(10000)
fetch()
}
}

View file

@ -15,59 +15,47 @@ import io.ktor.server.routing.routing
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.sqld.RouteQueries
import moe.lava.banksia.core.sqld.StopQueries
import moe.lava.banksia.core.sqld.mappers.asModel
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.Constants
import moe.lava.banksia.di.CommonModules
import moe.lava.banksia.model.atDate
import moe.lava.banksia.room.dao.RouteDao
import moe.lava.banksia.room.dao.StopDao
import moe.lava.banksia.room.dao.StopTimeDao
import moe.lava.banksia.room.dao.VersionMetadataDao
import moe.lava.banksia.server.di.ServerModules
import moe.lava.banksia.server.gtfsrt.GtfsrtService
import moe.lava.banksia.server.routes.stopTimeRoutes
import moe.lava.banksia.server.gtfs.GtfsHandler
import moe.lava.banksia.server.gtfsr.GtfsrService
import moe.lava.banksia.util.serialise
import org.koin.dsl.module
import org.koin.ktor.ext.get
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin
import kotlin.time.Clock
fun main() {
if (System.getenv("BANKSIA_PRODUCTION") == "1") Constants.devMode = false
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
log.info("devMode: ${Constants.devMode}")
install(ContentNegotiation) {
json()
}
install(Koin) {
modules(module { single { log } })
modules(ServerModules)
modules(CommonModules, ServerModules)
}
@Suppress("KotlinConstantConditions")
launch { get<GtfsrtService>().start(this, !Constants.devMode) }
if (!Constants.devMode) {
val gtfsr by inject<GtfsrService>()
launch { gtfsr.start() }
}
routing {
stopTimeRoutes()
if (Constants.devMode) {
get("/fixup") {
call.respondText("received")
get<GtfsDataFixer>().addParentsToStops()
}
}
get("/manage/fixup") {
val key = call.parameters["key"]
if (key != Constants.updateKey) {
call.respond(HttpStatusCode.Forbidden)
return@get
}
call.respondText("fixing")
launch(context = Dispatchers.IO) {
get<GtfsDataFixer>().addParentsToStops()
}
}
get("/manage/update") {
get("/update") {
val key = call.parameters["key"]
if (key != Constants.updateKey) {
call.respond(HttpStatusCode.Forbidden)
@ -79,14 +67,30 @@ 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) {
get<GtfsImporter>().import(datasetUrl)
get<GtfsDataFixer>().addParentsToStops()
val handler by inject<GtfsHandler>()
handler.update(datasetUrl)
}
}
get("/metadata/{type?}") {
val dao by inject<VersionMetadataDao>()
val type = call.parameters["type"]
if (type == null) {
call.respond(dao.getAll().map { it.asModel() })
return@get
}
val data = dao.get(type)?.asModel()
if (data == null) {
call.respond(HttpStatusCode.NotFound)
} else {
call.respond(data)
}
}
get("/routes") {
val routes = withContext(context = Dispatchers.IO) {
get<RouteQueries>().getAll().executeAsList()
inject<RouteDao>().value.getAll()
}
val res = routes.map { it.asModel() }
call.respond(res)
@ -94,17 +98,16 @@ fun Application.module() {
get("/routes/{route_id}") {
val routeId = call.parameters["route_id"]!!
val route = withContext(context = Dispatchers.IO) {
get<RouteQueries>().get(routeId).executeAsOneOrNull()
inject<RouteDao>().value.get(routeId)
}
if (route != null) {
if (route != null)
call.respond(route.asModel())
} else {
else
call.respond(HttpStatusCode.NotFound)
}
}
get("/stops") {
val routes = withContext(context = Dispatchers.IO) {
get<StopQueries>().getAll().executeAsList()
inject<StopDao>().value.getAll()
}
val res = routes.map { it.asModel() }
call.respond(res)
@ -112,26 +115,56 @@ fun Application.module() {
get("/stops/{stop_id}") {
val stopId = call.parameters["stop_id"]!!
val stop = withContext(context = Dispatchers.IO) {
get<StopQueries>().get(stopId).executeAsOneOrNull()
inject<StopDao>().value.get(stopId)
}
if (stop != null) {
if (stop != null)
call.respond(stop.asModel())
} else {
else
call.respond(HttpStatusCode.NotFound)
}
}
get("/route_stops/{route_id}") {
val routeId = call.parameters["route_id"]!!
val useParent = call.queryParameters["parent"] !in listOf("false", "0")
val useParent = call.queryParameters["parent"] in listOf("true", "1")
val stops = withContext(Dispatchers.IO) {
val queries = get<StopQueries>()
if (useParent) {
queries.getParentsByRoute(routeId).executeAsList()
} else {
queries.getByRoute(routeId).executeAsList()
}
val routeDao by inject<RouteDao>()
if (useParent)
routeDao.stopsParent(routeId)
else
routeDao.stops(routeId)
}
call.respond(stops.map { it.asModel() })
// val stops = withContext(Dispatchers.IO) {
// val stopDao by inject<StopDao>()
// val stopTimeDao by inject<StopTimeDao>()
// val tripDao by inject<TripDao>()
//
// tripDao.getByRoute(routeId)
// .map { it.id }
// .let { stopTimeDao.get(it) }
// .flatMap { it.asModel().stopInfos }
// .map { it.stopId }
// .let { stopDao.get(it) }
// .map { it.asModel() }
// }
// call.respond(stops)
}
get("/stoptimes/by_stop/{stop_id}") {
val stopId = call.parameters["stop_id"]!!
val date = call.queryParameters["date"]
?.let { LocalDate.parse(it, LocalDate.Formats.ISO) }
?: Clock.System.todayIn(TimeZone.currentSystemDefault())
val times = withContext(context = Dispatchers.IO) {
inject<StopTimeDao>().value
.getForStopDated(
stopId,
listOf(date.dayOfWeek).serialise(),
date.toEpochDays().toInt(),
)
.map { it.asModel().atDate(date) }
.sortedBy { it.departureTime }
}
call.respond(times)
}
}
}

View file

@ -1,45 +0,0 @@
package moe.lava.banksia.server
import moe.lava.banksia.core.sqld.BanksiaDatabase
import moe.lava.banksia.core.util.log
import java.security.MessageDigest
import moe.lava.banksia.core.sqld.Stop as DbStop
class GtfsDataFixer(
private val database: BanksiaDatabase,
) {
fun addParentsToStops() {
val queries = database.stopQueries
val stops = queries.getAllParentless().executeAsList()
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 = DbStop(
id = parentId,
name = name,
lat = avgLat,
lng = avgLng,
parent = null,
hasWheelChairBoarding = if (stops.all { it.hasWheelChairBoarding == 1L }) 1L else 0L,
level = "",
platformCode = "",
)
log("datafixer", "inserting ${parentId} for ${stops.size} children")
queries.transaction {
queries.insert(parent)
queries.updateParents(parentId, stops.map { it.id })
}
}
}
}
private fun String.sha256() =
MessageDigest
.getInstance("SHA-256")
.digest(this.toByteArray())
.joinToString("") { "%02x".format(it) }

View file

@ -1,115 +0,0 @@
package moe.lava.banksia.server
import io.ktor.util.logging.Logger
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.model.ServiceException
import moe.lava.banksia.core.model.Shape
import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.model.Trip
import moe.lava.banksia.core.sqld.DatabaseManager
import moe.lava.banksia.core.sqld.mappers.asDb
import moe.lava.banksia.server.gtfs.GtfsData
import moe.lava.banksia.server.gtfs.GtfsParser
import kotlin.time.Clock
import moe.lava.banksia.core.sqld.BanksiaDatabase as Database
class GtfsImporter(
private val parser: GtfsParser,
private val dbm: DatabaseManager,
private val log: Logger,
) {
suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) {
val (database, close) = dbm.makeAlt()
parser.update(url).collect { chunk ->
when (chunk) {
is GtfsData.RouteChunk -> database.addRoutes(chunk.routes)
is GtfsData.ServiceChunk -> database.addServices(chunk.services)
is GtfsData.ServiceExceptionChunk -> database.addServiceExceptions(chunk.exceptions)
is GtfsData.ShapeChunk -> database.addShapes(chunk.shapes)
is GtfsData.StopChunk -> database.addStops(chunk.stops)
is GtfsData.TripChunk -> database.addTrips(chunk.trips)
}
}
close()
dbm.swap()
}
private fun Database.addRoutes(routes: List<Route>) {
log.info("inserting routes...")
routeQueries.transaction {
routes.forEach {
routeQueries.insert(it.asDb())
}
}
log.info("done")
}
private fun Database.addServices(services: List<Service>) {
log.info("inserting services...")
serviceQueries.transaction {
services.forEach {
serviceQueries.insert(it.asDb())
}
}
log.info("done")
}
private fun Database.addServiceExceptions(exceptions: List<ServiceException>) {
log.info("inserting exceptions...")
serviceExceptionQueries.transaction {
exceptions.forEach {
serviceExceptionQueries.insert(it.asDb())
}
}
log.info("done")
}
private fun Database.addShapes(shapes: List<Shape>) {
log.info("inserting shapes...")
shapeQueries.transaction {
shapes.forEach {
shapeQueries.insert(it.asDb())
}
}
log.info("done")
}
private fun Database.addStops(stops: List<Stop>) {
log.info("inserting stops...")
stops
.groupBy { it.id }
.forEach { (id, gstops) ->
if (gstops.size > 1) {
if (gstops.withIndex().any { (i, stop) -> i != 0 && stop != gstops[i - 1] }) {
gstops.forEach {
log.warn("duplicate $id: $it")
}
}
}
}
stopQueries.transaction {
stops.forEach {
stopQueries.insert(it.asDb())
}
}
log.info("done")
}
private fun Database.addTrips(trips: List<Trip.Undated>) {
log.info("inserting ${trips.size} trips...")
transaction {
trips.forEach { trip ->
stoppingPatternQueries.insert(trip.pattern.asDb())
trip.pattern.stoptimes.forEach { stoptime ->
stopTimeQueries.insert(stoptime.asDb())
}
tripQueries.insert(trip.asDb())
}
}
log.info("done")
}
}

View file

@ -1,22 +1,13 @@
package moe.lava.banksia.server.di
import io.ktor.client.HttpClient
import moe.lava.banksia.core.data.dataDiModule
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
import org.koin.core.module.dsl.factoryOf
import moe.lava.banksia.server.gtfs.GtfsHandler
import moe.lava.banksia.server.gtfsr.GtfsrService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val ServerModules = module {
includes(dataDiModule)
single { HttpClient() }
singleOf(::GtfsParser)
singleOf(::GtfsrtService)
factoryOf(::GtfsDataFixer)
factoryOf(::GtfsImporter)
singleOf(::GtfsHandler)
singleOf(::GtfsrService)
}

View file

@ -0,0 +1,336 @@
package moe.lava.banksia.server.gtfs
import com.lightningkite.kotlinx.serialization.csv.CsvFormat
import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig
import io.ktor.client.HttpClient
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsChannel
import io.ktor.util.cio.writeChannel
import io.ktor.util.logging.Logger
import io.ktor.utils.io.copyAndClose
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.serializer
import moe.lava.banksia.Constants
import moe.lava.banksia.model.Route
import moe.lava.banksia.model.Service
import moe.lava.banksia.model.Shape
import moe.lava.banksia.model.Stop
import moe.lava.banksia.model.StopTime
import moe.lava.banksia.model.Trip
import moe.lava.banksia.room.Database
import moe.lava.banksia.room.converter.RouteTypeConverter
import moe.lava.banksia.room.entity.asEntity
import moe.lava.banksia.server.gtfs.structures.GtfsRoute
import moe.lava.banksia.server.gtfs.structures.GtfsService
import moe.lava.banksia.server.gtfs.structures.GtfsShape
import moe.lava.banksia.server.gtfs.structures.GtfsStop
import moe.lava.banksia.server.gtfs.structures.GtfsStopTime
import moe.lava.banksia.server.gtfs.structures.GtfsTrip
import moe.lava.banksia.util.Point
import java.io.File
import java.util.zip.ZipFile
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
class GtfsHandler(
private val log: Logger,
private val client: HttpClient,
private val db: Database,
) {
private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule()))
private val datasetPath = File("/tmp/banksia", "dataset.zip")
@OptIn(ExperimentalTime::class)
suspend fun update(datasetUrl: String, date: Long? = null) {
val parentDir = datasetPath.parentFile
@Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions")
if (parentDir.exists() && !Constants.devMode)
parentDir.deleteRecursively()
parentDir.mkdirs()
log.info("fetching..")
client.prepareRequest {
url(datasetUrl)
}.execute { resp ->
if (!datasetPath.exists())
resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel())
log.info("fetched!")
}
log.info("extracting...")
@Suppress("KotlinConstantConditions")
val files = if (Constants.devMode) {
datasetPath.parentFile
.listFiles { it.isDirectory }
.flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() }
.ifEmpty { extractAll(datasetPath) }
.filter { it.parentFile.name == "2" }
} else {
extractAll(datasetPath)
}
addRoutes(files)
addStops(files)
addShapes(files)
val services = addServices(files)
val trips = addTrips(files, services.associateBy { it.id })
addStopTimes(files, trips.associateBy { it.id })
updateMetadata(date ?: Clock.System.now().epochSeconds)
@Suppress("KotlinConstantConditions")
if (!Constants.devMode) {
parentDir.deleteRecursively()
}
log.info("done!")
}
private suspend fun updateMetadata(date: Long) {
val dao = db.versionMetadataDao
log.info("updating metadata...")
dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times"))
}
private suspend fun addRoutes(files: List<File>) {
val dao = db.routeDao
log.info("parsing routes...")
val routes = files
.filter { it.name == "routes.txt" }
.flatMap { fd -> parseRoutes(fd) }
log.info("inserting routes...")
dao.deleteAll()
dao.insertAll(*routes.map { it.asEntity() }.toTypedArray())
}
private fun parseRoutes(fd: File) =
fd.parseCsv<GtfsRoute>()
.map { with(it) {
Route(
id = route_id,
type = RouteTypeConverter.from(fd.parentFile.name.toInt()),
number = route_short_name,
name = route_long_name,
)
} }
private suspend fun addShapes(files: List<File>) {
val dao = db.shapeDao
log.info("parsing shapes...")
val shapes = files
.filter { it.name == "shapes.txt" }
.flatMap { fd -> parseShapes(fd) }
log.info("inserting shapes...")
dao.deleteAll()
dao.insertAll(*shapes.map { it.asEntity() }.toTypedArray())
}
private fun parseShapes(fd: File) =
fd.parseCsv<GtfsShape>()
.groupBy { it.shape_id }
.map { (id, group) ->
val points = group
.sortedBy { it.shape_pt_sequence }
.map { Point(it.shape_pt_lat, it.shape_pt_lon) }
Shape(id, points)
}
private suspend fun addStops(files: List<File>) {
val dao = db.stopDao
log.info("parsing stops...")
val stops = files
.filter { it.name == "stops.txt" }
.flatMap { fd -> parseStops(fd) }
log.info("inserting stops...")
dao.deleteAll()
stops
.groupBy { it.id }
.forEach { (id, gstops) ->
if (gstops.size > 1) {
if (gstops.withIndex().any { (i, stop) -> i != 0 && stop != gstops[i - 1] }) {
gstops.forEach {
log.info("duplicate $id: $it")
}
}
}
}
dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
}
private fun parseStops(fd: File) =
fd.parseCsv<GtfsStop>()
.map { with(it) {
Stop(
id = stop_id,
name = stop_name,
pos = Point(stop_lat, stop_lon),
parent = parent_station,
hasWheelChairBoarding = wheelchair_boarding == "1",
level = level_id,
platformCode = platform_code,
)
} }
private suspend fun addStopTimes(files: List<File>, trips: Map<String, Trip>) {
val dao = db.stopTimeDao
dao.deleteAll()
log.info("parsing stop times...")
files
.filter { it.name == "stop_times.txt" }
.forEach { fd ->
log.info("parsing stop times for ${fd.parent}...")
parseStopTimes(fd, trips) { seq ->
seq.chunked(1000000)
.forEach { queue ->
log.info("converting stop times (${queue.size}) for ${fd.parent}...")
val conv = queue.map { it.asEntity() }.toTypedArray()
log.info("inserting stop times (${conv.size}) for ${fd.parent}...")
dao.insertOrReplaceAll(*conv)
}
}
}
}
private inline fun parseStopTimes(fd: File, trips: Map<String, Trip>, block: (Sequence<StopTime>) -> Unit) =
fd.parseCsvSequence<GtfsStopTime> { seq ->
seq
.map { with(it) {
StopTime(
tripId = trip_id,
stopId = stop_id,
arrivalTime = GtfsStopTime.parseGtfsTime(arrival_time),
departureTime = GtfsStopTime.parseGtfsTime(departure_time),
headsign = stop_headsign.ifEmpty { trips[trip_id]!!.tripHeadsign },
pickupType = pickup_type,
dropOffType = drop_off_type,
)
} }
.let { block(it) }
}
private suspend fun addServices(files: List<File>): List<Service> {
val dao = db.serviceDao
log.info("parsing services...")
val services = files
.filter { it.name == "calendar.txt" }
.flatMap { fd -> parseServices(fd) }
log.info("inserting services...")
dao.deleteAll()
dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray())
return services
}
private fun parseServices(fd: File) =
fd.parseCsv<GtfsService>()
.map { with(it) {
val days = buildList {
if (monday == 1) add(DayOfWeek.MONDAY)
if (tuesday == 1) add(DayOfWeek.TUESDAY)
if (wednesday == 1) add(DayOfWeek.WEDNESDAY)
if (thursday == 1) add(DayOfWeek.THURSDAY)
if (friday == 1) add(DayOfWeek.FRIDAY)
if (saturday == 1) add(DayOfWeek.SATURDAY)
if (sunday == 1) add(DayOfWeek.SUNDAY)
}
Service(
id = service_id,
days = days,
start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC),
end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC),
)
} }
private suspend fun addTrips(files: List<File>, services: Map<String, Service>): List<Trip> {
val dao = db.tripDao
log.info("parsing trips...")
val trips = files
.filter { it.name == "trips.txt" }
.flatMap { fd -> parseTrips(fd, services) }
log.info("inserting trips...")
dao.deleteAll()
dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray())
return trips
}
private fun parseTrips(fd: File, services: Map<String, Service>) =
fd.parseCsv<GtfsTrip>()
.map { with(it) {
Trip(
id = trip_id,
routeId = route_id,
service = services[service_id]!!,
shapeId = shape_id.ifEmpty { null },
tripHeadsign = trip_headsign,
directionId = direction_id,
blockId = block_id,
wheelchairAccessible = wheelchair_accessible,
)
} }
private fun extract(fd: File): List<File> {
val outputs = mutableListOf<File>()
ZipFile(fd).use { zip ->
for (entry in zip.entries()) {
zip.getInputStream(entry).use { input ->
val out = File(fd.parentFile, entry.name)
out.parentFile.mkdirs()
out.outputStream().use { output ->
input.copyTo(output)
}
outputs.add(out)
}
}
}
return outputs
}
private fun extractAll(fd: File) = extract(fd).flatMap(::extract)
private inline fun <reified T> File.parseCsv(): List<T> = this
.readText()
.replace("\uFEFF", "") // remove bom
.replace("\r\n", "\n") // crlf -> lf
.let { csv.decodeFromString(it) }
private inline fun <reified T> File.parseCsvSequence(block: (Sequence<T>) -> Unit) = this
.bufferedReader()
.use { reader ->
val iter = object : CharIterator() {
var next: Char? = null
override fun nextChar(): Char {
if (!hasNext()) {
throw NoSuchElementException()
}
val ret = next!!
next = null
return ret
}
override fun hasNext(): Boolean {
if (next == null) {
do {
next = null
val new = reader.read()
if (new != -1) {
next = new.toChar()
}
} while (next == '\uFEFF' || next == '\r')
}
return next != null
}
}
block(csv.decodeToSequence(iter, csv.serializersModule.serializer()))
}
}

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
internal data class GtfsRoute(
data class GtfsRoute(
val route_id: String,
val agency_id: String,
val route_short_name: String,

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
internal data class GtfsService(
data class GtfsService(
val service_id: String,
val monday: Int,
val tuesday: Int,

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
internal data class GtfsShape(
data class GtfsShape(
val shape_id: String,
val shape_pt_lat: Double,
val shape_pt_lon: Double,

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
internal data class GtfsStop(
data class GtfsStop(
val stop_id: String,
val stop_name: String,
val stop_lat: Double,

View file

@ -1,16 +1,16 @@
package moe.lava.banksia.server.gtfs.structures
import kotlinx.serialization.Serializable
import moe.lava.banksia.core.model.FutureTime
import moe.lava.banksia.model.FutureTime
@Suppress("PropertyName")
@Serializable
internal data class GtfsStopTime(
data class GtfsStopTime(
val trip_id: String,
val arrival_time: String,
val departure_time: String,
val stop_id: String,
val stop_sequence: Long,
val stop_sequence: Int,
val stop_headsign: String,
val pickup_type: Int,
val drop_off_type: Int,

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
internal data class GtfsTrip(
data class GtfsTrip(
val route_id: String,
val service_id: String,
val trip_id: String,

View file

@ -0,0 +1,164 @@
package moe.lava.banksia.server.gtfsr
import com.google.transit.realtime.FeedMessage
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.readRawBytes
import io.ktor.http.isSuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import moe.lava.banksia.Constants
import moe.lava.banksia.util.LogScope
import moe.lava.banksia.util.log
import java.io.File
import java.time.Instant
import java.time.ZoneId
private const val BASE_DIR = "./data/gtfsr-archive/"
class GtfsrService(private val client: HttpClient) {
private var started = false
private val latest = mutableMapOf<String, FeedMessage>()
fun latestFor(type: String) = latest[type]
private val iFlow = MutableSharedFlow<Pair<String, FeedMessage>>()
val flow = iFlow.asSharedFlow()
companion object {
val types = arrayOf(
"metro/trip-updates",
"metro/vehicle-positions",
"metro/service-alerts",
"tram/trip-updates",
"tram/vehicle-positions",
"tram/service-alerts",
"bus/trip-updates",
"bus/vehicle-positions",
"vline/trip-updates",
"vline/vehicle-positions",
)
}
suspend fun start() {
if (started) {
log("GtfsrService", "Tried to start when already started")
return
}
started = true
coroutineScope {
launch { compressJob() }
while (true) {
val results = mutableMapOf<String, ByteArray>()
types.map { type ->
launch(context = Dispatchers.IO) {
val logger = LogScope("gtfsr $type")
try {
val res = client.get {
url("https://api.opendata.transport.vic.gov.au/opendata/public-transport/gtfs/realtime/v1/${type}")
header("KeyId", Constants.opendataKey)
}
if (!res.status.isSuccess()) {
logger.log("${res.status} | ${res.bodyAsText()}")
} else {
results[type] = res.readRawBytes()
}
} catch (e: Throwable) {
logger.log("$e")
logger.log(e.stackTraceToString())
}
}
}.joinAll()
results.forEach { (type, data) ->
val dec = try {
FeedMessage.ADAPTER.decode(data)
} catch (e: Throwable) {
log("gtfsr $type", "Failed to parse proto: $e")
return@forEach
}
val timestamp = dec.header_.timestamp
?: return@forEach log("gtfsr $type", "Failed to read proto timestamp")
val time = Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
val base = File(BASE_DIR, type)
val previousParent = File(base, "${time.year}-${((time.dayOfYear - 1) / 7).toString().padStart(2, '0')}")
val currentParent = File(base, "${time.year}-${((time.dayOfYear - 1) / 7 + 1).toString().padStart(2, '0')}")
val target = File(currentParent, "${timestamp}.proto")
if (previousParent.isDirectory) {
enqueueCompression(previousParent)
}
if (!target.exists()) {
try {
if (!target.parentFile.isDirectory) {
target.parentFile.mkdirs()
}
target.writeBytes(data)
} catch (e: Throwable) {
log("gtfsr $type", "Failed to write ${target}: $e")
}
}
}
delay(10000)
}
}
}
private val cqueue = mutableSetOf<File>()
private val ignore = mutableSetOf<File>()
private val cmut = Mutex()
private suspend fun enqueueCompression(fd: File) {
cmut.withLock { cqueue.add(fd) }
}
private suspend fun compressJob() {
while(true) {
while(true) {
val next = cmut.withLock { cqueue.firstOrNull() }
?: break
if (!next.isDirectory) {
cmut.withLock { cqueue.remove(next) }
continue
}
if (next in ignore) continue
withContext(Dispatchers.IO) {
val proc = ProcessBuilder(
"tar", "-acf",
"${next.absolutePath}.tar.zst",
next.absolutePath
).start()
val exitCode = proc.waitFor()
if (exitCode == 0) {
if (next.deleteRecursively()) {
cmut.withLock { cqueue.remove(next) }
} else {
log("CompressJob", "Failed to delete $next")
ignore.add(next)
}
} else {
val msg = proc.errorStream.readAllBytes().decodeToString()
log("CompressJob", "Failed to delete $next (exit code $exitCode")
log("CompressJob", msg)
}
}
}
delay(30000)
}
}
}

View file

@ -14,7 +14,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<root level="trace">
<appender-ref ref="FILE"/>
<appender-ref ref="STDOUT"/>
</root>

View file

@ -32,13 +32,9 @@ dependencyResolutionManagement {
}
include(":androidApp")
include(":client")
include(":server")
include(":server:gtfs")
include(":server:gtfs_rt")
include(":core")
include(":core:data")
include(":core:stoptime")
include(":core:sqld")
include(":shared")
include(":ui")
include(":ui:maps")
include(":ui:shared")

View file

@ -4,11 +4,18 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
alias(libs.plugins.room)
alias(libs.plugins.wire)
}
room {
schemaDirectory("$projectDir/schemas")
}
kotlin {
android {
namespace = "moe.lava.banksia.core"
namespace = "moe.lava.banksia.shared"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
@ -40,9 +47,25 @@ kotlin {
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
dependencies {
add("kspAndroid", libs.room.compiler)
add("kspIosArm64", libs.room.compiler)
add("kspIosSimulatorArm64", libs.room.compiler)
add("kspJvm", libs.room.compiler)
}
wire {
sourcePath {
srcDir("src/commonMain/proto")
}
kotlin {}
}

View file

@ -0,0 +1,72 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e536f5a9b1408377bcc449195169648c",
"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": "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"
]
}
}
],
"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, 'e536f5a9b1408377bcc449195169648c')"
]
}
}

View file

@ -0,0 +1,315 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "83ece554400bb035c267dc2414c23293",
"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": "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"
]
},
"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(`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_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": "Shape",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"shapeId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, '83ece554400bb035c267dc2414c23293')"
]
}
}

View file

@ -0,0 +1,339 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "5a7252ab3bcae4d0d0950024b19ba002",
"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": "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"
]
},
"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(`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_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": "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, '5a7252ab3bcae4d0d0950024b19ba002')"
]
}
}

View file

@ -0,0 +1,368 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "4426fd2ccc826d9d9d9021546b105850",
"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": "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": true,
"columnNames": [
"tripId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)"
},
{
"name": "index_StopTime_stopId",
"unique": true,
"columnNames": [
"stopId"
],
"orders": [],
"createSql": "CREATE UNIQUE 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(`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": "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, '4426fd2ccc826d9d9d9021546b105850')"
]
}
}

View file

@ -0,0 +1,368 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "4426fd2ccc826d9d9d9021546b105850",
"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": "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": true,
"columnNames": [
"tripId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)"
},
{
"name": "index_StopTime_stopId",
"unique": true,
"columnNames": [
"stopId"
],
"orders": [],
"createSql": "CREATE UNIQUE 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(`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": "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, '4426fd2ccc826d9d9d9021546b105850')"
]
}
}

View file

@ -0,0 +1,368 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "5f52de4cc0ddbcf02a0d8be4cf4d4cfd",
"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": "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(`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": "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, '5f52de4cc0ddbcf02a0d8be4cf4d4cfd')"
]
}
}

View file

@ -0,0 +1,415 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "15c94df0a62438ff28d451c074c94c59",
"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": "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(`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": "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, '15c94df0a62438ff28d451c074c94c59')"
]
}
}

View file

@ -0,0 +1,426 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "6e0f07bf1af88b2e37b5ad7c38a3fb2a",
"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": "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, '6e0f07bf1af88b2e37b5ad7c38a3fb2a')"
]
}
}

View file

@ -0,0 +1,426 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "6e0f07bf1af88b2e37b5ad7c38a3fb2a",
"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": "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, '6e0f07bf1af88b2e37b5ad7c38a3fb2a')"
]
}
}

View file

@ -0,0 +1,25 @@
package moe.lava.banksia.di
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import moe.lava.banksia.room.Database
import org.koin.core.parameter.ParametersHolder
import org.koin.core.scope.Scope
import org.koin.dsl.module
class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder {
override fun getBuilder(): RoomDatabase.Builder<Database> {
val appContext = ctx.applicationContext
val dbFile = appContext.getDatabasePath("room.db")
return Room.databaseBuilder<Database>(
context = appContext,
name = dbFile.absolutePath
)
}
}
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
AndroidDatabaseBuilder(get())
internal actual val ExtPlatformModule = module { }

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.core.util
package moe.lava.banksia.util
import android.util.Log

View file

@ -6,7 +6,7 @@ object Constants {
const val opendataKey: String = ""
const val serverUrl: String = "https://banksia.lava.moe/api/"
// TODO
var devMode: Boolean = false
const val devMode: Boolean = false
const val updateKey: String = ""
const val protomapsKey: String = ""
}

View file

@ -1,4 +1,4 @@
package moe.lava.banksia.server.gtfsrt
package moe.lava.banksia.data.gtfsr
import com.google.transit.realtime.FeedMessage

View file

@ -1,7 +1,7 @@
package moe.lava.banksia.server.gtfsrt
package moe.lava.banksia.data.gtfsr
import com.google.transit.realtime.FeedMessage
import moe.lava.banksia.core.util.Point
import moe.lava.banksia.util.Point
class RealtimeVehiclePositions(data: FeedMessage) : GtfsRealtime(data) {
private val positions = mutableMapOf<String, Point>()

Some files were not shown because too many files have changed in this diff Show more