Compare commits
1 commit
feat/depar
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b3016004b |
44 changed files with 627 additions and 211 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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>>()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package moe.lava.banksia.core.data
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
internal actual val platformModule = module {
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ?;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -8,3 +8,6 @@ CREATE TABLE StoppingPattern (
|
|||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO StoppingPattern VALUES ?;
|
||||
|
||||
get:
|
||||
SELECT * FROM StoppingPattern WHERE id == :id;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package moe.lava.banksia.core.endpoints
|
||||
|
||||
object Endpoint
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package moe.lava.banksia.core.endpoints
|
||||
|
||||
fun Endpoint.stopTimeByStop(stopId: String) = "stoptimes/by_stop/${stopId}"
|
||||
|
|
@ -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,11 +1,11 @@
|
|||
[versions]
|
||||
agp = "9.1.0"
|
||||
android-compileSdk = "36"
|
||||
android-compileSdk = "37"
|
||||
android-minSdk = "24"
|
||||
android-targetSdk = "36"
|
||||
android-targetSdk = "37"
|
||||
androidx-activity= "1.13.0"
|
||||
androidx-lifecycle = "2.10.0"
|
||||
compose-multiplatform = "1.11.0-alpha04"
|
||||
compose-multiplatform = "1.12.0-alpha02"
|
||||
composeunstyled = "1.49.6"
|
||||
coroutines = "1.10.2"
|
||||
geo = "0.8.0"
|
||||
|
|
@ -19,7 +19,7 @@ ktor = "3.4.1"
|
|||
logback = "1.5.32"
|
||||
maplibre = "0.12.1"
|
||||
material = "1.7.3"
|
||||
material3 = "1.11.0-alpha04"
|
||||
material3 = "1.11.0-alpha07"
|
||||
okio = "3.17.0"
|
||||
playServicesLocation = "21.3.0"
|
||||
secretsGradlePlugin = "2.0.1"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ kotlin {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.core)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.sqld)
|
||||
implementation(projects.core.stoptime)
|
||||
implementation(projects.server.gtfs)
|
||||
implementation(projects.server.gtfsRt)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,16 @@ import io.ktor.server.routing.routing
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.todayIn
|
||||
import moe.lava.banksia.core.Constants
|
||||
import moe.lava.banksia.core.model.atDate
|
||||
import moe.lava.banksia.core.sqld.RouteQueries
|
||||
import moe.lava.banksia.core.sqld.StopQueries
|
||||
import moe.lava.banksia.core.sqld.StopTimeQueries
|
||||
import moe.lava.banksia.core.sqld.mappers.asModel
|
||||
import moe.lava.banksia.core.util.serialise
|
||||
import moe.lava.banksia.server.di.ServerModules
|
||||
import moe.lava.banksia.server.gtfsrt.GtfsrtService
|
||||
import moe.lava.banksia.server.routes.stopTimeRoutes
|
||||
import org.koin.dsl.module
|
||||
import org.koin.ktor.ext.get
|
||||
import org.koin.ktor.plugin.Koin
|
||||
import kotlin.time.Clock
|
||||
|
||||
fun main() {
|
||||
if (System.getenv("BANKSIA_PRODUCTION") == "1") Constants.devMode = false
|
||||
|
|
@ -53,6 +47,8 @@ fun Application.module() {
|
|||
launch { get<GtfsrtService>().start(this, !Constants.devMode) }
|
||||
|
||||
routing {
|
||||
stopTimeRoutes()
|
||||
|
||||
if (Constants.devMode) {
|
||||
get("/fixup") {
|
||||
call.respondText("received")
|
||||
|
|
@ -137,23 +133,5 @@ fun Application.module() {
|
|||
}
|
||||
call.respond(stops.map { it.asModel() })
|
||||
}
|
||||
get("/stoptimes/by_stop/{stop_id}") {
|
||||
val stopId = call.parameters["stop_id"]!!
|
||||
val date = call.queryParameters["date"]
|
||||
?.let { LocalDate.parse(it, LocalDate.Formats.ISO) }
|
||||
?: Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||
val times = withContext(context = Dispatchers.IO) {
|
||||
get<StopTimeQueries>()
|
||||
.getForStopDated(
|
||||
listOf(date.dayOfWeek).serialise().toLong(),
|
||||
date.toEpochDays(),
|
||||
stopId,
|
||||
)
|
||||
.executeAsList()
|
||||
.map { it.asModel().atDate(date) }
|
||||
.sortedBy { it.time.departure }
|
||||
}
|
||||
call.respond(times)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package moe.lava.banksia.server.di
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import moe.lava.banksia.core.sqld.sqldDiModule
|
||||
import moe.lava.banksia.core.data.dataDiModule
|
||||
import moe.lava.banksia.server.GtfsDataFixer
|
||||
import moe.lava.banksia.server.GtfsImporter
|
||||
import moe.lava.banksia.server.gtfs.GtfsParser
|
||||
|
|
@ -11,7 +11,7 @@ import org.koin.core.module.dsl.singleOf
|
|||
import org.koin.dsl.module
|
||||
|
||||
val ServerModules = module {
|
||||
includes(sqldDiModule)
|
||||
includes(dataDiModule)
|
||||
|
||||
single { HttpClient() }
|
||||
singleOf(::GtfsParser)
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ include(":server:gtfs")
|
|||
include(":server:gtfs_rt")
|
||||
include(":core")
|
||||
include(":core:data")
|
||||
include(":core:data:client")
|
||||
include(":core:data:server")
|
||||
include(":core:data:stoptime")
|
||||
include(":core:stoptime")
|
||||
include(":core:sqld")
|
||||
include(":ui")
|
||||
include(":ui:maps")
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
implementation(libs.play.services.location)
|
||||
implementation(projects.ui.shared)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.compose.components.resources)
|
||||
|
|
@ -68,7 +70,8 @@ kotlin {
|
|||
implementation(libs.ui.backhandler)
|
||||
|
||||
implementation(projects.core)
|
||||
implementation(projects.core.data.client)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.stoptime)
|
||||
implementation(projects.ui.maps)
|
||||
implementation(projects.ui.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ kotlin {
|
|||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
enable = true
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
|
|
@ -47,4 +51,5 @@ dependencies {
|
|||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "moe.lava.banksia.resources"
|
||||
generateResClass = always
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M480,600L280,400L680,400L480,600Z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M280,560L480,360L680,560L280,560Z"/>
|
||||
</vector>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package moe.lava.banksia.ui.di
|
||||
|
||||
import moe.lava.banksia.core.data.clientDataDiModule
|
||||
import moe.lava.banksia.core.data.dataDiModule
|
||||
import moe.lava.banksia.ui.screens.map.MapScreenViewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val AppModule = module {
|
||||
includes(clientDataDiModule)
|
||||
includes(dataDiModule)
|
||||
|
||||
// ViewModel
|
||||
viewModelOf(::MapScreenViewModel)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContent
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -27,7 +28,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtMost
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
|
@ -45,6 +45,7 @@ sealed class InfoPanelState {
|
|||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun InfoPanel(
|
||||
modifier: Modifier = Modifier,
|
||||
state: InfoPanelState,
|
||||
onEvent: (InfoPanelEvent) -> Unit,
|
||||
onPeekHeightChange: (Dp) -> Unit,
|
||||
|
|
@ -65,11 +66,13 @@ fun InfoPanel(
|
|||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.heightIn(min = 350.dp)
|
||||
.onSizeChanged {
|
||||
onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
|
||||
// onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
|
||||
onPeekHeightChange(350.dp)
|
||||
}
|
||||
) {
|
||||
Box {
|
||||
|
|
|
|||
|
|
@ -1,42 +1,297 @@
|
|||
package moe.lava.banksia.ui.layout.info
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedListItem
|
||||
import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.arrow_drop_down
|
||||
import moe.lava.banksia.resources.arrow_drop_up
|
||||
import moe.lava.banksia.ui.extensions.BUS_ORANGE
|
||||
import moe.lava.banksia.ui.extensions.TRAIN_BLUE
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Instant
|
||||
|
||||
sealed class StopInfoPanelEvent : InfoPanelEvent()
|
||||
sealed class StopInfoPanelEvent : InfoPanelEvent() {
|
||||
data object ToggleGrouping : StopInfoPanelEvent()
|
||||
}
|
||||
|
||||
data class StopInfoPanelState(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val subname: String? = null,
|
||||
val departures: List<Departure>? = null,
|
||||
val departures: List<DeparturePlatforms>? = null,
|
||||
) : InfoPanelState() {
|
||||
override val loading: Boolean
|
||||
get() = departures == null
|
||||
get() = departures.isNullOrEmpty()
|
||||
|
||||
data class Departure(val directionName: String, val formattedTimes: String)
|
||||
data class DeparturePlatforms(
|
||||
val platform: String,
|
||||
val departures: List<DepartureInfo>,
|
||||
)
|
||||
|
||||
data class DepartureInfo(
|
||||
val routeName: String,
|
||||
val routeColour: Color?,
|
||||
val headsign: String,
|
||||
val description: String?,
|
||||
val time: Instant,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun listColors() = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
selectedContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
private fun MonoPlatform(
|
||||
state: StopInfoPanelState.DeparturePlatforms
|
||||
) {
|
||||
val departures = state.departures
|
||||
val lazyState = LazyListState(firstVisibleItemIndex =
|
||||
departures.indexOfFirst {
|
||||
it.time > Clock.System.now()
|
||||
}.coerceAtLeast(0)
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||
state = lazyState,
|
||||
) {
|
||||
itemsIndexed(departures) { idx, dep ->
|
||||
SegmentedListItem(
|
||||
onClick = {},
|
||||
colors = listColors(),
|
||||
shapes = ListItemDefaults.segmentedShapes(
|
||||
idx,
|
||||
departures.size,
|
||||
),
|
||||
supportingContent = {
|
||||
dep.description?.let { Text(dep.description) }
|
||||
},
|
||||
trailingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy((-4).dp)
|
||||
) {
|
||||
Text(
|
||||
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
|
||||
style = MaterialTheme.typography.headlineSmallEmphasized,
|
||||
)
|
||||
Text(
|
||||
text = "mn",
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(ShapeDefaults.ExtraSmall)
|
||||
.background(dep.routeColour ?: MaterialTheme.colorScheme.surface)
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = dep.routeName,
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = dep.headsign,
|
||||
style = MaterialTheme.typography.labelLargeEmphasized,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
private fun ManyPlatforms(
|
||||
state: List<StopInfoPanelState.DeparturePlatforms>,
|
||||
) {
|
||||
val expandedList = remember { mutableStateListOf(*Array(state.size) { true }) }
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
state.forEachIndexed { idx, depInfo ->
|
||||
val (platform, departures) = depInfo
|
||||
val expanded = expandedList[idx]
|
||||
stickyHeader(key = "header_${depInfo.hashCode()}") {
|
||||
val base = ListItemDefaults.segmentedShapes(0, 2)
|
||||
val large = MaterialTheme.shapes.large
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.animateItem()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.padding(bottom = ListItemDefaults.SegmentedGap)
|
||||
) {
|
||||
SegmentedListItem(
|
||||
onClick = { expandedList[idx] = !expandedList[idx] },
|
||||
colors = listColors(),
|
||||
shapes = if (expanded) base else base.copy(shape = large),
|
||||
trailingContent = {
|
||||
Icon(
|
||||
painterResource(if (expanded) Res.drawable.arrow_drop_up else Res.drawable.arrow_drop_down),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (expanded) MaterialTheme.colorScheme.surface else Color.Transparent,
|
||||
shape = RoundedCornerShape(100)
|
||||
)
|
||||
.padding(6.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = platform,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
item(key = "items_${depInfo.hashCode()}") {
|
||||
Column(
|
||||
modifier = Modifier.animateItem(),
|
||||
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||
) {
|
||||
departures.filter { it.time > Clock.System.now() }.take(5)
|
||||
.forEachIndexed { idx, dep ->
|
||||
SegmentedListItem(
|
||||
onClick = {},
|
||||
colors = listColors(),
|
||||
shapes = ListItemDefaults.segmentedShapes(
|
||||
idx + 1,
|
||||
(departures.size + 1).coerceAtMost(6),
|
||||
),
|
||||
supportingContent = {
|
||||
dep.description?.let { Text(dep.description) }
|
||||
},
|
||||
trailingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy((-4).dp)
|
||||
) {
|
||||
Text(
|
||||
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
|
||||
style = MaterialTheme.typography.headlineSmallEmphasized,
|
||||
)
|
||||
Text(
|
||||
text = "mn",
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(ShapeDefaults.ExtraSmall)
|
||||
.background(
|
||||
dep.routeColour
|
||||
?: MaterialTheme.colorScheme.surface
|
||||
)
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = dep.routeName,
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = dep.headsign,
|
||||
style = MaterialTheme.typography.labelLargeEmphasized,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item(key = "spacer_${depInfo.hashCode()}") {
|
||||
Spacer(
|
||||
modifier = Modifier.animateItem().height(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
internal fun StopInfoPanel(
|
||||
state: StopInfoPanelState,
|
||||
onEvent: (StopInfoPanelEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300))
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
contentKey = { it.id },
|
||||
transitionSpec = { spec },
|
||||
) { state ->
|
||||
Column(Modifier.fillMaxWidth().fillMaxHeight()) {
|
||||
Row {
|
||||
Column {
|
||||
Text(
|
||||
state.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
|
|
@ -53,23 +308,51 @@ internal fun StopInfoPanel(
|
|||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
state.departures?.let {
|
||||
Spacer(Modifier.height(5.dp))
|
||||
it.forEach { (name, formatted) ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
formatted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 5.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onEvent(StopInfoPanelEvent.ToggleGrouping) },
|
||||
) { Icon(Icons.Default.Edit, null) }
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
AnimatedContent(
|
||||
targetState = state.departures,
|
||||
transitionSpec = { spec },
|
||||
) { departures ->
|
||||
departures?.let { departurePlatforms ->
|
||||
if (departurePlatforms.size > 1) {
|
||||
ManyPlatforms(departurePlatforms)
|
||||
} else if (departurePlatforms.size == 1) {
|
||||
MonoPlatform(departurePlatforms[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun StopInfoPanelPreview() {
|
||||
fun dateIn(dur: Duration) = (Clock.System.now() + dur)
|
||||
|
||||
InfoPanel(
|
||||
modifier = Modifier.background(BanksiaTheme.colors.background),
|
||||
state = StopInfoPanelState(
|
||||
id = "id",
|
||||
name = "name",
|
||||
subname = "sub",
|
||||
departures = listOf(
|
||||
StopInfoPanelState.DeparturePlatforms("Platform 1", listOf(
|
||||
StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "Sunbury", "··· Malvern -> Anzac ··· Sunbury", dateIn(2.minutes)),
|
||||
StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "West Footscray", "Express via Metro Tunnel", dateIn(8.minutes)),
|
||||
)),
|
||||
StopInfoPanelState.DeparturePlatforms("Platform 2", listOf(
|
||||
StopInfoPanelState.DepartureInfo("237", Color(BUS_ORANGE), "Westall", null, dateIn(7.minutes)),
|
||||
StopInfoPanelState.DepartureInfo("442", Color(BUS_ORANGE), "Dandenong", null, dateIn(8.minutes)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onEvent = {},
|
||||
onPeekHeightChange = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import moe.lava.banksia.core.data.dto.ExtendedStopTime
|
||||
import moe.lava.banksia.core.data.repositories.RouteRepository
|
||||
import moe.lava.banksia.core.data.repositories.StopRepository
|
||||
import moe.lava.banksia.core.data.repositories.StopTimeRepository
|
||||
|
|
@ -26,9 +27,11 @@ import moe.lava.banksia.core.util.LoopFlow.Companion.waitUntilSubscribed
|
|||
import moe.lava.banksia.core.util.Point
|
||||
import moe.lava.banksia.core.util.log
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.ui.extensions.getUIProperties
|
||||
import moe.lava.banksia.ui.layout.info.InfoPanelEvent
|
||||
import moe.lava.banksia.ui.layout.info.InfoPanelState
|
||||
import moe.lava.banksia.ui.layout.info.RouteInfoPanelState
|
||||
import moe.lava.banksia.ui.layout.info.StopInfoPanelEvent
|
||||
import moe.lava.banksia.ui.layout.info.StopInfoPanelState
|
||||
import moe.lava.banksia.ui.layout.info.TripInfoPanelState
|
||||
import moe.lava.banksia.ui.map.util.CameraPosition
|
||||
|
|
@ -36,8 +39,6 @@ import moe.lava.banksia.ui.map.util.CameraPositionBounds
|
|||
import moe.lava.banksia.ui.map.util.Marker
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
sealed class MapScreenEvent {
|
||||
data object DismissState : MapScreenEvent()
|
||||
|
|
@ -53,6 +54,9 @@ private data class InternalState(
|
|||
val route: String? = null,
|
||||
val stop: String? = null,
|
||||
val run: String? = null,
|
||||
|
||||
val lastStopDepartures: List<ExtendedStopTime>? = null,
|
||||
val stopsGrouped: Boolean = true,
|
||||
)
|
||||
|
||||
class MapScreenViewModel(
|
||||
|
|
@ -69,6 +73,10 @@ class MapScreenViewModel(
|
|||
viewModelScope.launch { switchRoute(value.route) }
|
||||
if (value.stop != last.stop)
|
||||
viewModelScope.launch { switchStop(value.stop) }
|
||||
if (value.lastStopDepartures != last.lastStopDepartures)
|
||||
viewModelScope.launch { buildDepartures() }
|
||||
if (value.stopsGrouped != last.stopsGrouped)
|
||||
viewModelScope.launch { buildDepartures() }
|
||||
if (value.run != last.run)
|
||||
switchRun(value.run)
|
||||
}
|
||||
|
|
@ -105,7 +113,9 @@ class MapScreenViewModel(
|
|||
|
||||
fun handleEvent(event: InfoPanelEvent) {
|
||||
viewModelScope.launch {
|
||||
// when (event) { }
|
||||
when (event) {
|
||||
StopInfoPanelEvent.ToggleGrouping -> state = state.copy(stopsGrouped = !state.stopsGrouped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +175,7 @@ class MapScreenViewModel(
|
|||
}
|
||||
|
||||
val route = routeRepository.get(routeId)
|
||||
// val gtfsRoute = ptvService.route(routeId)
|
||||
?: return
|
||||
iInfoState.update {
|
||||
RouteInfoPanelState(
|
||||
name = route.name,
|
||||
|
|
@ -215,11 +225,11 @@ class MapScreenViewModel(
|
|||
private suspend fun switchStop(id: String?) {
|
||||
if (id == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
state = state.copy(lastStopDepartures = null)
|
||||
return
|
||||
}
|
||||
|
||||
val stop = stopRepository.get(id)
|
||||
// val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.name.split("/")
|
||||
val name = split[0]
|
||||
val subname = split.getOrNull(1)
|
||||
|
|
@ -232,27 +242,54 @@ class MapScreenViewModel(
|
|||
}
|
||||
|
||||
stopTimeRepository.getForStop(id)
|
||||
.onEach { stoptimes ->
|
||||
val departures = stoptimes
|
||||
// .filter { !it.headsign.isNullOrBlank() }
|
||||
// .groupBy { it.headsign!! }
|
||||
.groupBy { it.stopId } // TODO: Placeholder
|
||||
.map { (headsign, stopTimes) ->
|
||||
val now = Clock.System.now()
|
||||
val times = stopTimes
|
||||
.map { it.time.arrival.toInstant(TimeZone.currentSystemDefault()) }
|
||||
.filter { it >= (now - 1.minutes) }
|
||||
.joinToString(" | ") {
|
||||
val diff = (it - now).inWholeMinutes.coerceAtLeast(0)
|
||||
if (diff >= 65) {
|
||||
"${((diff + 30.0) / 60.0).toInt()}hr"
|
||||
} else {
|
||||
"${diff}mn"
|
||||
.onEach { departures ->
|
||||
state = state.copy(
|
||||
lastStopDepartures = departures
|
||||
)
|
||||
}
|
||||
}
|
||||
StopInfoPanelState.Departure(headsign, times)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun friendlyPlatform(platform: String) =
|
||||
platform.takeUnless { it.firstOrNull()?.isDigit() == true }
|
||||
?: "Platform $platform"
|
||||
private fun buildDepartures() {
|
||||
val rawDepartures = state.lastStopDepartures ?: return
|
||||
val departures = if (state.stopsGrouped) {
|
||||
rawDepartures
|
||||
.groupBy { it.stopPlatformCode }
|
||||
.mapKeys { (platform) -> platform?.let { friendlyPlatform(it) } }
|
||||
.entries
|
||||
.sortedBy { (platform) -> platform }
|
||||
.map { (platform, deps) ->
|
||||
StopInfoPanelState.DeparturePlatforms(
|
||||
platform = platform ?: "",
|
||||
departures = deps.map {
|
||||
StopInfoPanelState.DepartureInfo(
|
||||
routeName = it.routeNumber ?: it.routeName,
|
||||
routeColour = it.routeType.getUIProperties().colour,
|
||||
headsign = it.headsign ?: it.routeName,
|
||||
description = null,
|
||||
time = it.time.departure.toInstant(TimeZone.currentSystemDefault()),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (rawDepartures.isEmpty()) {
|
||||
listOf()
|
||||
} else {
|
||||
listOf(StopInfoPanelState.DeparturePlatforms(platform = "", departures = rawDepartures.map { dep ->
|
||||
StopInfoPanelState.DepartureInfo(
|
||||
routeName = dep.routeNumber ?: dep.routeName,
|
||||
routeColour = dep.routeType.getUIProperties().colour,
|
||||
headsign = dep.headsign ?: dep.routeName,
|
||||
description = dep.stopPlatformCode?.let { friendlyPlatform(it) },
|
||||
time = dep.time.departure.toInstant(TimeZone.currentSystemDefault()),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
departures.let { departures ->
|
||||
iInfoState.update {
|
||||
if (it !is StopInfoPanelState)
|
||||
it
|
||||
|
|
@ -260,7 +297,6 @@ class MapScreenViewModel(
|
|||
it.copy(departures = departures)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
/*private suspend fun buildPolylines(route: PtvRoute) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue