feat: vehicle positions and state dismissal

This commit is contained in:
LavaDesu 2025-07-29 20:35:32 +10:00
parent c526269e5d
commit e52274a6ef
Signed by: cilly
GPG key ID: 6500251E087653C9
10 changed files with 163 additions and 42 deletions

View file

@ -18,7 +18,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 kotlinx.coroutines.flow.Flow
import moe.lava.banksia.R
import moe.lava.banksia.api.ptv.structures.ComposableRouteIcon
import moe.lava.banksia.native.BanksiaTheme
import moe.lava.banksia.util.BoxedValue
import com.google.android.gms.maps.model.CameraPosition as GoogleCameraPosition
@ -70,7 +70,6 @@ actual fun Maps(
setLastKnownLocation: (Point) -> Unit,
extInsets: WindowInsets,
) {
val scope = rememberCoroutineScope()
val camPos = rememberCameraPositionState()
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
LaunchedEffect(newCameraPos) {
@ -120,17 +119,26 @@ actual fun Maps(
val state = rememberMarkerState()
state.position = marker.point.toLatLng()
MarkerComposable(
keys = arrayOf(marker.colour),
keys = arrayOf(marker.data),
zIndex = if (marker.data is Marker.Data.Vehicle) 1f else 0f,
state = state,
onClick = { marker.onClick() }
) {
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(BanksiaTheme.colors.surface)
.border(2.dp, marker.colour, CircleShape)
)
when (marker.data) {
is Marker.Data.Stop ->
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(BanksiaTheme.colors.surface)
.border(2.dp, marker.data.colour, CircleShape)
)
is Marker.Data.Vehicle ->
ComposableRouteIcon(
size = 30.dp,
routeType = marker.data.type,
)
}
}
}
for (polyline in polylines) {

View file

@ -47,6 +47,7 @@ import moe.lava.banksia.native.maps.Point
import moe.lava.banksia.native.maps.getScreenHeight
import moe.lava.banksia.resources.Res
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.InfoPanel
import moe.lava.banksia.ui.Searcher
@ -139,7 +140,7 @@ fun App(
extInsets = WindowInsets(top = with(LocalDensity.current) {
SearchBarDefaults.InputFieldHeight.roundToPx()
}, bottom = extInsets),
markers = mapState.markers,
markers = mapState.stops + mapState.vehicles,
setLastKnownLocation = viewModel::setLastKnownLocation,
polylines = mapState.polylines,
)
@ -168,6 +169,7 @@ fun App(
scope.launch {
scaffoldState.bottomSheetState.hide()
peekHeightMultiplier = 1F
viewModel.handleEvent(BanksiaEvent.DismissState)
}
} catch (_: CancellationException) {
peekHeightMultiplier = 1F

View file

@ -1,10 +1,15 @@
package moe.lava.banksia.api.ptv.structures
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.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import moe.lava.banksia.resources.Res
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 org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
data class RouteTypeProperties(
val colour: Color,
@ -43,18 +49,37 @@ fun PtvRouteType.getProperties(): RouteTypeProperties {
return RouteTypeProperties(colour, drawable, background, icon)
}
const val ICON_PADDING = 0.25f
@Preview
@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()
Image(
painter = painterResource(properties.icon),
contentDescription = null,
modifier = Modifier
modifier = modifier
.size(size)
.aspectRatio(1f)
.padding(size * ICON_PADDING / 2)
.drawBehind {
drawCircle(properties.colour, radius = (this.size.minDimension + 10.dp.toPx()) / 2f)
drawCircle(properties.colour, radius = size.toPx() / 2f)
}
)
}
@Composable
inline fun PtvRouteType.ComposableIcon() = ComposableRouteIcon(this)
inline fun PtvRouteType.ComposableIcon(modifier: Modifier = Modifier) = ComposableRouteIcon(modifier, routeType = this)

View file

@ -6,17 +6,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.flow.Flow
import moe.lava.banksia.api.ptv.structures.PtvRouteType
import moe.lava.banksia.util.BoxedValue
enum class MarkerType {
GENERIC_STOP,
}
data class Marker(
val point: Point,
val type: MarkerType,
val colour: Color,
val onClick: () -> Boolean
)
val data: Data,
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 Polyline(val points: List<Point>, val colour: Color)

View file

@ -22,7 +22,6 @@ import moe.lava.banksia.log
import moe.lava.banksia.native.maps.CameraPosition
import moe.lava.banksia.native.maps.CameraPositionBounds
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.Polyline
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
sealed class BanksiaEvent {
data object DismissState : BanksiaEvent()
data class SelectRoute(val id: Int?) : BanksiaEvent()
data class SelectStop(val routeType: PtvRouteType, val stopId: Int?) : BanksiaEvent()
@ -61,6 +62,7 @@ class BanksiaViewModel : ViewModel() {
fun handleEvent(event: BanksiaEvent) {
viewModelScope.launch {
when (event) {
is BanksiaEvent.DismissState -> dismissState()
is BanksiaEvent.SelectRoute -> switchRoute(event.id)
is BanksiaEvent.SelectStop -> switchStop(event.routeType, event.stopId)
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
@ -87,6 +89,13 @@ class BanksiaViewModel : ViewModel() {
lastKnownLocation = location
}
private fun dismissState() {
viewModelScope.launch {
switchRoute(null)
searchUpdate("")
}
}
private suspend fun searchUpdate(text: String) {
val entries = ptvService.routes()
.sortedWith(
@ -126,6 +135,7 @@ class BanksiaViewModel : ViewModel() {
}
viewModelScope.launch { buildPolylines(route) }
viewModelScope.launch { buildRuns(route) }
viewModelScope.launch { buildStops(route) }
// viewModelScope.launch { buildDepartures() }
// viewModelScope.launch { buildRuns() }
@ -219,6 +229,22 @@ class BanksiaViewModel : ViewModel() {
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) {
// val directions = ptvService.directionsByRoute(route.routeId)
//
@ -239,27 +265,22 @@ class BanksiaViewModel : ViewModel() {
private suspend fun buildStops(route: PtvRoute) {
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
val markers = mutableListOf<Marker>()
val colour = route.routeType.getProperties().colour
for (stop in stops) {
if (stop.stopLatitude != null && stop.stopLongitude != null) {
val pos = Point(stop.stopLatitude!!, stop.stopLongitude!!)
val marker = Marker(
point = pos,
type = MarkerType.GENERIC_STOP,
colour = colour,
val markers = stops
.filter { it.stopLatitude != null && it.stopLongitude != null }
.map { stop ->
Marker(
point = Point(stop.stopLatitude!!, stop.stopLongitude!!),
data = Marker.Data.Stop(colour),
onClick = {
viewModelScope.launch { switchStop(route.routeType, stop.stopId) }
false
}
)
markers.add(marker)
}
}
iMapState.update { it.copy(markers = markers) }
iMapState.update { it.copy(stops = markers) }
}
private fun buildBounds(points: List<Point>): CameraPositionBounds {

View file

@ -70,13 +70,13 @@ fun InfoPanel(
}
@Composable
private fun RouteInfoPanel(
private inline fun RouteInfoPanel(
state: InfoPanelState.Route,
onEvent: (BanksiaEvent) -> Unit,
) {
Column(Modifier.fillMaxWidth()) {
Row {
ComposableRouteIcon(state.type)
ComposableRouteIcon(routeType = state.type)
Text(
state.name,
style = MaterialTheme.typography.titleLarge,
@ -88,7 +88,7 @@ private fun RouteInfoPanel(
}
@Composable
private fun StopInfoPanel(
private inline fun StopInfoPanel(
state: InfoPanelState.Stop,
onEvent: (BanksiaEvent) -> Unit,
) {

View file

@ -78,7 +78,7 @@ fun Searcher(
ListItem(
headlineContent = { Text(entry.mainText) },
supportingContent = { entry.subText?.let { Text(it) } },
leadingContent = { ComposableRouteIcon(entry.routeType) },
leadingContent = { ComposableRouteIcon(routeType = entry.routeType) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier
.fillMaxWidth()

View file

@ -4,6 +4,7 @@ import moe.lava.banksia.native.maps.Marker
import moe.lava.banksia.native.maps.Polyline
data class MapState(
val markers: List<Marker> = listOf(),
val stops: List<Marker> = listOf(),
val vehicles: List<Marker> = listOf(),
val polylines: List<Polyline> = listOf(),
)