feat: preliminary run info panel, and heavy refactoring
This commit is contained in:
parent
e52274a6ef
commit
ce8425d6a7
9 changed files with 226 additions and 97 deletions
|
|
@ -43,6 +43,8 @@ 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.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import com.google.android.gms.maps.model.CameraPosition as GoogleCameraPosition
|
||||
|
||||
|
|
@ -64,8 +66,8 @@ actual fun getScreenHeight(): Int {
|
|||
@Composable
|
||||
actual fun Maps(
|
||||
modifier: Modifier,
|
||||
markers: List<Marker>,
|
||||
polylines: List<Polyline>,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
@ -115,33 +117,46 @@ actual fun Maps(
|
|||
contentPadding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues()
|
||||
) {
|
||||
// [TODO]: Slight lag when routes with many stops such as the 901 bus is set
|
||||
for (marker in markers) {
|
||||
for (marker in state.stops) {
|
||||
val state = rememberMarkerState()
|
||||
state.position = marker.point.toLatLng()
|
||||
MarkerComposable(
|
||||
keys = arrayOf(marker.data),
|
||||
zIndex = if (marker.data is Marker.Data.Vehicle) 1f else 0f,
|
||||
keys = arrayOf(marker),
|
||||
zIndex = 0f,
|
||||
state = state,
|
||||
onClick = { marker.onClick() }
|
||||
) {
|
||||
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,
|
||||
)
|
||||
onClick = {
|
||||
onEvent(BanksiaEvent.SelectStop(marker.type to marker.id))
|
||||
false
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(BanksiaTheme.colors.surface)
|
||||
.border(2.dp, marker.colour, CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
for (polyline in polylines) {
|
||||
for (marker in state.vehicles) {
|
||||
val state = rememberMarkerState()
|
||||
state.position = marker.point.toLatLng()
|
||||
MarkerComposable(
|
||||
keys = arrayOf(marker),
|
||||
zIndex = 1f,
|
||||
state = state,
|
||||
onClick = {
|
||||
onEvent(BanksiaEvent.SelectRun(marker.ref))
|
||||
false
|
||||
}
|
||||
) {
|
||||
ComposableRouteIcon(
|
||||
size = 30.dp,
|
||||
routeType = marker.type,
|
||||
)
|
||||
}
|
||||
}
|
||||
for (polyline in state.polylines) {
|
||||
Polyline(
|
||||
points = polyline.points.map { it.toLatLng() },
|
||||
color = polyline.colour
|
||||
|
|
|
|||
|
|
@ -136,13 +136,13 @@ fun App(
|
|||
) {
|
||||
Maps(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = mapState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||
}, bottom = extInsets),
|
||||
markers = mapState.stops + mapState.vehicles,
|
||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
polylines = mapState.polylines,
|
||||
)
|
||||
Searcher(
|
||||
state = searchState,
|
||||
|
|
|
|||
|
|
@ -7,17 +7,25 @@ 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.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
|
||||
data class Marker(
|
||||
val point: Point,
|
||||
val data: Data,
|
||||
val onClick: () -> Boolean,
|
||||
) {
|
||||
sealed class Data {
|
||||
data class Stop(val colour: Color) : Data()
|
||||
data class Vehicle(val type: PtvRouteType) : Data()
|
||||
}
|
||||
sealed class Marker {
|
||||
abstract val point: Point
|
||||
|
||||
data class Stop(
|
||||
override val point: Point,
|
||||
val id: Int,
|
||||
val type: PtvRouteType,
|
||||
val colour: Color,
|
||||
) : Marker()
|
||||
|
||||
data class Vehicle(
|
||||
override val point: Point,
|
||||
val ref: String,
|
||||
val type: PtvRouteType,
|
||||
) : Marker()
|
||||
}
|
||||
data class Point(val lat: Double, val lng: Double)
|
||||
data class Polyline(val points: List<Point>, val colour: Color)
|
||||
|
|
@ -35,8 +43,8 @@ expect fun getScreenHeight(): Int
|
|||
@Composable
|
||||
expect fun Maps(
|
||||
modifier: Modifier = Modifier,
|
||||
markers: List<Marker> = listOf(),
|
||||
polylines: List<Polyline> = listOf(),
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.updateAndGet
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
|
@ -34,12 +36,31 @@ sealed class BanksiaEvent {
|
|||
data object DismissState : BanksiaEvent()
|
||||
|
||||
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
||||
data class SelectStop(val routeType: PtvRouteType, val stopId: Int?) : BanksiaEvent()
|
||||
data class SelectRun(val ref: String?) : BanksiaEvent()
|
||||
data class SelectStop(val typeAndId: Pair<PtvRouteType, Int>) : BanksiaEvent()
|
||||
|
||||
data class SearchUpdate(val text: String) : BanksiaEvent()
|
||||
}
|
||||
|
||||
data class InternalState(
|
||||
val route: Int? = null,
|
||||
val stop: Pair<PtvRouteType, Int>? = null,
|
||||
val run: String? = null,
|
||||
)
|
||||
|
||||
class BanksiaViewModel : ViewModel() {
|
||||
private var state = InternalState()
|
||||
set(value) {
|
||||
val last = field
|
||||
field = value
|
||||
if (value.route != last.route)
|
||||
viewModelScope.launch { switchRoute(value.route) }
|
||||
if (value.stop != last.stop)
|
||||
viewModelScope.launch { switchStop(value.stop) }
|
||||
if (value.run != last.run)
|
||||
switchRun(value.run)
|
||||
}
|
||||
|
||||
private val iInfoState = MutableStateFlow<InfoPanelState>(InfoPanelState.None)
|
||||
val infoState = iInfoState.asStateFlow()
|
||||
|
||||
|
|
@ -64,7 +85,8 @@ class BanksiaViewModel : ViewModel() {
|
|||
when (event) {
|
||||
is BanksiaEvent.DismissState -> dismissState()
|
||||
is BanksiaEvent.SelectRoute -> switchRoute(event.id)
|
||||
is BanksiaEvent.SelectStop -> switchStop(event.routeType, event.stopId)
|
||||
is BanksiaEvent.SelectRun -> switchRun(event.ref)
|
||||
is BanksiaEvent.SelectStop -> switchStop(event.typeAndId)
|
||||
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
}
|
||||
}
|
||||
|
|
@ -135,18 +157,49 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
viewModelScope.launch { buildPolylines(route) }
|
||||
viewModelScope.launch { buildRuns(route) }
|
||||
viewModelScope.launch { buildStops(route) }
|
||||
// viewModelScope.launch { buildDepartures() }
|
||||
// viewModelScope.launch { buildRuns() }
|
||||
buildRuns(route)
|
||||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
private suspend fun switchStop(routeType: PtvRouteType, stopId: Int?) {
|
||||
if (stopId == null) {
|
||||
private fun switchRun(ref: String?) {
|
||||
if (ref == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
|
||||
var lastState = iInfoState.value
|
||||
var routeName: String? = null
|
||||
ptvService.runFlow(ref, firstWithCache = true)
|
||||
.takeWhile { lastState == iInfoState.value }
|
||||
.onEach { run ->
|
||||
if (routeName == null) {
|
||||
lastState = iInfoState.updateAndGet {
|
||||
InfoPanelState.Run(
|
||||
direction = run.destinationName,
|
||||
type = run.routeType,
|
||||
)
|
||||
}
|
||||
routeName = ptvService.route(run.routeId).routeName
|
||||
}
|
||||
|
||||
lastState = iInfoState.updateAndGet {
|
||||
InfoPanelState.Run(
|
||||
direction = run.destinationName,
|
||||
type = run.routeType,
|
||||
routeName = routeName,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
private suspend fun switchStop(typeAndId: Pair<PtvRouteType, Int>?) {
|
||||
if (typeAndId == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
val (routeType, stopId) = typeAndId
|
||||
val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.stopName.split("/")
|
||||
val name = split[0]
|
||||
|
|
@ -229,40 +282,31 @@ class BanksiaViewModel : ViewModel() {
|
|||
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||
}
|
||||
|
||||
private suspend fun buildRuns(route: PtvRoute) {
|
||||
val runs = ptvService.runs(route.routeId)
|
||||
var runsRouteKey: Int? = null
|
||||
private fun buildRuns(route: PtvRoute) {
|
||||
runsRouteKey = route.routeId
|
||||
ptvService
|
||||
.runsFlow(route.routeId)
|
||||
.takeWhile { route.routeId == runsRouteKey }
|
||||
.onEach { runs ->
|
||||
val markers = runs
|
||||
.filter { it.vehiclePosition != null }
|
||||
.map { it to it.vehiclePosition!! }
|
||||
.distinctBy { (_, pos) -> pos.latitude to pos.longitude }
|
||||
.map { (run, pos) ->
|
||||
Marker.Vehicle(
|
||||
Point(pos.latitude, pos.longitude),
|
||||
ref = run.runRef,
|
||||
type = route.routeType,
|
||||
)
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
iMapState.update { it.copy(vehicles = markers) }
|
||||
}
|
||||
|
||||
// private suspend fun buildDepartures(route: PtvRoute) {
|
||||
// val directions = ptvService.directionsByRoute(route.routeId)
|
||||
//
|
||||
// iState.update {
|
||||
// it.copy(routeState = it.routeState?.copy(directions = directions))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private suspend fun buildRuns() {
|
||||
// val route = iState.value.routeState?.route ?: return
|
||||
//
|
||||
// val directions = ptvService.directionsByRoute(route.routeId)
|
||||
//
|
||||
// iState.update {
|
||||
// it.copy(routeState = it.routeState?.copy(directions = directions))
|
||||
// }
|
||||
// }
|
||||
|
||||
private suspend fun buildStops(route: PtvRoute) {
|
||||
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
||||
val colour = route.routeType.getProperties().colour
|
||||
|
|
@ -270,13 +314,11 @@ class BanksiaViewModel : ViewModel() {
|
|||
val markers = stops
|
||||
.filter { it.stopLatitude != null && it.stopLongitude != null }
|
||||
.map { stop ->
|
||||
Marker(
|
||||
Marker.Stop(
|
||||
point = Point(stop.stopLatitude!!, stop.stopLongitude!!),
|
||||
data = Marker.Data.Stop(colour),
|
||||
onClick = {
|
||||
viewModelScope.launch { switchStop(route.routeType, stop.stopId) }
|
||||
false
|
||||
}
|
||||
id = stop.stopId,
|
||||
colour = colour,
|
||||
type = route.routeType,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ fun InfoPanel(
|
|||
when (state) {
|
||||
is InfoPanelState.Route -> RouteInfoPanel(state, onEvent)
|
||||
is InfoPanelState.Stop -> StopInfoPanel(state, onEvent)
|
||||
else -> throw UnsupportedOperationException()
|
||||
is InfoPanelState.Run -> RunInfoPanel(state, onEvent)
|
||||
is InfoPanelState.None -> throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
if (state.loading)
|
||||
|
|
@ -87,6 +88,24 @@ private inline fun RouteInfoPanel(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun RunInfoPanel(
|
||||
state: InfoPanelState.Run,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row {
|
||||
ComposableRouteIcon(routeType = state.type)
|
||||
Text(
|
||||
"${state.direction} via ${state.routeName ?: "..."}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun StopInfoPanel(
|
||||
state: InfoPanelState.Stop,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ sealed class InfoPanelState {
|
|||
override val loading = false
|
||||
}
|
||||
|
||||
data class Run(
|
||||
val direction: String,
|
||||
val type: PtvRouteType,
|
||||
val routeName: String? = null,
|
||||
) : InfoPanelState() {
|
||||
override val loading = routeName == null
|
||||
}
|
||||
|
||||
data class Stop(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
|
|
@ -27,4 +35,4 @@ sealed class InfoPanelState {
|
|||
|
||||
data class Departure(val directionName: String, val formattedTimes: String)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import moe.lava.banksia.native.maps.Marker
|
|||
import moe.lava.banksia.native.maps.Polyline
|
||||
|
||||
data class MapState(
|
||||
val stops: List<Marker> = listOf(),
|
||||
val vehicles: List<Marker> = listOf(),
|
||||
val stops: List<Marker.Stop> = listOf(),
|
||||
val vehicles: List<Marker.Vehicle> = listOf(),
|
||||
val polylines: List<Polyline> = listOf(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
|
|
@ -19,8 +21,8 @@ actual fun getScreenHeight(): Int {
|
|||
@Composable
|
||||
actual fun Maps(
|
||||
modifier: Modifier,
|
||||
markers: List<Marker>,
|
||||
polylines: List<Polyline>,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue