feat: vehicle positions and state dismissal
This commit is contained in:
parent
c526269e5d
commit
e52274a6ef
10 changed files with 163 additions and 42 deletions
|
|
@ -18,7 +18,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
|
@ -42,6 +41,7 @@ import com.google.maps.android.compose.rememberCameraPositionState
|
||||||
import com.google.maps.android.compose.rememberMarkerState
|
import com.google.maps.android.compose.rememberMarkerState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import moe.lava.banksia.R
|
import moe.lava.banksia.R
|
||||||
|
import moe.lava.banksia.api.ptv.structures.ComposableRouteIcon
|
||||||
import moe.lava.banksia.native.BanksiaTheme
|
import moe.lava.banksia.native.BanksiaTheme
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
import com.google.android.gms.maps.model.CameraPosition as GoogleCameraPosition
|
import com.google.android.gms.maps.model.CameraPosition as GoogleCameraPosition
|
||||||
|
|
@ -70,7 +70,6 @@ actual fun Maps(
|
||||||
setLastKnownLocation: (Point) -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val camPos = rememberCameraPositionState()
|
val camPos = rememberCameraPositionState()
|
||||||
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
||||||
LaunchedEffect(newCameraPos) {
|
LaunchedEffect(newCameraPos) {
|
||||||
|
|
@ -120,17 +119,26 @@ actual fun Maps(
|
||||||
val state = rememberMarkerState()
|
val state = rememberMarkerState()
|
||||||
state.position = marker.point.toLatLng()
|
state.position = marker.point.toLatLng()
|
||||||
MarkerComposable(
|
MarkerComposable(
|
||||||
keys = arrayOf(marker.colour),
|
keys = arrayOf(marker.data),
|
||||||
|
zIndex = if (marker.data is Marker.Data.Vehicle) 1f else 0f,
|
||||||
state = state,
|
state = state,
|
||||||
onClick = { marker.onClick() }
|
onClick = { marker.onClick() }
|
||||||
) {
|
) {
|
||||||
|
when (marker.data) {
|
||||||
|
is Marker.Data.Stop ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(12.dp)
|
.size(12.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(BanksiaTheme.colors.surface)
|
.background(BanksiaTheme.colors.surface)
|
||||||
.border(2.dp, marker.colour, CircleShape)
|
.border(2.dp, marker.data.colour, CircleShape)
|
||||||
)
|
)
|
||||||
|
is Marker.Data.Vehicle ->
|
||||||
|
ComposableRouteIcon(
|
||||||
|
size = 30.dp,
|
||||||
|
routeType = marker.data.type,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (polyline in polylines) {
|
for (polyline in polylines) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import moe.lava.banksia.native.maps.Point
|
||||||
import moe.lava.banksia.native.maps.getScreenHeight
|
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.BanksiaEvent
|
||||||
import moe.lava.banksia.ui.BanksiaViewModel
|
import moe.lava.banksia.ui.BanksiaViewModel
|
||||||
import moe.lava.banksia.ui.InfoPanel
|
import moe.lava.banksia.ui.InfoPanel
|
||||||
import moe.lava.banksia.ui.Searcher
|
import moe.lava.banksia.ui.Searcher
|
||||||
|
|
@ -139,7 +140,7 @@ fun App(
|
||||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||||
}, bottom = extInsets),
|
}, bottom = extInsets),
|
||||||
markers = mapState.markers,
|
markers = mapState.stops + mapState.vehicles,
|
||||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||||
polylines = mapState.polylines,
|
polylines = mapState.polylines,
|
||||||
)
|
)
|
||||||
|
|
@ -168,6 +169,7 @@ fun App(
|
||||||
scope.launch {
|
scope.launch {
|
||||||
scaffoldState.bottomSheetState.hide()
|
scaffoldState.bottomSheetState.hide()
|
||||||
peekHeightMultiplier = 1F
|
peekHeightMultiplier = 1F
|
||||||
|
viewModel.handleEvent(BanksiaEvent.DismissState)
|
||||||
}
|
}
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
peekHeightMultiplier = 1F
|
peekHeightMultiplier = 1F
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
package moe.lava.banksia.api.ptv.structures
|
package moe.lava.banksia.api.ptv.structures
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import moe.lava.banksia.resources.Res
|
import moe.lava.banksia.resources.Res
|
||||||
import moe.lava.banksia.resources.bus
|
import moe.lava.banksia.resources.bus
|
||||||
|
|
@ -18,6 +23,7 @@ import moe.lava.banksia.resources.tram_background
|
||||||
import moe.lava.banksia.resources.tram_icon
|
import moe.lava.banksia.resources.tram_icon
|
||||||
import org.jetbrains.compose.resources.DrawableResource
|
import org.jetbrains.compose.resources.DrawableResource
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
data class RouteTypeProperties(
|
data class RouteTypeProperties(
|
||||||
val colour: Color,
|
val colour: Color,
|
||||||
|
|
@ -43,18 +49,37 @@ fun PtvRouteType.getProperties(): RouteTypeProperties {
|
||||||
return RouteTypeProperties(colour, drawable, background, icon)
|
return RouteTypeProperties(colour, drawable, background, icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val ICON_PADDING = 0.25f
|
||||||
|
|
||||||
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ComposableRouteIcon(routeType: PtvRouteType) {
|
private fun RouteIconPreview() {
|
||||||
|
Row {
|
||||||
|
ComposableRouteIcon(routeType = PtvRouteType.TRAIN)
|
||||||
|
ComposableRouteIcon(routeType = PtvRouteType.TRAM)
|
||||||
|
ComposableRouteIcon(routeType = PtvRouteType.BUS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComposableRouteIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 40.dp,
|
||||||
|
routeType: PtvRouteType,
|
||||||
|
) {
|
||||||
val properties = routeType.getProperties()
|
val properties = routeType.getProperties()
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(properties.icon),
|
painter = painterResource(properties.icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.padding(size * ICON_PADDING / 2)
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
drawCircle(properties.colour, radius = (this.size.minDimension + 10.dp.toPx()) / 2f)
|
drawCircle(properties.colour, radius = size.toPx() / 2f)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
inline fun PtvRouteType.ComposableIcon() = ComposableRouteIcon(this)
|
inline fun PtvRouteType.ComposableIcon(modifier: Modifier = Modifier) = ComposableRouteIcon(modifier, routeType = this)
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,19 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
|
|
||||||
enum class MarkerType {
|
|
||||||
GENERIC_STOP,
|
|
||||||
}
|
|
||||||
data class Marker(
|
data class Marker(
|
||||||
val point: Point,
|
val point: Point,
|
||||||
val type: MarkerType,
|
val data: Data,
|
||||||
val colour: Color,
|
val onClick: () -> Boolean,
|
||||||
val onClick: () -> Boolean
|
) {
|
||||||
)
|
sealed class Data {
|
||||||
|
data class Stop(val colour: Color) : Data()
|
||||||
|
data class Vehicle(val type: PtvRouteType) : Data()
|
||||||
|
}
|
||||||
|
}
|
||||||
data class Point(val lat: Double, val lng: Double)
|
data class Point(val lat: Double, val lng: Double)
|
||||||
data class Polyline(val points: List<Point>, val colour: Color)
|
data class Polyline(val points: List<Point>, val colour: Color)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import moe.lava.banksia.log
|
||||||
import moe.lava.banksia.native.maps.CameraPosition
|
import moe.lava.banksia.native.maps.CameraPosition
|
||||||
import moe.lava.banksia.native.maps.CameraPositionBounds
|
import moe.lava.banksia.native.maps.CameraPositionBounds
|
||||||
import moe.lava.banksia.native.maps.Marker
|
import moe.lava.banksia.native.maps.Marker
|
||||||
import moe.lava.banksia.native.maps.MarkerType
|
|
||||||
import moe.lava.banksia.native.maps.Point
|
import moe.lava.banksia.native.maps.Point
|
||||||
import moe.lava.banksia.native.maps.Polyline
|
import moe.lava.banksia.native.maps.Polyline
|
||||||
import moe.lava.banksia.ui.state.InfoPanelState
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
|
|
@ -32,6 +31,8 @@ import moe.lava.banksia.util.BoxedValue
|
||||||
import moe.lava.banksia.util.BoxedValue.Companion.box
|
import moe.lava.banksia.util.BoxedValue.Companion.box
|
||||||
|
|
||||||
sealed class BanksiaEvent {
|
sealed class BanksiaEvent {
|
||||||
|
data object DismissState : BanksiaEvent()
|
||||||
|
|
||||||
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
||||||
data class SelectStop(val routeType: PtvRouteType, val stopId: Int?) : BanksiaEvent()
|
data class SelectStop(val routeType: PtvRouteType, val stopId: Int?) : BanksiaEvent()
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
fun handleEvent(event: BanksiaEvent) {
|
fun handleEvent(event: BanksiaEvent) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is BanksiaEvent.DismissState -> dismissState()
|
||||||
is BanksiaEvent.SelectRoute -> switchRoute(event.id)
|
is BanksiaEvent.SelectRoute -> switchRoute(event.id)
|
||||||
is BanksiaEvent.SelectStop -> switchStop(event.routeType, event.stopId)
|
is BanksiaEvent.SelectStop -> switchStop(event.routeType, event.stopId)
|
||||||
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
||||||
|
|
@ -87,6 +89,13 @@ class BanksiaViewModel : ViewModel() {
|
||||||
lastKnownLocation = location
|
lastKnownLocation = location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dismissState() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
switchRoute(null)
|
||||||
|
searchUpdate("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun searchUpdate(text: String) {
|
private suspend fun searchUpdate(text: String) {
|
||||||
val entries = ptvService.routes()
|
val entries = ptvService.routes()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
|
|
@ -126,6 +135,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch { buildPolylines(route) }
|
viewModelScope.launch { buildPolylines(route) }
|
||||||
|
viewModelScope.launch { buildRuns(route) }
|
||||||
viewModelScope.launch { buildStops(route) }
|
viewModelScope.launch { buildStops(route) }
|
||||||
// viewModelScope.launch { buildDepartures() }
|
// viewModelScope.launch { buildDepartures() }
|
||||||
// viewModelScope.launch { buildRuns() }
|
// viewModelScope.launch { buildRuns() }
|
||||||
|
|
@ -219,6 +229,22 @@ class BanksiaViewModel : ViewModel() {
|
||||||
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun buildRuns(route: PtvRoute) {
|
||||||
|
val runs = ptvService.runs(route.routeId)
|
||||||
|
|
||||||
|
val markers = runs.mapNotNull { it.vehiclePosition }
|
||||||
|
.distinctBy { it.latitude to it.longitude }
|
||||||
|
.map {
|
||||||
|
Marker(
|
||||||
|
Point(it.latitude, it.longitude),
|
||||||
|
onClick = { false },
|
||||||
|
data = Marker.Data.Vehicle(route.routeType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
iMapState.update { it.copy(vehicles = markers) }
|
||||||
|
}
|
||||||
|
|
||||||
// private suspend fun buildDepartures(route: PtvRoute) {
|
// private suspend fun buildDepartures(route: PtvRoute) {
|
||||||
// val directions = ptvService.directionsByRoute(route.routeId)
|
// val directions = ptvService.directionsByRoute(route.routeId)
|
||||||
//
|
//
|
||||||
|
|
@ -239,27 +265,22 @@ class BanksiaViewModel : ViewModel() {
|
||||||
|
|
||||||
private suspend fun buildStops(route: PtvRoute) {
|
private suspend fun buildStops(route: PtvRoute) {
|
||||||
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
||||||
val markers = mutableListOf<Marker>()
|
|
||||||
val colour = route.routeType.getProperties().colour
|
val colour = route.routeType.getProperties().colour
|
||||||
|
|
||||||
for (stop in stops) {
|
val markers = stops
|
||||||
if (stop.stopLatitude != null && stop.stopLongitude != null) {
|
.filter { it.stopLatitude != null && it.stopLongitude != null }
|
||||||
val pos = Point(stop.stopLatitude!!, stop.stopLongitude!!)
|
.map { stop ->
|
||||||
|
Marker(
|
||||||
val marker = Marker(
|
point = Point(stop.stopLatitude!!, stop.stopLongitude!!),
|
||||||
point = pos,
|
data = Marker.Data.Stop(colour),
|
||||||
type = MarkerType.GENERIC_STOP,
|
|
||||||
colour = colour,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModelScope.launch { switchStop(route.routeType, stop.stopId) }
|
viewModelScope.launch { switchStop(route.routeType, stop.stopId) }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
markers.add(marker)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iMapState.update { it.copy(markers = markers) }
|
iMapState.update { it.copy(stops = markers) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildBounds(points: List<Point>): CameraPositionBounds {
|
private fun buildBounds(points: List<Point>): CameraPositionBounds {
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,13 @@ fun InfoPanel(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RouteInfoPanel(
|
private inline fun RouteInfoPanel(
|
||||||
state: InfoPanelState.Route,
|
state: InfoPanelState.Route,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (BanksiaEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
Row {
|
Row {
|
||||||
ComposableRouteIcon(state.type)
|
ComposableRouteIcon(routeType = state.type)
|
||||||
Text(
|
Text(
|
||||||
state.name,
|
state.name,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
|
@ -88,7 +88,7 @@ private fun RouteInfoPanel(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StopInfoPanel(
|
private inline fun StopInfoPanel(
|
||||||
state: InfoPanelState.Stop,
|
state: InfoPanelState.Stop,
|
||||||
onEvent: (BanksiaEvent) -> Unit,
|
onEvent: (BanksiaEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ fun Searcher(
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(entry.mainText) },
|
headlineContent = { Text(entry.mainText) },
|
||||||
supportingContent = { entry.subText?.let { Text(it) } },
|
supportingContent = { entry.subText?.let { Text(it) } },
|
||||||
leadingContent = { ComposableRouteIcon(entry.routeType) },
|
leadingContent = { ComposableRouteIcon(routeType = entry.routeType) },
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import moe.lava.banksia.native.maps.Marker
|
||||||
import moe.lava.banksia.native.maps.Polyline
|
import moe.lava.banksia.native.maps.Polyline
|
||||||
|
|
||||||
data class MapState(
|
data class MapState(
|
||||||
val markers: List<Marker> = listOf(),
|
val stops: List<Marker> = listOf(),
|
||||||
|
val vehicles: List<Marker> = listOf(),
|
||||||
val polylines: List<Polyline> = listOf(),
|
val polylines: List<Polyline> = listOf(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import moe.lava.banksia.api.ptv.structures.PtvDeparture
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvDirection
|
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.PtvRun
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvStop
|
import moe.lava.banksia.api.ptv.structures.PtvStop
|
||||||
import moe.lava.banksia.log
|
import moe.lava.banksia.log
|
||||||
import okio.ByteString.Companion.encodeUtf8
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
|
|
@ -28,6 +29,9 @@ object Responses {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PtvRoutesResponse(val routes: List<PtvRoute>)
|
data class PtvRoutesResponse(val routes: List<PtvRoute>)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PtvRunsResponse(val runs: List<PtvRun>)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PtvStopResponse(val stop: PtvStop)
|
data class PtvStopResponse(val stop: PtvStop)
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -129,6 +133,20 @@ class PtvService {
|
||||||
return response.routes
|
return response.routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun runs(routeId: Int): List<PtvRun> {
|
||||||
|
val response: Responses.PtvRunsResponse = client.get() {
|
||||||
|
url {
|
||||||
|
appendPathSegments(
|
||||||
|
"runs",
|
||||||
|
"route",
|
||||||
|
routeId.toString(),
|
||||||
|
)
|
||||||
|
parameter("expand", "VehiclePosition")
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
return response.runs
|
||||||
|
}
|
||||||
|
|
||||||
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") {
|
val response: Responses.PtvStopsResponse = client.get("stops") {
|
||||||
url {
|
url {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,46 @@
|
||||||
package moe.lava.banksia.api.ptv.structures
|
package moe.lava.banksia.api.ptv.structures
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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
|
||||||
|
|
||||||
|
// 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(
|
data class PtvVehiclePosition(
|
||||||
val latitude: Double,
|
val latitude: Double,
|
||||||
val longitude: Double,
|
val longitude: Double,
|
||||||
|
|
@ -12,8 +49,14 @@ data class PtvVehiclePosition(
|
||||||
val direction: String?,
|
val direction: String?,
|
||||||
val bearing: Double?,
|
val bearing: Double?,
|
||||||
val supplier: String?,
|
val supplier: String?,
|
||||||
@SerialName("datetime_utc") val datetimeUtc: Instant?,
|
|
||||||
@SerialName("expiry_time") val expiryTime: Instant?,
|
@Serializable(CustomInstantSerialiser::class)
|
||||||
|
@SerialName("datetime_utc")
|
||||||
|
val datetimeUtc: Instant?,
|
||||||
|
|
||||||
|
@Serializable(CustomInstantSerialiser::class)
|
||||||
|
@SerialName("expiry_time")
|
||||||
|
val expiryTime: Instant?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -25,4 +68,5 @@ data class PtvRun(
|
||||||
@SerialName("destination_name") val destinationName: String,
|
@SerialName("destination_name") val destinationName: String,
|
||||||
@SerialName("direction_id") val directionId: Int,
|
@SerialName("direction_id") val directionId: Int,
|
||||||
@SerialName("status") val status: String,
|
@SerialName("status") val status: String,
|
||||||
|
@SerialName("vehicle_position") val vehiclePosition: PtvVehiclePosition?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue