feat: stop info panel
This commit is contained in:
parent
b417118e3d
commit
339e8c802f
7 changed files with 258 additions and 13 deletions
|
|
@ -49,6 +49,7 @@ kotlin {
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.lifecycle.viewmodel)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.moko.geo)
|
implementation(libs.moko.geo)
|
||||||
implementation(libs.moko.geo.compose)
|
implementation(libs.moko.geo.compose)
|
||||||
implementation(projects.shared)
|
implementation(projects.shared)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.add
|
import androidx.compose.foundation.layout.add
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeContent
|
import androidx.compose.foundation.layout.safeContent
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
import androidx.compose.material3.BottomSheetScaffold
|
import androidx.compose.material3.BottomSheetScaffold
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
|
@ -28,6 +31,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
||||||
|
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 dev.icerock.moko.geo.compose.BindLocationTrackerEffect
|
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
|
||||||
|
|
@ -49,6 +53,7 @@ import moe.lava.banksia.native.maps.getScreenHeight
|
||||||
import moe.lava.banksia.resources.Res
|
import moe.lava.banksia.resources.Res
|
||||||
import moe.lava.banksia.resources.my_location_24
|
import moe.lava.banksia.resources.my_location_24
|
||||||
import moe.lava.banksia.ui.Searcher
|
import moe.lava.banksia.ui.Searcher
|
||||||
|
import moe.lava.banksia.ui.StopInfoPanel
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
@ -147,18 +152,44 @@ fun App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var sheetSwipeEnabled by remember { mutableStateOf(true) }
|
var sheetSwipeEnabled by remember { mutableStateOf(true) }
|
||||||
var peekHeight by remember { mutableStateOf(128.dp) }
|
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||||
|
var peekHeight by remember { mutableStateOf(0.dp) }
|
||||||
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
||||||
|
|
||||||
|
var stop by remember { mutableStateOf<PtvStop?>(null) }
|
||||||
var markers by remember { mutableStateOf(listOf<Marker>()) }
|
var markers by remember { mutableStateOf(listOf<Marker>()) }
|
||||||
LaunchedEffect(route) { route?.let { markers = buildStops(ptvService, it) {} } }
|
LaunchedEffect(route) {
|
||||||
|
route?.let { route ->
|
||||||
|
markers = buildStops(ptvService, route) {
|
||||||
|
stop = it
|
||||||
|
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BanksiaTheme {
|
BanksiaTheme {
|
||||||
BottomSheetScaffold(
|
BottomSheetScaffold(
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
sheetPeekHeight = peekHeight * peekHeightMultiplier,
|
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
sheetContent = { Box(modifier = Modifier) },
|
sheetContent = { stop?.let {
|
||||||
|
StopInfoPanel(ptvService, it) {
|
||||||
|
peekHeight = it
|
||||||
|
}
|
||||||
|
} },
|
||||||
|
sheetDragHandle = {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.onSizeChanged {
|
||||||
|
handleHeight = with(density) { it.height.toDp() }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
BottomSheetDefaults.DragHandle(modifier = Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
},
|
||||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||||
) {
|
) {
|
||||||
Maps(
|
Maps(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package moe.lava.banksia.ui
|
||||||
|
|
||||||
|
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.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeContent
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
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.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.coerceAtMost
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import moe.lava.banksia.api.ptv.PtvService
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvStop
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StopInfoPanel(
|
||||||
|
ptvService: PtvService,
|
||||||
|
stop: PtvStop,
|
||||||
|
onPeekHeightChange: (Dp) -> Unit,
|
||||||
|
) {
|
||||||
|
var departures by remember { mutableStateOf<List<Pair<String, String>>>(listOf()) }
|
||||||
|
var loading by remember { mutableStateOf(true) }
|
||||||
|
// [TODO]: Cleanup
|
||||||
|
LaunchedEffect(stop) {
|
||||||
|
loading = true
|
||||||
|
val res = ptvService.departures(stop.routeType, stop.stopId)
|
||||||
|
// Map<
|
||||||
|
// Pair<DirectionId, RouteId>,
|
||||||
|
// Pair<DirectionName, List<DepartureTimes>>
|
||||||
|
// >
|
||||||
|
val timetable = HashMap<Pair<Int, Int>, Pair<String, MutableList<String>>>()
|
||||||
|
res.departures.forEach { dep ->
|
||||||
|
val key = Pair(dep.directionId, dep.routeId)
|
||||||
|
val direction = ptvService.cache.direction(dep.directionId, dep.routeId) ?: return@forEach
|
||||||
|
val route = res.routes[dep.routeId.toString()]
|
||||||
|
val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: ""
|
||||||
|
val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second
|
||||||
|
if (element.size >= 5)
|
||||||
|
return@forEach
|
||||||
|
|
||||||
|
val date = Instant.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc)
|
||||||
|
val min = (date - Clock.System.now()).inWholeMinutes
|
||||||
|
if (min <= -5)
|
||||||
|
return@forEach
|
||||||
|
if (min >= 65)
|
||||||
|
element.add("${((min + 30.0) / 60.0).toInt()}hr")
|
||||||
|
else
|
||||||
|
element.add("${min}mn")
|
||||||
|
}
|
||||||
|
departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
||||||
|
if (list.isEmpty())
|
||||||
|
Pair(name, "No departures")
|
||||||
|
else
|
||||||
|
Pair(name, list.joinToString(" | "))
|
||||||
|
}
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
val localDensity = LocalDensity.current;
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.heightIn(max = 250.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.onSizeChanged {
|
||||||
|
onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
val split = stop.stopName.split("/")
|
||||||
|
val mainName = split[0]
|
||||||
|
val subName = split.getOrNull(1)
|
||||||
|
Text(
|
||||||
|
mainName,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
if (subName != null)
|
||||||
|
Text(
|
||||||
|
"/ $subName",
|
||||||
|
modifier = Modifier.padding(start = 5.dp),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
if (!loading)
|
||||||
|
{
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
departures.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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loading)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ coroutines = "1.9.0"
|
||||||
geo = "0.8.0"
|
geo = "0.8.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
|
kotlinxDatetime = "0.6.2"
|
||||||
kotlinxSerializationJson = "1.8.1"
|
kotlinxSerializationJson = "1.8.1"
|
||||||
ktor = "3.1.1"
|
ktor = "3.1.1"
|
||||||
logback = "1.5.17"
|
logback = "1.5.17"
|
||||||
|
|
@ -28,20 +29,13 @@ secretsGradlePlugin = "2.0.1"
|
||||||
[libraries]
|
[libraries]
|
||||||
moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
|
moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" }
|
||||||
moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
|
moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" }
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
|
||||||
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
|
|
||||||
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
|
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
|
|
||||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
|
|
||||||
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
|
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
|
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||||
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.lava.banksia.Constants
|
import moe.lava.banksia.Constants
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvDeparture
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvDirection
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvStop
|
import moe.lava.banksia.api.ptv.structures.PtvStop
|
||||||
|
|
@ -27,9 +29,34 @@ object Responses {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PtvStopsResponse(val stops: List<PtvStop>)
|
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>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PtvService {
|
class PtvService {
|
||||||
|
class PtvCache(
|
||||||
|
private val service: PtvService,
|
||||||
|
private val directions: HashMap<Pair<Int, Int>, PtvDirection> = HashMap(),
|
||||||
|
) {
|
||||||
|
suspend fun direction(directionID: Int, routeID: Int): PtvDirection? {
|
||||||
|
val ret = directions[Pair(directionID, routeID)]
|
||||||
|
if (ret == null) {
|
||||||
|
val res = service.directionsByRoute(routeID)
|
||||||
|
for (dir in res)
|
||||||
|
directions[Pair(dir.directionId, dir.routeId)] = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret ?: directions[Pair(directionID, routeID)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cache = PtvCache(this)
|
||||||
|
|
||||||
private val client = HttpClient() {
|
private val client = HttpClient() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
|
|
@ -68,9 +95,10 @@ class PtvService {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopsByRoute(routeId: Int, routeType: PtvRouteType): List<PtvStop> {
|
suspend fun stopsByRoute(routeId: Int, routeType: PtvRouteType): List<PtvStop> {
|
||||||
val response: Responses.PtvStopsResponse = client.get("stops/route") {
|
val response: Responses.PtvStopsResponse = client.get("stops") {
|
||||||
url {
|
url {
|
||||||
appendPathSegments(
|
appendPathSegments(
|
||||||
|
"route",
|
||||||
routeId.toString(),
|
routeId.toString(),
|
||||||
"route_type",
|
"route_type",
|
||||||
routeType.ordinal.toString()
|
routeType.ordinal.toString()
|
||||||
|
|
@ -79,4 +107,38 @@ class PtvService {
|
||||||
}.body()
|
}.body()
|
||||||
return response.stops
|
return response.stops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun directionsByRoute(routeId: Int): List<PtvDirection> {
|
||||||
|
val response: Responses.PtvDirectionsResponse = client.get("directions") {
|
||||||
|
url {
|
||||||
|
appendPathSegments("route", routeId.toString())
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
return response.directions
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun direction(id: Int, routeType: PtvRouteType?): List<PtvDirection> {
|
||||||
|
val response: Responses.PtvDirectionsResponse = client.get("directions") {
|
||||||
|
url {
|
||||||
|
appendPathSegments(id.toString())
|
||||||
|
if (routeType != null)
|
||||||
|
appendPathSegments("route_type", routeType.ordinal.toString())
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
return response.directions
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun departures(routeType: PtvRouteType, stopId: Int): Responses.PtvDeparturesResponse =
|
||||||
|
client.get("departures") {
|
||||||
|
url {
|
||||||
|
appendPathSegments(
|
||||||
|
"route_type",
|
||||||
|
routeType.ordinal.toString(),
|
||||||
|
"stop",
|
||||||
|
stopId.toString()
|
||||||
|
)
|
||||||
|
parameter("expand", "Route")
|
||||||
|
parameter("expand", "Direction")
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package moe.lava.banksia.api.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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package moe.lava.banksia.api.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,
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue