Compare commits
24 commits
refactor/h
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b3016004b | |||
| b31067992d | |||
| 102c028407 | |||
| f1770744db | |||
| ff2af310fb | |||
| ef630b6d58 | |||
| 415ce8d88f | |||
| 29a804b0fb | |||
| 27f2a08d77 | |||
| 38bcdc54bc | |||
| 0524eda5d2 | |||
| 959b022cf9 | |||
| e7caeca356 | |||
| 4cdb9a305c | |||
| c912723c78 | |||
| 104a77b27e | |||
| c55e3a3232 | |||
| 58649b6171 | |||
| c9aeeb99c1 | |||
| 91d4fe25a6 | |||
| b568ed547a | |||
| ed9d294afc | |||
| 0181497420 | |||
| aad5ae4024 |
177 changed files with 2637 additions and 4898 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -18,6 +18,6 @@ captures
|
||||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||||
|
|
||||||
secrets.properties
|
secrets.properties
|
||||||
shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt
|
/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt
|
||||||
/data/
|
/data/
|
||||||
/data
|
/data
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ plugins {
|
||||||
alias(libs.plugins.composeCompiler) apply false
|
alias(libs.plugins.composeCompiler) apply false
|
||||||
alias(libs.plugins.kotlinJvm) apply false
|
alias(libs.plugins.kotlinJvm) apply false
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
|
alias(libs.plugins.sqldelight) apply false
|
||||||
alias(libs.plugins.wire) apply false
|
alias(libs.plugins.wire) apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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>>()
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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>>()
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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) }
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,18 +4,11 @@ plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.androidMultiplatformLibrary)
|
alias(libs.plugins.androidMultiplatformLibrary)
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
alias(libs.plugins.room)
|
|
||||||
alias(libs.plugins.wire)
|
|
||||||
}
|
|
||||||
|
|
||||||
room {
|
|
||||||
schemaDirectory("$projectDir/schemas")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
android {
|
android {
|
||||||
namespace = "moe.lava.banksia.shared"
|
namespace = "moe.lava.banksia.core"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
|
|
@ -47,25 +40,9 @@ kotlin {
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.serialization.protobuf)
|
implementation(libs.kotlinx.serialization.protobuf)
|
||||||
implementation(libs.room.runtime)
|
|
||||||
implementation(libs.sqlite.bundled)
|
|
||||||
}
|
}
|
||||||
iosMain.dependencies {
|
iosMain.dependencies {
|
||||||
implementation(libs.ktor.client.darwin)
|
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 {}
|
|
||||||
}
|
|
||||||
64
core/data/build.gradle.kts
Normal file
64
core/data/build.gradle.kts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.client.di
|
package moe.lava.banksia.core.data
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.plugins.HttpSend
|
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.client.plugins.plugin
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.lava.banksia.Constants
|
import moe.lava.banksia.core.Constants
|
||||||
import moe.lava.banksia.client.data.route.RouteLocalDataSource
|
import moe.lava.banksia.core.data.repositories.ClientRouteRepository
|
||||||
import moe.lava.banksia.client.data.route.RouteRemoteDataSource
|
import moe.lava.banksia.core.data.repositories.ClientStopRepository
|
||||||
import moe.lava.banksia.client.data.stop.StopLocalDataSource
|
import moe.lava.banksia.core.data.repositories.RouteRepository
|
||||||
import moe.lava.banksia.client.data.stop.StopRemoteDataSource
|
import moe.lava.banksia.core.data.repositories.StopRepository
|
||||||
import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource
|
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
|
||||||
import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource
|
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
|
||||||
import moe.lava.banksia.client.repository.RouteRepository
|
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
|
||||||
import moe.lava.banksia.client.repository.StopRepository
|
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
|
||||||
import moe.lava.banksia.client.repository.StopTimeRepository
|
import moe.lava.banksia.core.util.log
|
||||||
import moe.lava.banksia.data.ptv.PtvService
|
import moe.lava.banksia.data.ptv.PtvService
|
||||||
import moe.lava.banksia.util.log
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val ClientModule = module {
|
actual val platformModule = module {
|
||||||
// HTTP Clients
|
// HTTP Clients
|
||||||
singleOf(::PtvService)
|
singleOf(::PtvService)
|
||||||
single {
|
single {
|
||||||
|
|
@ -49,11 +49,8 @@ val ClientModule = module {
|
||||||
singleOf(::RouteRemoteDataSource)
|
singleOf(::RouteRemoteDataSource)
|
||||||
singleOf(::StopLocalDataSource)
|
singleOf(::StopLocalDataSource)
|
||||||
singleOf(::StopRemoteDataSource)
|
singleOf(::StopRemoteDataSource)
|
||||||
singleOf(::StopTimeLocalDataSource)
|
|
||||||
singleOf(::StopTimeRemoteDataSource)
|
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
singleOf(::RouteRepository)
|
singleOf(::ClientRouteRepository) bind RouteRepository::class
|
||||||
singleOf(::StopRepository)
|
singleOf(::ClientStopRepository) bind StopRepository::class
|
||||||
singleOf(::StopTimeRepository)
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
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>>()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package moe.lava.banksia.client.data.stop
|
package moe.lava.banksia.core.data.sources.stop
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import moe.lava.banksia.model.Stop
|
import moe.lava.banksia.core.model.Stop
|
||||||
|
|
||||||
class StopRemoteDataSource(val client: HttpClient) {
|
internal class StopRemoteDataSource(val client: HttpClient) {
|
||||||
suspend fun get(id: String) = client.get("stops/${id}").body<Stop>()
|
suspend fun get(id: String) = client.get("stops/${id}").body<Stop>()
|
||||||
suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body<List<Stop>>()
|
suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body<List<Stop>>()
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package moe.lava.banksia.core.data
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
internal actual val platformModule = module {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,12 @@ plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.androidMultiplatformLibrary)
|
alias(libs.plugins.androidMultiplatformLibrary)
|
||||||
|
alias(libs.plugins.sqldelight)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
android {
|
android {
|
||||||
namespace = "moe.lava.banksia.client"
|
namespace = "moe.lava.banksia.core.sqld"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
|
|
@ -16,28 +17,37 @@ kotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compilerOptions {
|
|
||||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
|
||||||
}
|
|
||||||
|
|
||||||
iosArm64()
|
iosArm64()
|
||||||
iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
|
|
||||||
|
jvm()
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.ui.tooling.preview)
|
implementation(libs.sqldelight.driver.android)
|
||||||
implementation(libs.androidx.activity.compose)
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
|
||||||
implementation(libs.play.services.location)
|
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
implementation(libs.okio)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.ktor.client.core)
|
|
||||||
implementation(libs.ktor.client.contentnegotiation)
|
implementation(projects.core)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
}
|
||||||
implementation(projects.shared)
|
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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package moe.lava.banksia.core.sqld
|
||||||
|
|
||||||
|
internal const val DBNAME = "timetable"
|
||||||
|
|
||||||
|
expect class DatabaseManager() {
|
||||||
|
val database: BanksiaDatabase
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
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)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
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 ?;
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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 ?;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE ServiceException (
|
||||||
|
serviceId TEXT NOT NULL,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
date INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (serviceId, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert:
|
||||||
|
INSERT INTO ServiceException VALUES ?;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE Shape (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
path BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
insert:
|
||||||
|
INSERT INTO Shape VALUES ?;
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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 ?;
|
||||||
BIN
core/sqld/src/commonMain/sqldelight/schema/1.db
Normal file
BIN
core/sqld/src/commonMain/sqldelight/schema/1.db
Normal file
Binary file not shown.
|
|
@ -0,0 +1,11 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@ object Constants {
|
||||||
const val opendataKey: String = ""
|
const val opendataKey: String = ""
|
||||||
const val serverUrl: String = "https://banksia.lava.moe/api/"
|
const val serverUrl: String = "https://banksia.lava.moe/api/"
|
||||||
// TODO
|
// TODO
|
||||||
const val devMode: Boolean = false
|
var devMode: Boolean = false
|
||||||
const val updateKey: String = ""
|
const val updateKey: String = ""
|
||||||
const val protomapsKey: String = ""
|
const val protomapsKey: String = ""
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package moe.lava.banksia.core.endpoints
|
||||||
|
|
||||||
|
object Endpoint
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.datetime.DateTimeUnit
|
import kotlinx.datetime.DateTimeUnit
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
@ -12,7 +12,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import moe.lava.banksia.model.FutureTime.Companion.asInt
|
import moe.lava.banksia.core.model.FutureTime.Companion.asInt
|
||||||
|
|
||||||
@Serializable(FutureTimeSerialiser::class)
|
@Serializable(FutureTimeSerialiser::class)
|
||||||
data class FutureTime(
|
data class FutureTime(
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -13,4 +13,8 @@ enum class RouteType(val value: Int) {
|
||||||
SkyBus(11),
|
SkyBus(11),
|
||||||
Interstate(10),
|
Interstate(10),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(value: Int) = entries.first { it.value == value }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
data class Run(
|
data class Run(
|
||||||
val ref: String,
|
val ref: String,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.core.util.Point
|
||||||
|
|
||||||
typealias ShapePath = List<Point>
|
typealias ShapePath = List<Point>
|
||||||
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import moe.lava.banksia.util.Point
|
import moe.lava.banksia.core.util.Point
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Stop(
|
data class Stop(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val pos: Point,
|
val pos: Point,
|
||||||
val parent: String,
|
val parent: String?,
|
||||||
val hasWheelChairBoarding: Boolean,
|
val hasWheelChairBoarding: Boolean,
|
||||||
val level: String,
|
val level: String?,
|
||||||
val platformCode: String,
|
val platformCode: String?,
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.model
|
package moe.lava.banksia.core.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
/** Wraps an arbitrary value, such that equality checks are forced to be done by reference */
|
/** Wraps an arbitrary value, such that equality checks are forced to be done by reference */
|
||||||
class BoxedValue<T>(val value: T) {
|
class BoxedValue<T>(val value: T) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
fun error(tag: String, throwable: Throwable) = error(tag, "", throwable)
|
fun error(tag: String, throwable: Throwable) = error(tag, "", throwable)
|
||||||
expect fun log(tag: String, msg: String)
|
expect fun log(tag: String, msg: String)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -16,7 +16,12 @@ import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.lava.banksia.Constants
|
import moe.lava.banksia.core.Constants
|
||||||
|
import moe.lava.banksia.core.model.RouteType
|
||||||
|
import moe.lava.banksia.core.util.LoopFlow.Companion.initWith
|
||||||
|
import moe.lava.banksia.core.util.error
|
||||||
|
import moe.lava.banksia.core.util.log
|
||||||
|
import moe.lava.banksia.core.util.loopFlow
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvDeparture
|
import moe.lava.banksia.data.ptv.structures.PtvDeparture
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvDirection
|
import moe.lava.banksia.data.ptv.structures.PtvDirection
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||||
|
|
@ -24,11 +29,6 @@ import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
|
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvRun
|
import moe.lava.banksia.data.ptv.structures.PtvRun
|
||||||
import moe.lava.banksia.data.ptv.structures.PtvStop
|
import moe.lava.banksia.data.ptv.structures.PtvStop
|
||||||
import moe.lava.banksia.model.RouteType
|
|
||||||
import moe.lava.banksia.util.LoopFlow.Companion.initWith
|
|
||||||
import moe.lava.banksia.util.error
|
|
||||||
import moe.lava.banksia.util.log
|
|
||||||
import moe.lava.banksia.util.loopFlow
|
|
||||||
import okio.ByteString.Companion.encodeUtf8
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@ package moe.lava.banksia.data.ptv.structures
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import moe.lava.banksia.model.RouteType
|
import moe.lava.banksia.core.model.RouteType
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PtvRoute(
|
data class PtvRoute(
|
||||||
|
|
@ -7,7 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import moe.lava.banksia.model.RouteType
|
import moe.lava.banksia.core.model.RouteType
|
||||||
|
|
||||||
object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
|
object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
actual fun log(tag: String, msg: String) {
|
actual fun log(tag: String, msg: String) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package moe.lava.banksia.util
|
package moe.lava.banksia.core.util
|
||||||
|
|
||||||
actual fun log(tag: String, msg: String) {
|
actual fun log(tag: String, msg: String) {
|
||||||
println("[$tag] $msg")
|
println("[$tag] $msg")
|
||||||
64
core/stoptime/build.gradle.kts
Normal file
64
core/stoptime/build.gradle.kts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package moe.lava.banksia.core.endpoints
|
||||||
|
|
||||||
|
fun Endpoint.stopTimeByStop(stopId: String) = "stoptimes/by_stop/${stopId}"
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,36 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp = "9.1.0"
|
agp = "9.1.0"
|
||||||
android-compileSdk = "36"
|
android-compileSdk = "37"
|
||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-targetSdk = "36"
|
android-targetSdk = "37"
|
||||||
androidx-activityCompose = "1.12.4"
|
androidx-activity= "1.13.0"
|
||||||
androidx-appcompat = "1.7.0"
|
androidx-lifecycle = "2.10.0"
|
||||||
androidx-constraintlayout = "2.2.1"
|
compose-multiplatform = "1.12.0-alpha02"
|
||||||
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"
|
composeunstyled = "1.49.6"
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
geo = "0.8.0"
|
geo = "0.8.0"
|
||||||
junit = "4.13.2"
|
koin = "4.2.0"
|
||||||
koin = "4.1.1"
|
kotlin = "2.3.20"
|
||||||
kotlin = "2.3.10"
|
|
||||||
kotlinxDatetime = "0.7.1"
|
kotlinxDatetime = "0.7.1"
|
||||||
kotlinxSerializationCsv = "0.2.18"
|
kotlinxSerializationCsv = "0.2.18"
|
||||||
kotlinxSerialization = "1.10.0"
|
kotlinxSerialization = "1.10.0"
|
||||||
ksp = "2.3.4"
|
ksp = "2.3.4"
|
||||||
ktor = "3.4.0"
|
ktor = "3.4.1"
|
||||||
logback = "1.5.32"
|
logback = "1.5.32"
|
||||||
maplibre = "0.12.1"
|
maplibre = "0.12.1"
|
||||||
material = "1.7.3"
|
material = "1.7.3"
|
||||||
material3 = "1.11.0-alpha02"
|
material3 = "1.11.0-alpha07"
|
||||||
okio = "3.16.4"
|
okio = "3.17.0"
|
||||||
playServicesLocation = "21.3.0"
|
playServicesLocation = "21.3.0"
|
||||||
sqlite = "2.6.2"
|
|
||||||
room = "2.8.4"
|
|
||||||
secretsGradlePlugin = "2.0.1"
|
secretsGradlePlugin = "2.0.1"
|
||||||
wire = "5.5.0"
|
sqldelight = "2.3.2"
|
||||||
|
wire = "6.1.0"
|
||||||
|
|
||||||
[libraries]
|
[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-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-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" }
|
compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material" }
|
||||||
|
|
@ -44,18 +40,11 @@ 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 = { 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" }
|
compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" }
|
||||||
composeunstyled = { module = "com.composables:composeunstyled", version.ref = "composeunstyled" }
|
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 = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
|
||||||
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", 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-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
koin-ktor = { module = "io.insert-koin:koin-ktor", 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-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-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||||
|
|
@ -73,14 +62,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" }
|
ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre" }
|
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" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||||
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
|
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" }
|
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" }
|
ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
@ -93,6 +82,6 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref =
|
||||||
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
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" }
|
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" }
|
wire = { id = "com.squareup.wire", version.ref = "wire" }
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,27 @@ plugins {
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "moe.lava.banksia"
|
group = "moe.lava.banksia.server"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
application {
|
application {
|
||||||
mainClass.set("moe.lava.banksia.server.ApplicationKt")
|
mainClass.set("moe.lava.banksia.server.ApplicationKt")
|
||||||
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
|
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xexplicit-backing-fields")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.shared)
|
implementation(projects.core)
|
||||||
|
implementation(projects.core.data)
|
||||||
|
implementation(projects.core.sqld)
|
||||||
|
implementation(projects.core.stoptime)
|
||||||
|
implementation(projects.server.gtfs)
|
||||||
|
implementation(projects.server.gtfsRt)
|
||||||
|
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.ktor)
|
implementation(libs.koin.ktor)
|
||||||
|
|
@ -26,8 +38,6 @@ dependencies {
|
||||||
implementation(libs.ktor.server.contentnegotiation)
|
implementation(libs.ktor.server.contentnegotiation)
|
||||||
implementation(libs.ktor.server.core)
|
implementation(libs.ktor.server.core)
|
||||||
implementation(libs.ktor.server.netty)
|
implementation(libs.ktor.server.netty)
|
||||||
implementation(libs.room.runtime)
|
|
||||||
implementation(libs.sqlite.bundled)
|
|
||||||
testImplementation(libs.ktor.server.tests)
|
testImplementation(libs.ktor.server.tests)
|
||||||
testImplementation(libs.kotlin.test.junit)
|
testImplementation(libs.kotlin.test.junit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
server/gtfs/build.gradle.kts
Normal file
20
server/gtfs/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GtfsRoute(
|
internal data class GtfsRoute(
|
||||||
val route_id: String,
|
val route_id: String,
|
||||||
val agency_id: String,
|
val agency_id: String,
|
||||||
val route_short_name: String,
|
val route_short_name: String,
|
||||||
|
|
@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GtfsService(
|
internal data class GtfsService(
|
||||||
val service_id: String,
|
val service_id: String,
|
||||||
val monday: Int,
|
val monday: Int,
|
||||||
val tuesday: Int,
|
val tuesday: Int,
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package moe.lava.banksia.server.gtfs.structures
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Suppress("PropertyName")
|
||||||
|
@Serializable
|
||||||
|
internal data class GtfsServiceException(
|
||||||
|
val service_id: String,
|
||||||
|
val date: String,
|
||||||
|
val exception_type: Int,
|
||||||
|
)
|
||||||
|
|
@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GtfsShape(
|
internal data class GtfsShape(
|
||||||
val shape_id: String,
|
val shape_id: String,
|
||||||
val shape_pt_lat: Double,
|
val shape_pt_lat: Double,
|
||||||
val shape_pt_lon: Double,
|
val shape_pt_lon: Double,
|
||||||
|
|
@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GtfsStop(
|
internal data class GtfsStop(
|
||||||
val stop_id: String,
|
val stop_id: String,
|
||||||
val stop_name: String,
|
val stop_name: String,
|
||||||
val stop_lat: Double,
|
val stop_lat: Double,
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package moe.lava.banksia.server.gtfs.structures
|
package moe.lava.banksia.server.gtfs.structures
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import moe.lava.banksia.model.FutureTime
|
import moe.lava.banksia.core.model.FutureTime
|
||||||
|
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GtfsStopTime(
|
internal data class GtfsStopTime(
|
||||||
val trip_id: String,
|
val trip_id: String,
|
||||||
val arrival_time: String,
|
val arrival_time: String,
|
||||||
val departure_time: String,
|
val departure_time: String,
|
||||||
val stop_id: String,
|
val stop_id: String,
|
||||||
val stop_sequence: Int,
|
val stop_sequence: Long,
|
||||||
val stop_headsign: String,
|
val stop_headsign: String,
|
||||||
val pickup_type: Int,
|
val pickup_type: Int,
|
||||||
val drop_off_type: Int,
|
val drop_off_type: Int,
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue