wip departures + refactor

This commit is contained in:
Cilly Leang 2026-06-22 00:14:19 +10:00
parent b31067992d
commit 41f3523a5a
Signed by: cilly
GPG key ID: 6500251E087653C9
43 changed files with 596 additions and 204 deletions

View file

@ -25,9 +25,40 @@ kotlin {
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.data.stoptime)
api(projects.core.stoptime)
}
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.sqld)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}

View file

@ -1,54 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.data.client"
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 {
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
api(projects.core.data)
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.sqld)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}

View file

@ -1,20 +0,0 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
api(projects.core.data)
implementation(projects.core)
}

View file

@ -16,17 +16,13 @@ import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
import moe.lava.banksia.core.sqld.sqldDiModule
import moe.lava.banksia.core.util.log
import moe.lava.banksia.data.ptv.PtvService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val clientDataDiModule = module {
includes(sqldDiModule)
includes(stopTimeDataDiModule)
actual val platformModule = module {
// HTTP Clients
singleOf(::PtvService)
single {

View file

@ -4,6 +4,7 @@ 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(
@ -22,5 +23,14 @@ internal class ClientRouteRepository internal constructor(
}
}
private val tripRouteMap = mutableMapOf<Long, Route>()
override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
override suspend fun getByPattern(patternId: Long) = mutex.withLock {
tripRouteMap[patternId]
?: remote.getByPattern(patternId).also {
local.save(it)
tripRouteMap[patternId] = it
}
}
}

View file

@ -10,6 +10,7 @@ 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 {

View file

@ -7,5 +7,6 @@ import moe.lava.banksia.core.model.Route
internal class RouteRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("routes/${id}").body<Route>()
suspend fun getByPattern(patternId: Long) = client.get("routes/by_pattern/${patternId}").body<Route>()
suspend fun getAll() = client.get("routes").body<List<Route>>()
}

View file

@ -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)
}

View file

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

View file

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

View file

@ -2,9 +2,10 @@ 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 DbStoppingPattern.asModel(stoptimes: List<StopTime.Undated>) = StoppingPattern.Undated(
fun <T: TimeType> DbStoppingPattern.asModel(stoptimes: List<StopTime<T>>) = StoppingPattern(
id = id,
routeId = routeId,
shapeId = shapeId,
@ -13,7 +14,7 @@ fun DbStoppingPattern.asModel(stoptimes: List<StopTime.Undated>) = StoppingPatte
stoptimes = stoptimes,
)
fun StoppingPattern.Undated.asDb() = DbStoppingPattern(
fun StoppingPattern<*>.asDb() = DbStoppingPattern(
id = id,
routeId = routeId,
shapeId = shapeId,

View file

@ -11,5 +11,10 @@ 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 INTO Route VALUES ?;
INSERT OR REPLACE INTO Route VALUES ?;

View file

@ -22,3 +22,24 @@ INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;
getExtendedForStop:
SELECT DISTINCT
StopTime.patternId,
StopTime.arrivalDelta,
StopTime.departureTime,
StoppingPattern.headsign,
Route.type AS routeType,
Route.number AS routeNumber,
Route.name AS routeName,
Stop.platformCode AS stopPlatformCode
FROM StopTime
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
INNER JOIN Trip ON Trip.serviceId == Service.id
INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
INNER JOIN Route ON Route.id == StoppingPattern.routeId
INNER JOIN Stop ON Stop.id == StopTime.stopId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;

View file

@ -8,3 +8,6 @@ CREATE TABLE StoppingPattern (
insert:
INSERT OR REPLACE INTO StoppingPattern VALUES ?;
get:
SELECT * FROM StoppingPattern WHERE id == :id;

View file

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

View file

@ -31,13 +31,15 @@ sealed class TimeType {
) : 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 = TimeType.Dated(
arrival = time.arrival.atDate(date),
departure = time.departure.atDate(date),
),
time = time.atDate(date),
pickupType = pickupType,
dropOffType = dropOffType,
)

View file

@ -9,7 +9,7 @@ plugins {
kotlin {
android {
namespace = "moe.lava.banksia.core.data.stoptime"
namespace = "moe.lava.banksia.core.stoptime"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
@ -56,5 +56,9 @@ kotlin {
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
jvmMain.dependencies {
implementation(libs.koin.ktor)
implementation(libs.ktor.server.core)
}
}
}

View file

@ -7,7 +7,9 @@ import io.ktor.client.request.parameter
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.core.model.StopTime
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(
@ -16,9 +18,9 @@ internal class StopTimeRemoteDataSource(
suspend fun getAtStop(
stopId: String,
date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()),
): List<StopTime.Dated> {
return client.get("stoptimes/by_stop/${stopId}") {
): List<ExtendedStopTime> {
return client.get(Endpoint.stopTimeByStop(stopId)) {
parameter("date", date)
}.body<List<StopTime.Dated>>()
}.body<List<ExtendedStopTime>>()
}
}

View file

@ -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,
)

View file

@ -4,12 +4,12 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import moe.lava.banksia.core.model.StopTime
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<StopTime.Dated>>
): Flow<List<ExtendedStopTime>>
}

View file

@ -4,10 +4,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.atDate
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.sqld.mappers.asModel
import moe.lava.banksia.core.util.serialise
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@ -15,16 +14,16 @@ import org.koin.core.component.get
internal class StopTimeLocalDataSource : KoinComponent {
private val queries get() = get<StopTimeQueries>()
suspend fun getAtStop(stopId: String, date: LocalDate): List<StopTime.Dated> {
suspend fun getAtStop(stopId: String, date: LocalDate): List<ExtendedStopTime> {
return withContext(context = Dispatchers.IO) {
queries
.getForStopDated(
.getExtendedForStop(
listOf(date.dayOfWeek).serialise().toLong(),
date.toEpochDays(),
stopId,
)
.executeAsList()
.map { it.asModel().atDate(date) }
.map { it.asModel(date) }
.sortedBy { it.time.departure }
}
}

View file

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

View file

@ -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)
}
}