refactor: shared -> core

This commit is contained in:
Cilly Leang 2026-04-02 01:57:08 +11:00
parent 104a77b27e
commit c912723c78
Signed by: cilly
GPG key ID: 6500251E087653C9
112 changed files with 133 additions and 140 deletions

View file

@ -0,0 +1,12 @@
package moe.lava.banksia.core.util
import android.util.Log
actual fun log(tag: String, msg: String) {
Log.i(tag, msg)
}
actual fun error(tag: String, msg: String, throwable: Throwable?) {
Log.e(tag, msg)
throwable?.let { Log.e(tag, it.stackTraceToString()) }
}

View file

@ -0,0 +1,12 @@
package moe.lava.banksia
object Constants {
const val devid: String = ""
const val key: String = ""
const val opendataKey: String = ""
const val serverUrl: String = "https://banksia.lava.moe/api/"
// TODO
const val devMode: Boolean = false
const val updateKey: String = ""
const val protomapsKey: String = ""
}

View file

@ -0,0 +1,58 @@
package moe.lava.banksia.core.model
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.atTime
import kotlinx.datetime.plus
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.lava.banksia.core.model.FutureTime.Companion.asInt
@Serializable(FutureTimeSerialiser::class)
data class FutureTime(
val dayOffset: Boolean,
val time: LocalTime,
) {
companion object {
fun from(hour: Int, minute: Int, second: Int): FutureTime {
var nHour = hour
val nextDay = hour >= 24
if (nextDay)
nHour -= 24
val time = LocalTime(nHour, minute, second)
return FutureTime(nextDay, time)
}
fun FutureTime.asInt() =
trueHour * 3600 + minute * 60 + second
fun fromInt(int: Int) = from(
int / 3600,
(int / 60) % 60,
int % 60,
)
}
val hour = time.hour
val minute = time.minute
val second = time.second
val trueHour = time.hour + (if (dayOffset) 24 else 0)
fun atDate(date: LocalDate) = date
.let { if (dayOffset) date.plus(1, DateTimeUnit.DAY) else date }
.atTime(time)
}
object FutureTimeSerialiser: KSerializer<FutureTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(FutureTimeSerialiser::class.qualifiedName!!, PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: FutureTime) = encoder.encodeInt(value.asInt())
override fun deserialize(decoder: Decoder) = FutureTime.fromInt(decoder.decodeInt())
}

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
data class Route(
val id: String,
val type: RouteType,
val number: String?,
val name: String,
)

View file

@ -0,0 +1,20 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
enum class RouteType(val value: Int) {
MetroTrain(2),
MetroTram(3),
MetroBus(4),
RegionalTrain(1),
RegionalCoach(5),
RegionalBus(6),
SkyBus(11),
Interstate(10),
;
companion object {
fun from(value: Int) = entries.first { it.value == value }
}
}

View file

@ -0,0 +1,5 @@
package moe.lava.banksia.core.model
data class Run(
val ref: String,
)

View file

@ -0,0 +1,13 @@
package moe.lava.banksia.core.model
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class Service(
val id: String,
val days: List<DayOfWeek>,
val start: LocalDate,
val end: LocalDate,
)

View file

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

View file

@ -0,0 +1,12 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
import moe.lava.banksia.core.util.Point
typealias ShapePath = List<Point>
@Serializable
data class Shape(
val id: String,
val path: ShapePath,
)

View file

@ -0,0 +1,15 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
import moe.lava.banksia.core.util.Point
@Serializable
data class Stop(
val id: String,
val name: String,
val pos: Point,
val parent: String?,
val hasWheelChairBoarding: Boolean,
val level: String,
val platformCode: String,
)

View file

@ -0,0 +1,14 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
data class StopTime(
val tripId: String,
val stopId: String,
val arrivalTime: FutureTime,
val departureTime: FutureTime,
val headsign: String?,
val pickupType: Int,
val dropOffType: Int,
)

View file

@ -0,0 +1,26 @@
package moe.lava.banksia.core.model
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
data class StopTimeDated(
val tripId: String,
val stopId: String,
val arrivalTime: LocalDateTime,
val departureTime: LocalDateTime,
val headsign: String?,
val pickupType: Int,
val dropOffType: Int,
)
fun StopTime.atDate(date: LocalDate) = StopTimeDated(
tripId = tripId,
stopId = stopId,
arrivalTime = arrivalTime.atDate(date),
departureTime = departureTime.atDate(date),
headsign = headsign,
pickupType = pickupType,
dropOffType = dropOffType,
)

View file

@ -0,0 +1,15 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
data class Trip(
val id: String,
val routeId: String,
val service: Service,
val shapeId: String?,
val tripHeadsign: String,
val directionId: String,
val blockId: String,
val wheelchairAccessible: String,
)

View file

@ -0,0 +1,9 @@
package moe.lava.banksia.core.model
import kotlinx.serialization.Serializable
@Serializable
data class VersionMetadata(
val type: String,
val lastUpdated: Long,
)

View file

@ -0,0 +1,10 @@
package moe.lava.banksia.core.util
/** Wraps an arbitrary value, such that equality checks are forced to be done by reference */
class BoxedValue<T>(val value: T) {
operator fun component1() = value
companion object {
fun <T> T.box() = BoxedValue(this)
}
}

View file

@ -0,0 +1,47 @@
package moe.lava.banksia.core.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class CacheMap<K, V>(
coroutineScope: CoroutineScope,
val expiryMinutes: Int = 5,
private val innerMap: MutableMap<K, V> = mutableMapOf()
) : MutableMap<K, V> by innerMap {
val keyExpiries = mutableMapOf<K, Int>()
var counter = 0
init {
coroutineScope.launch {
while (true) {
delay(60000)
counter += 1
keyExpiries
.filterValues { expiry -> expiry >= counter }
.keys
.forEach { key ->
innerMap.remove(key)
keyExpiries.remove(key)
}
}
}
}
override fun put(key: K, value: V): V? {
keyExpiries[key] = counter + expiryMinutes + 1
return innerMap.put(key, value)
}
override fun putAll(from: Map<out K, V>) {
keyExpiries.putAll(from.map { it.key to (counter + expiryMinutes + 1) })
innerMap.putAll(from)
}
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() {
error("CacheMap", ".entries accessed, cloning..", IllegalStateException())
return this.entries.toMutableSet()
}
}

View file

@ -0,0 +1,36 @@
package moe.lava.banksia.core.util
import kotlinx.datetime.DayOfWeek
private fun Int.check(other: Int) = (this and other) != 0
fun Int.deserialiseDaysBitflag(): List<DayOfWeek> = buildList {
val days = this@deserialiseDaysBitflag
if (days.check(1))
add(DayOfWeek.MONDAY)
if (days.check(1 shl 1))
add(DayOfWeek.TUESDAY)
if (days.check(1 shl 2))
add(DayOfWeek.WEDNESDAY)
if (days.check(1 shl 3))
add(DayOfWeek.THURSDAY)
if (days.check(1 shl 4))
add(DayOfWeek.FRIDAY)
if (days.check(1 shl 5))
add(DayOfWeek.SATURDAY)
if (days.check(1 shl 6))
add(DayOfWeek.SUNDAY)
}
fun List<DayOfWeek>.serialise(): Int =
this.fold(0) { vl, n ->
vl + when (n) {
DayOfWeek.MONDAY -> 1
DayOfWeek.TUESDAY -> 1 shl 1
DayOfWeek.WEDNESDAY -> 1 shl 2
DayOfWeek.THURSDAY -> 1 shl 3
DayOfWeek.FRIDAY -> 1 shl 4
DayOfWeek.SATURDAY -> 1 shl 5
DayOfWeek.SUNDAY -> 1 shl 6
}
}

View file

@ -0,0 +1,9 @@
package moe.lava.banksia.core.util
fun error(tag: String, throwable: Throwable) = error(tag, "", throwable)
expect fun log(tag: String, msg: String)
expect fun error(tag: String, msg: String, throwable: Throwable? = null)
class LogScope(private val tag: String) {
fun log(msg: String) = log(tag, msg)
}

View file

@ -0,0 +1,64 @@
package moe.lava.banksia.core.util
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.AbstractFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlin.experimental.ExperimentalTypeInference
@OptIn(ExperimentalCoroutinesApi::class)
class LoopFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() {
private var delayMs: Long = 5000
private var init: (suspend FlowCollector<T>.() -> Unit)? = null
private var waiter: (suspend () -> Unit)? = null
override suspend fun collectSafely(collector: FlowCollector<T>) {
init?.invoke(collector)
while (true) {
waiter?.invoke()
collector.block()
delay(delayMs)
}
}
companion object {
fun <T> Flow<T>.delayFor(delay: Long) = apply {
@Suppress("UnusedFlow")
if (this is LoopFlow)
this.delayMs = delay
else
throw IllegalStateException()
}
fun <T> Flow<T>.initWith(block: suspend FlowCollector<T>.() -> Unit) = apply {
@Suppress("UnusedFlow")
if (this is LoopFlow)
this.init = block
else
throw IllegalStateException()
}
fun <T> Flow<T>.waitFor(waiter: suspend () -> Unit) = apply {
@Suppress("UnusedFlow")
if (this is LoopFlow)
this.waiter = waiter
else
throw IllegalStateException()
}
fun <T> Flow<T>.waitUntilSubscribed(other: MutableStateFlow<*>) = waitFor {
val blocked = other.subscriptionCount.value == 0
if (blocked)
log("LoopFlow", "blocking flow")
other.subscriptionCount.first { it > 0 }
if (blocked)
log("LoopFlow", "unblocking flow")
}
}
}
@OptIn(ExperimentalTypeInference::class)
fun <T> loopFlow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit) = LoopFlow(block)

View file

@ -0,0 +1,6 @@
package moe.lava.banksia.core.util
import kotlinx.serialization.Serializable
@Serializable
data class Point(val lat: Double, val lng: Double)

View file

@ -0,0 +1,255 @@
package moe.lava.banksia.data.ptv
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.plugin
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.http.appendPathSegments
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.model.RouteType
import moe.lava.banksia.core.util.LoopFlow.Companion.initWith
import moe.lava.banksia.core.util.error
import moe.lava.banksia.core.util.log
import moe.lava.banksia.core.util.loopFlow
import moe.lava.banksia.data.ptv.structures.PtvDeparture
import moe.lava.banksia.data.ptv.structures.PtvDirection
import moe.lava.banksia.data.ptv.structures.PtvRoute
import moe.lava.banksia.data.ptv.structures.PtvRouteType
import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType
import moe.lava.banksia.data.ptv.structures.PtvRun
import moe.lava.banksia.data.ptv.structures.PtvStop
import okio.ByteString.Companion.encodeUtf8
import kotlin.random.Random
object Responses {
@Serializable
data class PtvRouteResponse(val route: PtvRoute)
@Serializable
data class PtvRoutesResponse(val routes: List<PtvRoute>)
@Serializable
data class PtvRunsResponse(val runs: List<PtvRun>)
@Serializable
data class PtvStopResponse(val stop: PtvStop)
@Serializable
data class PtvStopsResponse(val stops: List<PtvStop>)
@Serializable
data class PtvDeparturesResponse(val departures: List<PtvDeparture>, val routes: Map<String, PtvRoute>, val directions: Map<String, PtvDirection>)
@Serializable
data class PtvDirectionsResponse(val directions: List<PtvDirection>)
}
suspend inline fun <K, V> MutableMap<K, V>.getOrPutSuspend(key: K, defaultValue: suspend () -> V): V {
if (!containsKey(key))
this[key] = defaultValue()
return this[key]!!
}
class PtvService() {
class PtvCache(
val directions: MutableMap<Pair<Int, Int>, PtvDirection> = mutableMapOf(),
val routes: MutableMap<Int, PtvRoute> = mutableMapOf(),
val runs: MutableMap<String, PtvRun> = mutableMapOf(),
val stops: MutableMap<Int, PtvStop> = mutableMapOf(),
)
val cache = PtvCache()
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
defaultRequest {
url("https://timetableapi.ptv.vic.gov.au/v3/")
}
}
init {
client.plugin(HttpSend).intercept { req ->
req.parameter("devid", Constants.devid)
@OptIn(ExperimentalStdlibApi::class)
req.parameter("nonce", Random.nextBytes(6).toHexString())
val fullPath = req.url.build().encodedPathAndQuery
val hash = fullPath.encodeUtf8().hmacSha1(Constants.key.encodeUtf8()).hex()
req.parameter("signature", hash)
log("ktor.intercept", req.url.build().encodedPathAndQuery)
execute(req)
}
}
suspend fun HttpClient.safeGet(
urlString: String? = null,
retries: Int = 1,
block: (HttpRequestBuilder.() -> Unit)? = null
): HttpResponse =
runCatching {
get {
urlString?.let { url(it) }
block?.invoke(this)
}
}.getOrElse { e ->
error("PtvService", "Fetch error occurred (attempt $retries / 3), retrying in 5000ms...", e)
if (retries >= 3)
throw e
delay(5000)
safeGet(urlString, retries + 1, block)
}
suspend fun route(id: Int, includeGeopath: Boolean = false): PtvRoute {
val cached = cache.routes[id]
// TODO: im braindead so clean this up later
if (cached != null && (!includeGeopath || (includeGeopath && cached.geopath.isNotEmpty())))
return cached
return client
.safeGet("routes") {
url {
appendPathSegments(id.toString())
parameters.append("include_geopath", if (includeGeopath) "true" else "false")
}
}
.body<Responses.PtvRouteResponse>()
.route
.also { cache.routes[it.routeId] = it }
}
suspend fun routes(): List<PtvRoute> {
val cached = cache.routes
if (cached.isEmpty()) {
client
.safeGet("routes")
.body<Responses.PtvRoutesResponse>()
.routes
.forEach { route ->
cached[route.routeId] = route
}
}
return cached.values.toList()
}
fun runFlow(ref: String, firstWithCache: Boolean = false) =
loopFlow {
client
.safeGet {
url {
appendPathSegments("runs", ref)
}
}
.body<Responses.PtvRunsResponse>()
.runs
.also { it.forEach { run -> cache.runs[run.runRef] = run } }
.let { emit(it[0]) }
}.initWith {
cache.runs[ref]?.let {
if (firstWithCache)
emit(it)
}
}
fun runsFlow(routeId: Int) =
loopFlow {
client
.safeGet {
url {
appendPathSegments("runs", "route", routeId.toString())
parameter("expand", "VehiclePosition")
}
}
.body<Responses.PtvRunsResponse>()
.runs
.also { it.forEach { run -> cache.runs[run.runRef] = run } }
.let { emit(it) }
}
suspend fun stopsByRoute(routeId: Int, routeType: PtvRouteType): List<PtvStop> =
client
.safeGet("stops") {
url {
appendPathSegments(
"route", routeId.toString(),
"route_type", routeType.ordinal.toString(),
)
}
}
.body<Responses.PtvStopsResponse>()
.stops
.also { it.forEach { stop -> cache.stops[stop.stopId] = stop } }
suspend fun stop(routeType: PtvRouteType, stopId: Int): PtvStop =
cache.stops.getOrPutSuspend(stopId) {
client
.safeGet {
url {
appendPathSegments(
"stops", stopId.toString(),
"route_type", routeType.ordinal.toString(),
)
}
}
.body<Responses.PtvStopResponse>()
.stop
}
suspend fun directionsByRoute(routeId: Int): List<PtvDirection> =
client
.safeGet("directions") {
url {
appendPathSegments("route", routeId.toString())
}
}
.body<Responses.PtvDirectionsResponse>()
.directions
suspend fun direction(directionId: Int, routeId: Int): PtvDirection {
if (!cache.directions.containsKey(directionId to routeId)) {
val directions = directionsByRoute(routeId)
for (direction in directions)
cache.directions[direction.directionId to direction.routeId] = direction
}
return cache.directions[directionId to routeId]!!
}
suspend fun departures(routeType: RouteType, stopId: String): Responses.PtvDeparturesResponse =
client
.safeGet ("departures") {
url {
appendPathSegments(
"route_type", routeType.asPtvType().ordinal.toString(),
"stop", stopId,
)
parameter("expand", "Route")
parameter("expand", "Direction")
parameter("gtfs", "true")
}
}.body()
suspend fun departures(routeType: PtvRouteType, stopId: Int): Responses.PtvDeparturesResponse =
client
.safeGet ("departures") {
url {
appendPathSegments(
"route_type", routeType.ordinal.toString(),
"stop", stopId.toString(),
)
parameter("expand", "Route")
parameter("expand", "Direction")
}
}.body()
}

View file

@ -0,0 +1,12 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PtvDeparture(
@SerialName("scheduled_departure_utc") val scheduledDepartureUtc: String,
@SerialName("estimated_departure_utc") val estimatedDepartureUtc: String?,
@SerialName("direction_id") val directionId: Int,
@SerialName("route_id") val routeId: Int,
)

View file

@ -0,0 +1,11 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PtvDirection(
@SerialName("direction_id") val directionId: Int,
@SerialName("direction_name") val directionName: String,
@SerialName("route_id") val routeId: Int,
)

View file

@ -0,0 +1,13 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PtvGeopath(
@SerialName("direction_id") val directionId: Int,
@SerialName("valid_from") val validFrom: String,
@SerialName("valid_to") val validTo: String,
@SerialName("paths") val paths: List<String>,
)

View file

@ -0,0 +1,26 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import moe.lava.banksia.core.model.RouteType
@Serializable
data class PtvRoute(
@SerialName("route_type") val routeType: PtvRouteType,
@SerialName("route_id") val routeId: Int,
@SerialName("route_number") val routeNumber: String,
@SerialName("route_name") val routeName: String,
@SerialName("route_gtfs_id") val routeGtfsId: String,
@SerialName("geopath") val geopath: List<PtvGeopath>,
) {
fun gtfsSubType(): RouteType =
RouteType.entries.first { routeGtfsId.startsWith(it.value.toString() + "-") }
fun getShortFullName(): String {
var res = ""
if (this.routeNumber != "")
res += this.routeNumber + " - "
res += this.routeName.split(" via")[0]
return res
}
}

View file

@ -0,0 +1,50 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.lava.banksia.core.model.RouteType
object PtvRouteTypeSerialiser : KSerializer<PtvRouteType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
PtvRouteType::class.qualifiedName!!,
PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: PtvRouteType) {
encoder.encodeInt(value.ordinal)
}
override fun deserialize(decoder: Decoder): PtvRouteType {
val index = decoder.decodeInt()
return PtvRouteType.entries[index]
}
}
@Serializable(with = PtvRouteTypeSerialiser::class)
enum class PtvRouteType {
TRAIN,
TRAM,
BUS,
VLINE,
NIGHT_BUS,
;
companion object {
fun fromModel(type: RouteType) = when (type) {
RouteType.MetroTrain -> TRAIN
RouteType.MetroTram -> TRAM
RouteType.MetroBus -> BUS
RouteType.RegionalTrain -> VLINE
RouteType.RegionalCoach -> BUS
RouteType.RegionalBus -> BUS
RouteType.SkyBus -> BUS
RouteType.Interstate -> TRAIN
}
fun RouteType.asPtvType() = fromModel(this)
}
}

View file

@ -0,0 +1,72 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.time.Instant
// Some datetimes are in local time (no timezone), observed on bus vehicle positions,
// and some datetimes are in UTC, observed on train vehicle positions. We need to handle
// both cases.
private object CustomInstantSerialiser : KSerializer<Instant> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor(
CustomInstantSerialiser::class.qualifiedName!!,
PrimitiveKind.STRING,
)
override fun serialize(
encoder: Encoder,
value: Instant
) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
val str = decoder.decodeString()
return runCatching {
Instant.parse(str)
}.getOrElse {
LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault())
}
}
}
@Serializable
data class PtvVehiclePosition(
val latitude: Double,
val longitude: Double,
val easting: Double?,
val northing: Double?,
val direction: String?,
val bearing: Double?,
val supplier: String?,
@Serializable(CustomInstantSerialiser::class)
@SerialName("datetime_utc")
val datetimeUtc: Instant?,
@Serializable(CustomInstantSerialiser::class)
@SerialName("expiry_time")
val expiryTime: Instant?,
)
@Serializable
data class PtvRun(
@SerialName("run_ref") val runRef: String,
@SerialName("route_id") val routeId: Int,
@SerialName("route_type") val routeType: PtvRouteType,
@SerialName("final_stop_id") val finalStopId: Int,
@SerialName("destination_name") val destinationName: String,
@SerialName("direction_id") val directionId: Int,
@SerialName("status") val status: String,
@SerialName("vehicle_position") val vehiclePosition: PtvVehiclePosition?,
)

View file

@ -0,0 +1,13 @@
package moe.lava.banksia.data.ptv.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PtvStop(
@SerialName("stop_id") val stopId: Int,
@SerialName("stop_name") val stopName: String,
@SerialName("stop_latitude") val stopLatitude: Double?,
@SerialName("stop_longitude") val stopLongitude: Double?,
@SerialName("route_type") val routeType: PtvRouteType,
)

View file

@ -0,0 +1,9 @@
package moe.lava.banksia.core.util
actual fun log(tag: String, msg: String) {
TODO("Not yet implemented")
}
actual fun error(tag: String, msg: String, throwable: Throwable?) {
TODO("Not yet implemented")
}

View file

@ -0,0 +1,10 @@
package moe.lava.banksia.core.util
actual fun log(tag: String, msg: String) {
println("[$tag] $msg")
}
actual fun error(tag: String, msg: String, throwable: Throwable?) {
println("[$tag] $msg")
throwable?.let { println(it.stackTraceToString()) }
}