feat: basic departures support
also a huge refactor to simplify modules
This commit is contained in:
parent
b31067992d
commit
8b3016004b
44 changed files with 627 additions and 211 deletions
|
|
@ -41,7 +41,9 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
implementation(libs.play.services.location)
|
||||
implementation(projects.ui.shared)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.compose.components.resources)
|
||||
|
|
@ -68,7 +70,8 @@ kotlin {
|
|||
implementation(libs.ui.backhandler)
|
||||
|
||||
implementation(projects.core)
|
||||
implementation(projects.core.data.client)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.stoptime)
|
||||
implementation(projects.ui.maps)
|
||||
implementation(projects.ui.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ kotlin {
|
|||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
|
||||
androidResources {
|
||||
enable = true
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
|
|
@ -47,4 +51,5 @@ dependencies {
|
|||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "moe.lava.banksia.resources"
|
||||
generateResClass = always
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M480,600L280,400L680,400L480,600Z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M280,560L480,360L680,560L280,560Z"/>
|
||||
</vector>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package moe.lava.banksia.ui.di
|
||||
|
||||
import moe.lava.banksia.core.data.clientDataDiModule
|
||||
import moe.lava.banksia.core.data.dataDiModule
|
||||
import moe.lava.banksia.ui.screens.map.MapScreenViewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val AppModule = module {
|
||||
includes(clientDataDiModule)
|
||||
includes(dataDiModule)
|
||||
|
||||
// ViewModel
|
||||
viewModelOf(::MapScreenViewModel)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContent
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -27,7 +28,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtMost
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
|
@ -45,6 +45,7 @@ sealed class InfoPanelState {
|
|||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun InfoPanel(
|
||||
modifier: Modifier = Modifier,
|
||||
state: InfoPanelState,
|
||||
onEvent: (InfoPanelEvent) -> Unit,
|
||||
onPeekHeightChange: (Dp) -> Unit,
|
||||
|
|
@ -65,11 +66,13 @@ fun InfoPanel(
|
|||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.heightIn(min = 350.dp)
|
||||
.onSizeChanged {
|
||||
onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
|
||||
// onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) })
|
||||
onPeekHeightChange(350.dp)
|
||||
}
|
||||
) {
|
||||
Box {
|
||||
|
|
|
|||
|
|
@ -1,75 +1,358 @@
|
|||
package moe.lava.banksia.ui.layout.info
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedListItem
|
||||
import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.arrow_drop_down
|
||||
import moe.lava.banksia.resources.arrow_drop_up
|
||||
import moe.lava.banksia.ui.extensions.BUS_ORANGE
|
||||
import moe.lava.banksia.ui.extensions.TRAIN_BLUE
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Instant
|
||||
|
||||
sealed class StopInfoPanelEvent : InfoPanelEvent()
|
||||
sealed class StopInfoPanelEvent : InfoPanelEvent() {
|
||||
data object ToggleGrouping : StopInfoPanelEvent()
|
||||
}
|
||||
|
||||
data class StopInfoPanelState(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val subname: String? = null,
|
||||
val departures: List<Departure>? = null,
|
||||
val departures: List<DeparturePlatforms>? = null,
|
||||
) : InfoPanelState() {
|
||||
override val loading: Boolean
|
||||
get() = departures == null
|
||||
get() = departures.isNullOrEmpty()
|
||||
|
||||
data class Departure(val directionName: String, val formattedTimes: String)
|
||||
data class DeparturePlatforms(
|
||||
val platform: String,
|
||||
val departures: List<DepartureInfo>,
|
||||
)
|
||||
|
||||
data class DepartureInfo(
|
||||
val routeName: String,
|
||||
val routeColour: Color?,
|
||||
val headsign: String,
|
||||
val description: String?,
|
||||
val time: Instant,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StopInfoPanel(
|
||||
state: StopInfoPanelState,
|
||||
onEvent: (StopInfoPanelEvent) -> Unit,
|
||||
private fun listColors() = ListItemDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
selectedContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
private fun MonoPlatform(
|
||||
state: StopInfoPanelState.DeparturePlatforms
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
state.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
state.subname?.let {
|
||||
Text(
|
||||
"/ $it",
|
||||
modifier = Modifier.padding(start = 5.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = Color.Gray,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
state.departures?.let {
|
||||
Spacer(Modifier.height(5.dp))
|
||||
it.forEach { (name, formatted) ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val departures = state.departures
|
||||
val lazyState = LazyListState(firstVisibleItemIndex =
|
||||
departures.indexOfFirst {
|
||||
it.time > Clock.System.now()
|
||||
}.coerceAtLeast(0)
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||
state = lazyState,
|
||||
) {
|
||||
itemsIndexed(departures) { idx, dep ->
|
||||
SegmentedListItem(
|
||||
onClick = {},
|
||||
colors = listColors(),
|
||||
shapes = ListItemDefaults.segmentedShapes(
|
||||
idx,
|
||||
departures.size,
|
||||
),
|
||||
supportingContent = {
|
||||
dep.description?.let { Text(dep.description) }
|
||||
},
|
||||
trailingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy((-4).dp)
|
||||
) {
|
||||
Text(
|
||||
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
|
||||
style = MaterialTheme.typography.headlineSmallEmphasized,
|
||||
)
|
||||
Text(
|
||||
text = "mn",
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(ShapeDefaults.ExtraSmall)
|
||||
.background(dep.routeColour ?: MaterialTheme.colorScheme.surface)
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = dep.routeName,
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
formatted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 5.dp)
|
||||
text = dep.headsign,
|
||||
style = MaterialTheme.typography.labelLargeEmphasized,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
private fun ManyPlatforms(
|
||||
state: List<StopInfoPanelState.DeparturePlatforms>,
|
||||
) {
|
||||
val expandedList = remember { mutableStateListOf(*Array(state.size) { true }) }
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
state.forEachIndexed { idx, depInfo ->
|
||||
val (platform, departures) = depInfo
|
||||
val expanded = expandedList[idx]
|
||||
stickyHeader(key = "header_${depInfo.hashCode()}") {
|
||||
val base = ListItemDefaults.segmentedShapes(0, 2)
|
||||
val large = MaterialTheme.shapes.large
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.animateItem()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.padding(bottom = ListItemDefaults.SegmentedGap)
|
||||
) {
|
||||
SegmentedListItem(
|
||||
onClick = { expandedList[idx] = !expandedList[idx] },
|
||||
colors = listColors(),
|
||||
shapes = if (expanded) base else base.copy(shape = large),
|
||||
trailingContent = {
|
||||
Icon(
|
||||
painterResource(if (expanded) Res.drawable.arrow_drop_up else Res.drawable.arrow_drop_down),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (expanded) MaterialTheme.colorScheme.surface else Color.Transparent,
|
||||
shape = RoundedCornerShape(100)
|
||||
)
|
||||
.padding(6.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = platform,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
item(key = "items_${depInfo.hashCode()}") {
|
||||
Column(
|
||||
modifier = Modifier.animateItem(),
|
||||
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||
) {
|
||||
departures.filter { it.time > Clock.System.now() }.take(5)
|
||||
.forEachIndexed { idx, dep ->
|
||||
SegmentedListItem(
|
||||
onClick = {},
|
||||
colors = listColors(),
|
||||
shapes = ListItemDefaults.segmentedShapes(
|
||||
idx + 1,
|
||||
(departures.size + 1).coerceAtMost(6),
|
||||
),
|
||||
supportingContent = {
|
||||
dep.description?.let { Text(dep.description) }
|
||||
},
|
||||
trailingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy((-4).dp)
|
||||
) {
|
||||
Text(
|
||||
text = (dep.time - Clock.System.now()).inWholeMinutes.toString(),
|
||||
style = MaterialTheme.typography.headlineSmallEmphasized,
|
||||
)
|
||||
Text(
|
||||
text = "mn",
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(ShapeDefaults.ExtraSmall)
|
||||
.background(
|
||||
dep.routeColour
|
||||
?: MaterialTheme.colorScheme.surface
|
||||
)
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = dep.routeName,
|
||||
style = MaterialTheme.typography.labelSmallEmphasized,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = dep.headsign,
|
||||
style = MaterialTheme.typography.labelLargeEmphasized,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item(key = "spacer_${depInfo.hashCode()}") {
|
||||
Spacer(
|
||||
modifier = Modifier.animateItem().height(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
internal fun StopInfoPanel(
|
||||
state: StopInfoPanelState,
|
||||
onEvent: (StopInfoPanelEvent) -> Unit,
|
||||
) {
|
||||
val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300))
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
contentKey = { it.id },
|
||||
transitionSpec = { spec },
|
||||
) { state ->
|
||||
Column(Modifier.fillMaxWidth().fillMaxHeight()) {
|
||||
Row {
|
||||
Column {
|
||||
Text(
|
||||
state.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
state.subname?.let {
|
||||
Text(
|
||||
"/ $it",
|
||||
modifier = Modifier.padding(start = 5.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = Color.Gray,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onEvent(StopInfoPanelEvent.ToggleGrouping) },
|
||||
) { Icon(Icons.Default.Edit, null) }
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
AnimatedContent(
|
||||
targetState = state.departures,
|
||||
transitionSpec = { spec },
|
||||
) { departures ->
|
||||
departures?.let { departurePlatforms ->
|
||||
if (departurePlatforms.size > 1) {
|
||||
ManyPlatforms(departurePlatforms)
|
||||
} else if (departurePlatforms.size == 1) {
|
||||
MonoPlatform(departurePlatforms[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun StopInfoPanelPreview() {
|
||||
fun dateIn(dur: Duration) = (Clock.System.now() + dur)
|
||||
|
||||
InfoPanel(
|
||||
modifier = Modifier.background(BanksiaTheme.colors.background),
|
||||
state = StopInfoPanelState(
|
||||
id = "id",
|
||||
name = "name",
|
||||
subname = "sub",
|
||||
departures = listOf(
|
||||
StopInfoPanelState.DeparturePlatforms("Platform 1", listOf(
|
||||
StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "Sunbury", "··· Malvern -> Anzac ··· Sunbury", dateIn(2.minutes)),
|
||||
StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "West Footscray", "Express via Metro Tunnel", dateIn(8.minutes)),
|
||||
)),
|
||||
StopInfoPanelState.DeparturePlatforms("Platform 2", listOf(
|
||||
StopInfoPanelState.DepartureInfo("237", Color(BUS_ORANGE), "Westall", null, dateIn(7.minutes)),
|
||||
StopInfoPanelState.DepartureInfo("442", Color(BUS_ORANGE), "Dandenong", null, dateIn(8.minutes)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onEvent = {},
|
||||
onPeekHeightChange = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import moe.lava.banksia.core.data.dto.ExtendedStopTime
|
||||
import moe.lava.banksia.core.data.repositories.RouteRepository
|
||||
import moe.lava.banksia.core.data.repositories.StopRepository
|
||||
import moe.lava.banksia.core.data.repositories.StopTimeRepository
|
||||
|
|
@ -26,9 +27,11 @@ import moe.lava.banksia.core.util.LoopFlow.Companion.waitUntilSubscribed
|
|||
import moe.lava.banksia.core.util.Point
|
||||
import moe.lava.banksia.core.util.log
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.ui.extensions.getUIProperties
|
||||
import moe.lava.banksia.ui.layout.info.InfoPanelEvent
|
||||
import moe.lava.banksia.ui.layout.info.InfoPanelState
|
||||
import moe.lava.banksia.ui.layout.info.RouteInfoPanelState
|
||||
import moe.lava.banksia.ui.layout.info.StopInfoPanelEvent
|
||||
import moe.lava.banksia.ui.layout.info.StopInfoPanelState
|
||||
import moe.lava.banksia.ui.layout.info.TripInfoPanelState
|
||||
import moe.lava.banksia.ui.map.util.CameraPosition
|
||||
|
|
@ -36,8 +39,6 @@ import moe.lava.banksia.ui.map.util.CameraPositionBounds
|
|||
import moe.lava.banksia.ui.map.util.Marker
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
sealed class MapScreenEvent {
|
||||
data object DismissState : MapScreenEvent()
|
||||
|
|
@ -53,6 +54,9 @@ private data class InternalState(
|
|||
val route: String? = null,
|
||||
val stop: String? = null,
|
||||
val run: String? = null,
|
||||
|
||||
val lastStopDepartures: List<ExtendedStopTime>? = null,
|
||||
val stopsGrouped: Boolean = true,
|
||||
)
|
||||
|
||||
class MapScreenViewModel(
|
||||
|
|
@ -69,6 +73,10 @@ class MapScreenViewModel(
|
|||
viewModelScope.launch { switchRoute(value.route) }
|
||||
if (value.stop != last.stop)
|
||||
viewModelScope.launch { switchStop(value.stop) }
|
||||
if (value.lastStopDepartures != last.lastStopDepartures)
|
||||
viewModelScope.launch { buildDepartures() }
|
||||
if (value.stopsGrouped != last.stopsGrouped)
|
||||
viewModelScope.launch { buildDepartures() }
|
||||
if (value.run != last.run)
|
||||
switchRun(value.run)
|
||||
}
|
||||
|
|
@ -105,7 +113,9 @@ class MapScreenViewModel(
|
|||
|
||||
fun handleEvent(event: InfoPanelEvent) {
|
||||
viewModelScope.launch {
|
||||
// when (event) { }
|
||||
when (event) {
|
||||
StopInfoPanelEvent.ToggleGrouping -> state = state.copy(stopsGrouped = !state.stopsGrouped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +175,7 @@ class MapScreenViewModel(
|
|||
}
|
||||
|
||||
val route = routeRepository.get(routeId)
|
||||
// val gtfsRoute = ptvService.route(routeId)
|
||||
?: return
|
||||
iInfoState.update {
|
||||
RouteInfoPanelState(
|
||||
name = route.name,
|
||||
|
|
@ -215,11 +225,11 @@ class MapScreenViewModel(
|
|||
private suspend fun switchStop(id: String?) {
|
||||
if (id == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
state = state.copy(lastStopDepartures = null)
|
||||
return
|
||||
}
|
||||
|
||||
val stop = stopRepository.get(id)
|
||||
// val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.name.split("/")
|
||||
val name = split[0]
|
||||
val subname = split.getOrNull(1)
|
||||
|
|
@ -232,37 +242,63 @@ class MapScreenViewModel(
|
|||
}
|
||||
|
||||
stopTimeRepository.getForStop(id)
|
||||
.onEach { stoptimes ->
|
||||
val departures = stoptimes
|
||||
// .filter { !it.headsign.isNullOrBlank() }
|
||||
// .groupBy { it.headsign!! }
|
||||
.groupBy { it.stopId } // TODO: Placeholder
|
||||
.map { (headsign, stopTimes) ->
|
||||
val now = Clock.System.now()
|
||||
val times = stopTimes
|
||||
.map { it.time.arrival.toInstant(TimeZone.currentSystemDefault()) }
|
||||
.filter { it >= (now - 1.minutes) }
|
||||
.joinToString(" | ") {
|
||||
val diff = (it - now).inWholeMinutes.coerceAtLeast(0)
|
||||
if (diff >= 65) {
|
||||
"${((diff + 30.0) / 60.0).toInt()}hr"
|
||||
} else {
|
||||
"${diff}mn"
|
||||
}
|
||||
}
|
||||
StopInfoPanelState.Departure(headsign, times)
|
||||
}
|
||||
|
||||
iInfoState.update {
|
||||
if (it !is StopInfoPanelState)
|
||||
it
|
||||
else
|
||||
it.copy(departures = departures)
|
||||
}
|
||||
.onEach { departures ->
|
||||
state = state.copy(
|
||||
lastStopDepartures = departures
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun friendlyPlatform(platform: String) =
|
||||
platform.takeUnless { it.firstOrNull()?.isDigit() == true }
|
||||
?: "Platform $platform"
|
||||
private fun buildDepartures() {
|
||||
val rawDepartures = state.lastStopDepartures ?: return
|
||||
val departures = if (state.stopsGrouped) {
|
||||
rawDepartures
|
||||
.groupBy { it.stopPlatformCode }
|
||||
.mapKeys { (platform) -> platform?.let { friendlyPlatform(it) } }
|
||||
.entries
|
||||
.sortedBy { (platform) -> platform }
|
||||
.map { (platform, deps) ->
|
||||
StopInfoPanelState.DeparturePlatforms(
|
||||
platform = platform ?: "",
|
||||
departures = deps.map {
|
||||
StopInfoPanelState.DepartureInfo(
|
||||
routeName = it.routeNumber ?: it.routeName,
|
||||
routeColour = it.routeType.getUIProperties().colour,
|
||||
headsign = it.headsign ?: it.routeName,
|
||||
description = null,
|
||||
time = it.time.departure.toInstant(TimeZone.currentSystemDefault()),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else if (rawDepartures.isEmpty()) {
|
||||
listOf()
|
||||
} else {
|
||||
listOf(StopInfoPanelState.DeparturePlatforms(platform = "", departures = rawDepartures.map { dep ->
|
||||
StopInfoPanelState.DepartureInfo(
|
||||
routeName = dep.routeNumber ?: dep.routeName,
|
||||
routeColour = dep.routeType.getUIProperties().colour,
|
||||
headsign = dep.headsign ?: dep.routeName,
|
||||
description = dep.stopPlatformCode?.let { friendlyPlatform(it) },
|
||||
time = dep.time.departure.toInstant(TimeZone.currentSystemDefault()),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
departures.let { departures ->
|
||||
iInfoState.update {
|
||||
if (it !is StopInfoPanelState)
|
||||
it
|
||||
else
|
||||
it.copy(departures = departures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*private suspend fun buildPolylines(route: PtvRoute) {
|
||||
val routeWithGeo = if (route.geopath.isEmpty())
|
||||
ptvService.route(route.routeId, true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue