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() jvm()
sourceSets { sourceSets {
val clientMain by creating {
dependsOn(commonMain.get())
}
androidMain.get().dependsOn(clientMain)
iosArm64Main.get().dependsOn(clientMain)
iosSimulatorArm64Main.get().dependsOn(clientMain)
commonMain.dependencies { commonMain.dependencies {
implementation(libs.koin.core)
implementation(projects.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.route.RouteRemoteDataSource
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource 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.core.util.log
import moe.lava.banksia.data.ptv.PtvService import moe.lava.banksia.data.ptv.PtvService
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
val clientDataDiModule = module { actual val platformModule = module {
includes(sqldDiModule)
includes(stopTimeDataDiModule)
// HTTP Clients // HTTP Clients
singleOf(::PtvService) singleOf(::PtvService)
single { single {

View file

@ -4,6 +4,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource 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.route.RouteRemoteDataSource
import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.sqld.mappers.asModel import moe.lava.banksia.core.sqld.mappers.asModel
internal class ClientRouteRepository internal constructor( 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 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) { internal class RouteLocalDataSource(private val queries: RouteQueries) {
suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() } suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() }
suspend fun getAll() = withContext(Dispatchers.IO) { queries.getAll().executeAsList() } suspend fun getAll() = withContext(Dispatchers.IO) { queries.getAll().executeAsList() }
// suspend fun getByTrip(tripId: String) = dao.getByTrip(tripId)
suspend fun save(vararg routes: Route) { suspend fun save(vararg routes: Route) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
queries.transaction { queries.transaction {

View file

@ -7,5 +7,6 @@ import moe.lava.banksia.core.model.Route
internal class RouteRemoteDataSource(val client: HttpClient) { internal class RouteRemoteDataSource(val client: HttpClient) {
suspend fun get(id: String) = client.get("routes/${id}").body<Route>() 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>>() 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 import moe.lava.banksia.core.model.Route
interface RouteRepository { 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> 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.StopTime
import moe.lava.banksia.core.model.StoppingPattern import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.TimeType
import moe.lava.banksia.core.sqld.StoppingPattern as DbStoppingPattern 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, id = id,
routeId = routeId, routeId = routeId,
shapeId = shapeId, shapeId = shapeId,
@ -13,7 +14,7 @@ fun DbStoppingPattern.asModel(stoptimes: List<StopTime.Undated>) = StoppingPatte
stoptimes = stoptimes, stoptimes = stoptimes,
) )
fun StoppingPattern.Undated.asDb() = DbStoppingPattern( fun StoppingPattern<*>.asDb() = DbStoppingPattern(
id = id, id = id,
routeId = routeId, routeId = routeId,
shapeId = shapeId, shapeId = shapeId,

View file

@ -11,5 +11,10 @@ SELECT * FROM Route;
get: get:
SELECT * FROM Route WHERE id == ?; SELECT * FROM Route WHERE id == ?;
getByPattern:
SELECT Route.* FROM Route
INNER JOIN StoppingPattern ON Route.id == StoppingPattern.routeId
WHERE StoppingPattern.id == :patternId;
insert: 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 WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId) AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL; 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:
INSERT OR REPLACE INTO StoppingPattern VALUES ?; 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() ) : 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( fun StopTime<TimeType.Undated>.atDate(date: LocalDate) = StopTime(
patternId = patternId, patternId = patternId,
stopId = stopId, stopId = stopId,
time = TimeType.Dated( time = time.atDate(date),
arrival = time.arrival.atDate(date),
departure = time.departure.atDate(date),
),
pickupType = pickupType, pickupType = pickupType,
dropOffType = dropOffType, dropOffType = dropOffType,
) )

View file

@ -9,7 +9,7 @@ plugins {
kotlin { kotlin {
android { android {
namespace = "moe.lava.banksia.core.data.stoptime" namespace = "moe.lava.banksia.core.stoptime"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions { compilerOptions {
@ -56,5 +56,9 @@ kotlin {
iosMain.dependencies { iosMain.dependencies {
implementation(libs.ktor.client.darwin) 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.LocalDate
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn 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 import kotlin.time.Clock
internal class StopTimeRemoteDataSource( internal class StopTimeRemoteDataSource(
@ -16,9 +18,9 @@ internal class StopTimeRemoteDataSource(
suspend fun getAtStop( suspend fun getAtStop(
stopId: String, stopId: String,
date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()), date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()),
): List<StopTime.Dated> { ): List<ExtendedStopTime> {
return client.get("stoptimes/by_stop/${stopId}") { return client.get(Endpoint.stopTimeByStop(stopId)) {
parameter("date", date) 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.LocalDate
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import moe.lava.banksia.core.model.StopTime import moe.lava.banksia.core.data.dto.ExtendedStopTime
import kotlin.time.Clock import kotlin.time.Clock
expect class StopTimeRepository { expect class StopTimeRepository {
suspend fun getForStop( suspend fun getForStop(
id: String, id: String,
date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), 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.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import moe.lava.banksia.core.model.StopTime import moe.lava.banksia.core.data.dto.ExtendedStopTime
import moe.lava.banksia.core.model.atDate import moe.lava.banksia.core.data.dto.asModel
import moe.lava.banksia.core.sqld.StopTimeQueries 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.core.util.serialise
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@ -15,16 +14,16 @@ import org.koin.core.component.get
internal class StopTimeLocalDataSource : KoinComponent { internal class StopTimeLocalDataSource : KoinComponent {
private val queries get() = get<StopTimeQueries>() 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) { return withContext(context = Dispatchers.IO) {
queries queries
.getForStopDated( .getExtendedForStop(
listOf(date.dayOfWeek).serialise().toLong(), listOf(date.dayOfWeek).serialise().toLong(),
date.toEpochDays(), date.toEpochDays(),
stopId, stopId,
) )
.executeAsList() .executeAsList()
.map { it.asModel().atDate(date) } .map { it.asModel(date) }
.sortedBy { it.time.departure } .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)
}
}

View file

@ -20,7 +20,9 @@ kotlin {
dependencies { dependencies {
implementation(projects.core) implementation(projects.core)
implementation(projects.core.data)
implementation(projects.core.sqld) implementation(projects.core.sqld)
implementation(projects.core.stoptime)
implementation(projects.server.gtfs) implementation(projects.server.gtfs)
implementation(projects.server.gtfsRt) implementation(projects.server.gtfsRt)

View file

@ -15,22 +15,16 @@ import io.ktor.server.routing.routing
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.Constants
import moe.lava.banksia.core.model.atDate
import moe.lava.banksia.core.sqld.RouteQueries import moe.lava.banksia.core.sqld.RouteQueries
import moe.lava.banksia.core.sqld.StopQueries 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.sqld.mappers.asModel
import moe.lava.banksia.core.util.serialise
import moe.lava.banksia.server.di.ServerModules import moe.lava.banksia.server.di.ServerModules
import moe.lava.banksia.server.gtfsrt.GtfsrtService import moe.lava.banksia.server.gtfsrt.GtfsrtService
import moe.lava.banksia.server.routes.stopTimeRoutes
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
import org.koin.ktor.plugin.Koin import org.koin.ktor.plugin.Koin
import kotlin.time.Clock
fun main() { fun main() {
if (System.getenv("BANKSIA_PRODUCTION") == "1") Constants.devMode = false if (System.getenv("BANKSIA_PRODUCTION") == "1") Constants.devMode = false
@ -53,6 +47,8 @@ fun Application.module() {
launch { get<GtfsrtService>().start(this, !Constants.devMode) } launch { get<GtfsrtService>().start(this, !Constants.devMode) }
routing { routing {
stopTimeRoutes()
if (Constants.devMode) { if (Constants.devMode) {
get("/fixup") { get("/fixup") {
call.respondText("received") call.respondText("received")
@ -137,23 +133,5 @@ fun Application.module() {
} }
call.respond(stops.map { it.asModel() }) 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)
}
} }
} }

View file

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

View file

@ -37,9 +37,7 @@ include(":server:gtfs")
include(":server:gtfs_rt") include(":server:gtfs_rt")
include(":core") include(":core")
include(":core:data") include(":core:data")
include(":core:data:client") include(":core:stoptime")
include(":core:data:server")
include(":core:data:stoptime")
include(":core:sqld") include(":core:sqld")
include(":ui") include(":ui")
include(":ui:maps") include(":ui:maps")

View file

@ -41,7 +41,9 @@ kotlin {
sourceSets { sourceSets {
androidMain.dependencies { androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(projects.ui.shared)
} }
commonMain.dependencies { commonMain.dependencies {
implementation(libs.compose.components.resources) implementation(libs.compose.components.resources)
@ -68,7 +70,8 @@ kotlin {
implementation(libs.ui.backhandler) implementation(libs.ui.backhandler)
implementation(projects.core) implementation(projects.core)
implementation(projects.core.data.client) implementation(projects.core.data)
implementation(projects.core.stoptime)
implementation(projects.ui.maps) implementation(projects.ui.maps)
implementation(projects.ui.shared) implementation(projects.ui.shared)
} }

View file

@ -16,6 +16,10 @@ kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) jvmTarget.set(JvmTarget.JVM_11)
} }
androidResources {
enable = true
}
} }
compilerOptions { compilerOptions {
@ -47,4 +51,5 @@ dependencies {
compose.resources { compose.resources {
publicResClass = true publicResClass = true
packageOfResClass = "moe.lava.banksia.resources" packageOfResClass = "moe.lava.banksia.resources"
generateResClass = always
} }

View file

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

View file

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

View file

@ -1,12 +1,12 @@
package moe.lava.banksia.ui.di 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 moe.lava.banksia.ui.screens.map.MapScreenViewModel
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
val AppModule = module { val AppModule = module {
includes(clientDataDiModule) includes(dataDiModule)
// ViewModel // ViewModel
viewModelOf(::MapScreenViewModel) viewModelOf(::MapScreenViewModel)

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.size 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.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -45,6 +45,7 @@ sealed class InfoPanelState {
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun InfoPanel( fun InfoPanel(
modifier: Modifier = Modifier,
state: InfoPanelState, state: InfoPanelState,
onEvent: (InfoPanelEvent) -> Unit, onEvent: (InfoPanelEvent) -> Unit,
onPeekHeightChange: (Dp) -> Unit, onPeekHeightChange: (Dp) -> Unit,
@ -65,11 +66,13 @@ fun InfoPanel(
} }
Column( Column(
Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.heightIn(min = 350.dp)
.onSizeChanged { .onSizeChanged {
onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) }) // onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
onPeekHeightChange(350.dp)
} }
) { ) {
Box { Box {

View file

@ -1,42 +1,114 @@
package moe.lava.banksia.ui.layout.info package moe.lava.banksia.ui.layout.info
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
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.MaterialTheme
import androidx.compose.material3.SegmentedListItem
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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 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( data class StopInfoPanelState(
val id: String, val id: String,
val name: String, val name: String,
val subname: String? = null, val subname: String? = null,
val departures: List<Departure>? = null, val departures: List<DeparturePlatforms>? = null,
) : InfoPanelState() { ) : InfoPanelState() {
override val loading: Boolean 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,
)
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
internal fun StopInfoPanel( internal fun StopInfoPanel(
state: StopInfoPanelState, state: StopInfoPanelState,
onEvent: (StopInfoPanelEvent) -> Unit, onEvent: (StopInfoPanelEvent) -> Unit,
) { ) {
Column(Modifier.fillMaxWidth()) { val colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedContentColor = MaterialTheme.colorScheme.onPrimary,
)
// val spec = fadeIn(MaterialTheme.motionScheme.defaultEffectsSpec())
// .togetherWith(fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()))
val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300))
AnimatedContent(
targetState = state,
contentKey = { it.id },
// transitionSpec = { spec },
transitionSpec = { spec },
) { state ->
Column(Modifier.fillMaxWidth().fillMaxHeight()) {
Row {
Column {
Text( Text(
state.name, state.name,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -53,23 +125,210 @@ internal fun StopInfoPanel(
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
} }
state.departures?.let { }
Spacer(Modifier.height(5.dp)) IconButton(
it.forEach { (name, formatted) -> onClick = { onEvent(StopInfoPanelEvent.ToggleGrouping) },
Row(verticalAlignment = Alignment.CenterVertically) { ) { Icon(Icons.Default.Edit, null) }
}
Spacer(Modifier.height(10.dp))
AnimatedContent(
targetState = state.departures,
transitionSpec = { spec },
) { departures ->
departures?.let { departurePlatforms ->
val lazyState = if (departurePlatforms.size == 1) {
LazyListState(firstVisibleItemIndex =
departurePlatforms[0].departures.indexOfFirst {
it.time > Clock.System.now()
}.coerceAtLeast(0)
)
} else LazyListState()
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
state = lazyState,
) {
if (departurePlatforms.size > 1) {
items(departurePlatforms) { (platform, departures) ->
// departurePlatforms.forEach { (platform, departures) ->
var expanded by rememberSaveable { mutableStateOf(true) }
val base = ListItemDefaults.segmentedShapes(0, 2)
val large = MaterialTheme.shapes.large
if (departurePlatforms.size > 1) {
SegmentedListItem(
onClick = { expanded = !expanded },
colors = colors,
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(
name, text = platform,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold )
}
}
AnimatedVisibility(
visible = expanded,
enter = expandVertically(MaterialTheme.motionScheme.fastSpatialSpec()),
exit = shrinkVertically(MaterialTheme.motionScheme.fastSpatialSpec()),
) {
Column(
modifier = Modifier.height(200.dp),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap)
) {
departures
.filter { it.time > Clock.System.now() }
.take(5)
.forEachIndexed { idx, dep ->
SegmentedListItem(
onClick = {},
colors = colors,
shapes = ListItemDefaults.segmentedShapes(
idx + if (departurePlatforms.size > 1) 1 else 0,
departures.size + 1
),
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(
formatted, text = "mn",
maxLines = 1, style = MaterialTheme.typography.labelSmallEmphasized,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 5.dp)
) )
} }
},
) {
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,
)
}
}
}
}
}
Spacer(modifier = Modifier.height(10.dp))
}
} else if (departurePlatforms.size == 1) {
itemsIndexed(departurePlatforms[0].departures) { idx, dep ->
// departurePlatforms[0].departures.forEachIndexed { idx, dep ->
SegmentedListItem(
onClick = {},
colors = colors,
shapes = ListItemDefaults.segmentedShapes(
idx,
departurePlatforms[0].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,
)
}
}
}
}
}
}
} }
} }
} }
} }
@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 = {},
)
}

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant 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.RouteRepository
import moe.lava.banksia.core.data.repositories.StopRepository import moe.lava.banksia.core.data.repositories.StopRepository
import moe.lava.banksia.core.data.repositories.StopTimeRepository 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.Point
import moe.lava.banksia.core.util.log 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.ui.extensions.getUIProperties
import moe.lava.banksia.ui.layout.info.InfoPanelEvent import moe.lava.banksia.ui.layout.info.InfoPanelEvent
import moe.lava.banksia.ui.layout.info.InfoPanelState import moe.lava.banksia.ui.layout.info.InfoPanelState
import moe.lava.banksia.ui.layout.info.RouteInfoPanelState 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.StopInfoPanelState
import moe.lava.banksia.ui.layout.info.TripInfoPanelState import moe.lava.banksia.ui.layout.info.TripInfoPanelState
import moe.lava.banksia.ui.map.util.CameraPosition 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.map.util.Marker
import moe.lava.banksia.ui.state.MapState import moe.lava.banksia.ui.state.MapState
import moe.lava.banksia.ui.state.SearchState import moe.lava.banksia.ui.state.SearchState
import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes
sealed class MapScreenEvent { sealed class MapScreenEvent {
data object DismissState : MapScreenEvent() data object DismissState : MapScreenEvent()
@ -53,6 +54,9 @@ private data class InternalState(
val route: String? = null, val route: String? = null,
val stop: String? = null, val stop: String? = null,
val run: String? = null, val run: String? = null,
val lastStopDepartures: List<ExtendedStopTime>? = null,
val stopsGrouped: Boolean = true,
) )
class MapScreenViewModel( class MapScreenViewModel(
@ -69,6 +73,10 @@ class MapScreenViewModel(
viewModelScope.launch { switchRoute(value.route) } viewModelScope.launch { switchRoute(value.route) }
if (value.stop != last.stop) if (value.stop != last.stop)
viewModelScope.launch { switchStop(value.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) if (value.run != last.run)
switchRun(value.run) switchRun(value.run)
} }
@ -105,7 +113,9 @@ class MapScreenViewModel(
fun handleEvent(event: InfoPanelEvent) { fun handleEvent(event: InfoPanelEvent) {
viewModelScope.launch { 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 route = routeRepository.get(routeId)
// val gtfsRoute = ptvService.route(routeId) ?: return
iInfoState.update { iInfoState.update {
RouteInfoPanelState( RouteInfoPanelState(
name = route.name, name = route.name,
@ -215,11 +225,11 @@ class MapScreenViewModel(
private suspend fun switchStop(id: String?) { private suspend fun switchStop(id: String?) {
if (id == null) { if (id == null) {
iInfoState.update { InfoPanelState.None } iInfoState.update { InfoPanelState.None }
state = state.copy(lastStopDepartures = null)
return return
} }
val stop = stopRepository.get(id) val stop = stopRepository.get(id)
// val stop = ptvService.stop(routeType, stopId)
val split = stop.name.split("/") val split = stop.name.split("/")
val name = split[0] val name = split[0]
val subname = split.getOrNull(1) val subname = split.getOrNull(1)
@ -232,27 +242,54 @@ class MapScreenViewModel(
} }
stopTimeRepository.getForStop(id) stopTimeRepository.getForStop(id)
.onEach { stoptimes -> .onEach { departures ->
val departures = stoptimes state = state.copy(
// .filter { !it.headsign.isNullOrBlank() } lastStopDepartures = departures
// .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"
} }
} .launchIn(viewModelScope)
StopInfoPanelState.Departure(headsign, times)
} }
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 { iInfoState.update {
if (it !is StopInfoPanelState) if (it !is StopInfoPanelState)
it it
@ -260,7 +297,6 @@ class MapScreenViewModel(
it.copy(departures = departures) it.copy(departures = departures)
} }
} }
.launchIn(viewModelScope)
} }
/*private suspend fun buildPolylines(route: PtvRoute) { /*private suspend fun buildPolylines(route: PtvRoute) {