feat: server-handled routes and stops
This commit is contained in:
parent
efba64ea90
commit
58ee095522
61 changed files with 1634 additions and 349 deletions
|
|
@ -41,6 +41,7 @@ kotlin {
|
|||
implementation(libs.play.services.location)
|
||||
implementation(libs.play.services.maps)
|
||||
implementation(libs.maps.compose)
|
||||
implementation(libs.maps.compose.utils)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(compose.runtime)
|
||||
|
|
@ -53,8 +54,13 @@ kotlin {
|
|||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.contentnegotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.moko.geo)
|
||||
implementation(libs.moko.geo.compose)
|
||||
implementation(projects.shared)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ import com.google.maps.android.compose.rememberCameraPositionState
|
|||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import moe.lava.banksia.R
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.Point
|
||||
|
|
@ -67,7 +67,7 @@ actual fun getScreenHeight(): Int {
|
|||
actual fun Maps(
|
||||
modifier: Modifier,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
@ -135,7 +135,7 @@ actual fun Maps(
|
|||
zIndex = 0f,
|
||||
state = state,
|
||||
onClick = {
|
||||
onEvent(BanksiaEvent.SelectStop(marker.type to marker.id))
|
||||
onEvent(MapScreenEvent.SelectStop(marker.type to marker.id))
|
||||
false
|
||||
}
|
||||
) {
|
||||
|
|
@ -155,7 +155,7 @@ actual fun Maps(
|
|||
zIndex = 1f,
|
||||
state = state,
|
||||
onClick = {
|
||||
onEvent(BanksiaEvent.SelectRun(marker.ref))
|
||||
onEvent(MapScreenEvent.SelectRun(marker.ref))
|
||||
false
|
||||
}
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package moe.lava.banksia.client.datasource.local
|
||||
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.room.dao.RouteDao
|
||||
import moe.lava.banksia.room.entity.asEntity
|
||||
|
||||
class RouteLocalDataSource(private val dao: RouteDao) {
|
||||
suspend fun get(id: String) = dao.get(id)
|
||||
suspend fun getAll() = dao.getAll()
|
||||
suspend fun save(vararg routes: Route) = dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package moe.lava.banksia.client.datasource.local
|
||||
|
||||
import moe.lava.banksia.model.Stop
|
||||
import moe.lava.banksia.room.dao.RouteDao
|
||||
import moe.lava.banksia.room.dao.StopDao
|
||||
import moe.lava.banksia.room.entity.asEntity
|
||||
|
||||
class StopLocalDataSource(private val dao: StopDao, private val routeDao: RouteDao) {
|
||||
suspend fun get(id: String) = dao.get(id)
|
||||
suspend fun getByRoute(id: String) = routeDao.stops(id)
|
||||
suspend fun save(vararg stops: Stop) = dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray())
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package moe.lava.banksia.client.datasource.remote
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import moe.lava.banksia.model.Route
|
||||
|
||||
class RouteRemoteDataSource(val client: HttpClient) {
|
||||
suspend fun get(id: String) = client.get("/routes/${id}").body<Route>()
|
||||
suspend fun getAll() = client.get("/routes").body<List<Route>>()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package moe.lava.banksia.client.datasource.remote
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import moe.lava.banksia.model.Stop
|
||||
|
||||
class StopRemoteDataSource(val client: HttpClient) {
|
||||
suspend fun get(id: String) = client.get("/stops/${id}").body<Stop>()
|
||||
suspend fun getByRoute(id: String) = client.get("/route_stops/${id}").body<List<Stop>>()
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package moe.lava.banksia.client.di
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import moe.lava.banksia.Constants
|
||||
import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
|
||||
import moe.lava.banksia.client.datasource.local.StopLocalDataSource
|
||||
import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
|
||||
import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
|
||||
import moe.lava.banksia.client.repository.RouteRepository
|
||||
import moe.lava.banksia.client.repository.StopRepository
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.ui.screens.MapScreenViewModel
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val ClientModule = module {
|
||||
// HTTP Clients
|
||||
singleOf(::PtvService)
|
||||
single {
|
||||
HttpClient() {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
defaultRequest {
|
||||
url(Constants.serverUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data sources
|
||||
singleOf(::RouteLocalDataSource)
|
||||
singleOf(::RouteRemoteDataSource)
|
||||
singleOf(::StopLocalDataSource)
|
||||
singleOf(::StopRemoteDataSource)
|
||||
|
||||
// Repositories
|
||||
singleOf(::RouteRepository)
|
||||
singleOf(::StopRepository)
|
||||
|
||||
// ViewModel
|
||||
viewModelOf(::MapScreenViewModel)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package moe.lava.banksia.client.repository
|
||||
|
||||
import moe.lava.banksia.client.datasource.local.RouteLocalDataSource
|
||||
import moe.lava.banksia.client.datasource.remote.RouteRemoteDataSource
|
||||
|
||||
class RouteRepository(
|
||||
private val local: RouteLocalDataSource,
|
||||
private val remote: RouteRemoteDataSource,
|
||||
) {
|
||||
suspend fun getAll() =
|
||||
local
|
||||
.getAll()
|
||||
.map { it.asModel() }
|
||||
.ifEmpty {
|
||||
remote
|
||||
.getAll()
|
||||
.also { local.save(*it.toTypedArray()) }
|
||||
}
|
||||
|
||||
suspend fun get(id: String) = local.get(id)?.asModel() ?: remote.get(id)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package moe.lava.banksia.client.repository
|
||||
|
||||
import moe.lava.banksia.client.datasource.local.StopLocalDataSource
|
||||
import moe.lava.banksia.client.datasource.remote.StopRemoteDataSource
|
||||
|
||||
class StopRepository(
|
||||
private val local: StopLocalDataSource,
|
||||
private val remote: StopRemoteDataSource,
|
||||
) {
|
||||
suspend fun get(id: String) = local.get(id)?.asModel() ?: remote.get(id)
|
||||
suspend fun getByRoute(id: String) =
|
||||
local
|
||||
.getByRoute(id)
|
||||
.map { it.asModel() }
|
||||
.ifEmpty { null }
|
||||
?: remote.getByRoute(id)
|
||||
}
|
||||
|
|
@ -1,194 +1,21 @@
|
|||
package moe.lava.banksia.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
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.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
|
||||
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
|
||||
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.my_location_24
|
||||
import moe.lava.banksia.ui.layout.InfoPanel
|
||||
import moe.lava.banksia.ui.layout.Searcher
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.platform.maps.Maps
|
||||
import moe.lava.banksia.ui.platform.maps.getScreenHeight
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.util.Point
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.roundToInt
|
||||
import moe.lava.banksia.client.di.ClientModule
|
||||
import moe.lava.banksia.di.CommonModules
|
||||
import moe.lava.banksia.ui.screens.MapScreen
|
||||
import org.koin.compose.KoinMultiplatformApplication
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.dsl.koinConfiguration
|
||||
|
||||
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, KoinExperimentalAPI::class)
|
||||
@Composable
|
||||
fun App(
|
||||
viewModel: BanksiaViewModel = viewModel()
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
|
||||
val locationTracker = remember { locationFactory.createLocationTracker() }
|
||||
BindLocationTrackerEffect(locationTracker)
|
||||
viewModel.bindTracker(locationTracker)
|
||||
scope.launch { locationTracker.startTracking() }
|
||||
|
||||
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||
val mapState by viewModel.mapState.collectAsStateWithLifecycle()
|
||||
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
skipHiddenState = false
|
||||
)
|
||||
)
|
||||
|
||||
val sheetState = scaffoldState.bottomSheetState
|
||||
val extInsets = if (
|
||||
sheetState.currentValue != SheetValue.Hidden ||
|
||||
sheetState.targetValue != SheetValue.Hidden
|
||||
) {
|
||||
val offset = runCatching { sheetState.requireOffset() }
|
||||
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
|
||||
(getScreenHeight() - scaffoldOffset - WindowInsets.Companion.safeDrawing.getBottom(
|
||||
LocalDensity.current)).coerceAtLeast(0)
|
||||
} else 0
|
||||
|
||||
LaunchedEffect(infoState) {
|
||||
if (infoState !is InfoPanelState.None)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
}
|
||||
|
||||
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
||||
|
||||
BanksiaTheme {
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
sheetContent = {
|
||||
InfoPanel(
|
||||
state = infoState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
onPeekHeightChange = { peekHeight = it },
|
||||
)
|
||||
},
|
||||
sheetDragHandle = {
|
||||
val density = LocalDensity.current
|
||||
Box(
|
||||
Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.onSizeChanged {
|
||||
handleHeight = with(density) { it.height.toDp() }
|
||||
}
|
||||
) {
|
||||
BottomSheetDefaults.DragHandle(modifier = Modifier.Companion.align(Alignment.Companion.Center))
|
||||
}
|
||||
},
|
||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
) {
|
||||
Maps(
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
state = mapState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||
}, bottom = extInsets),
|
||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
)
|
||||
Searcher(
|
||||
state = searchState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
expanded = searchExpandedState,
|
||||
onExpandedChange = {
|
||||
searchExpandedState = it
|
||||
if (it)
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
},
|
||||
)
|
||||
|
||||
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
||||
sheetSwipeEnabled = false
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded) {
|
||||
peekHeightMultiplier = 1F - backEvent.progress
|
||||
}
|
||||
}
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded)
|
||||
scope.launch {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
peekHeightMultiplier = 1F
|
||||
viewModel.handleEvent(BanksiaEvent.DismissState)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
peekHeightMultiplier = 1F
|
||||
}
|
||||
sheetSwipeEnabled = true
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.Companion.windowInsetsPadding(
|
||||
WindowInsets.Companion.safeContent.add(
|
||||
WindowInsets(bottom = extInsets)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Companion.BottomEnd
|
||||
) {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
onClick = { viewModel.centreCameraToLocation() },
|
||||
) {
|
||||
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
||||
}
|
||||
}
|
||||
}
|
||||
fun App() {
|
||||
KoinMultiplatformApplication(config = koinConfiguration {
|
||||
modules(CommonModules, ClientModule)
|
||||
}) {
|
||||
MapScreen()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.model.RouteType.Interstate
|
||||
import moe.lava.banksia.model.RouteType.MetroBus
|
||||
import moe.lava.banksia.model.RouteType.MetroTrain
|
||||
import moe.lava.banksia.model.RouteType.MetroTram
|
||||
import moe.lava.banksia.model.RouteType.RegionalBus
|
||||
import moe.lava.banksia.model.RouteType.RegionalCoach
|
||||
import moe.lava.banksia.model.RouteType.RegionalTrain
|
||||
import moe.lava.banksia.model.RouteType.SkyBus
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.bus
|
||||
import moe.lava.banksia.resources.bus_background
|
||||
|
|
@ -33,12 +42,51 @@ data class RouteTypeProperties(
|
|||
val icon: DrawableResource,
|
||||
)
|
||||
|
||||
const val TRAIN_BLUE = 0xFF0072CE
|
||||
const val TRAM_GREEN = 0xFF78BE20
|
||||
const val BUS_ORANGE = 0xFFFF8200
|
||||
const val VLINE_PURPLE = 0xFF8F1A95
|
||||
|
||||
fun RouteType.getUIProperties(): RouteTypeProperties {
|
||||
val colour = when (this) {
|
||||
MetroTrain -> TRAIN_BLUE
|
||||
MetroTram -> TRAM_GREEN
|
||||
MetroBus -> BUS_ORANGE
|
||||
RegionalTrain -> VLINE_PURPLE
|
||||
RegionalCoach -> VLINE_PURPLE
|
||||
RegionalBus -> VLINE_PURPLE
|
||||
SkyBus -> BUS_ORANGE
|
||||
Interstate -> BUS_ORANGE
|
||||
}
|
||||
|
||||
val (drawable, background, icon) = when (this) {
|
||||
MetroTrain,
|
||||
RegionalTrain,
|
||||
Interstate -> Triple(
|
||||
Res.drawable.train, Res.drawable.train_background, Res.drawable.train_icon
|
||||
)
|
||||
|
||||
MetroTram -> Triple(
|
||||
Res.drawable.tram, Res.drawable.tram_background, Res.drawable.tram_icon
|
||||
)
|
||||
|
||||
MetroBus,
|
||||
RegionalCoach,
|
||||
RegionalBus,
|
||||
SkyBus -> Triple(
|
||||
Res.drawable.bus, Res.drawable.bus_background, Res.drawable.bus_icon
|
||||
)
|
||||
}
|
||||
|
||||
return RouteTypeProperties(Color(colour), drawable, background, icon)
|
||||
}
|
||||
|
||||
fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
||||
val colour = when (this) {
|
||||
PtvRouteType.TRAIN -> Color(0xFF0072CE)
|
||||
PtvRouteType.TRAM -> Color(0xFF78BE20)
|
||||
PtvRouteType.BUS, PtvRouteType.NIGHT_BUS -> Color(0xFFFF8200)
|
||||
PtvRouteType.VLINE -> Color(0xFF8F1A95)
|
||||
PtvRouteType.TRAIN -> Color(TRAIN_BLUE)
|
||||
PtvRouteType.TRAM -> Color(TRAM_GREEN)
|
||||
PtvRouteType.BUS, PtvRouteType.NIGHT_BUS -> Color(BUS_ORANGE)
|
||||
PtvRouteType.VLINE -> Color(VLINE_PURPLE)
|
||||
}
|
||||
val (drawable, background, icon) = when (this) {
|
||||
PtvRouteType.TRAM -> Triple(
|
||||
|
|
@ -58,7 +106,7 @@ fun PtvRouteType.getUIProperties(): RouteTypeProperties {
|
|||
fun RouteIcon(
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
size: Dp = 40.dp,
|
||||
routeType: PtvRouteType,
|
||||
routeType: RouteType,
|
||||
) {
|
||||
val properties = routeType.getUIProperties()
|
||||
Image(
|
||||
|
|
@ -80,9 +128,9 @@ const val ICON_PADDING = 0.25f
|
|||
@Composable
|
||||
private fun RouteIconPreview() {
|
||||
Row {
|
||||
RouteIcon(routeType = PtvRouteType.TRAIN)
|
||||
RouteIcon(routeType = PtvRouteType.TRAM)
|
||||
RouteIcon(routeType = PtvRouteType.BUS)
|
||||
RouteIcon(routeType = RouteType.MetroTrain)
|
||||
RouteIcon(routeType = RouteType.MetroTram)
|
||||
RouteIcon(routeType = RouteType.MetroBus)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ 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 moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
|
||||
@Composable
|
||||
fun InfoPanel(
|
||||
state: InfoPanelState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
onPeekHeightChange: (Dp) -> Unit,
|
||||
) {
|
||||
if (state is InfoPanelState.None)
|
||||
|
|
@ -74,7 +74,7 @@ fun InfoPanel(
|
|||
@Composable
|
||||
private inline fun RouteInfoPanel(
|
||||
state: InfoPanelState.Route,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.Companion.fillMaxWidth()) {
|
||||
Row {
|
||||
|
|
@ -92,7 +92,7 @@ private inline fun RouteInfoPanel(
|
|||
@Composable
|
||||
private inline fun RunInfoPanel(
|
||||
state: InfoPanelState.Run,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.Companion.fillMaxWidth()) {
|
||||
Row {
|
||||
|
|
@ -110,7 +110,7 @@ private inline fun RunInfoPanel(
|
|||
@Composable
|
||||
private inline fun StopInfoPanel(
|
||||
state: InfoPanelState.Stop,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.Companion.fillMaxWidth()) {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.components.RouteIcon
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Searcher(
|
||||
state: SearchState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
|
|
@ -55,7 +55,7 @@ fun Searcher(
|
|||
SearchBarDefaults.InputField(
|
||||
modifier = Modifier.Companion.padding(horizontal = 20.dp - animatedPadding),
|
||||
query = state.text,
|
||||
onQueryChange = { onEvent(BanksiaEvent.SearchUpdate(it)) },
|
||||
onQueryChange = { onEvent(MapScreenEvent.SearchUpdate(it)) },
|
||||
onSearch = {},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
|
|
@ -67,7 +67,7 @@ fun Searcher(
|
|||
contentDescription = null,
|
||||
modifier = Modifier.Companion.clickable {
|
||||
onEvent(
|
||||
BanksiaEvent.SearchUpdate(
|
||||
MapScreenEvent.SearchUpdate(
|
||||
""
|
||||
)
|
||||
)
|
||||
|
|
@ -92,8 +92,8 @@ fun Searcher(
|
|||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.clickable {
|
||||
onExpandedChange(false)
|
||||
onEvent(BanksiaEvent.SearchUpdate(""))
|
||||
onEvent(BanksiaEvent.SelectRoute(entry.routeId))
|
||||
onEvent(MapScreenEvent.SearchUpdate(""))
|
||||
onEvent(MapScreenEvent.SelectRoute(entry.routeId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import moe.lava.banksia.ui.BanksiaEvent
|
||||
import moe.lava.banksia.ui.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.Point
|
||||
|
|
@ -18,7 +18,7 @@ expect fun getScreenHeight(): Int
|
|||
expect fun Maps(
|
||||
modifier: Modifier = Modifier.Companion,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package moe.lava.banksia.ui.platform.maps
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.util.Point
|
||||
|
||||
sealed class Marker {
|
||||
|
|
@ -9,14 +9,14 @@ sealed class Marker {
|
|||
|
||||
data class Stop(
|
||||
override val point: Point,
|
||||
val id: Int,
|
||||
val type: PtvRouteType,
|
||||
val id: String,
|
||||
val type: RouteType,
|
||||
val colour: Color,
|
||||
) : Marker()
|
||||
|
||||
data class Vehicle(
|
||||
override val point: Point,
|
||||
val ref: String,
|
||||
val type: PtvRouteType,
|
||||
val type: RouteType,
|
||||
) : Marker()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
package moe.lava.banksia.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
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.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
|
||||
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
|
||||
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.resources.Res
|
||||
import moe.lava.banksia.resources.my_location_24
|
||||
import moe.lava.banksia.ui.layout.InfoPanel
|
||||
import moe.lava.banksia.ui.layout.Searcher
|
||||
import moe.lava.banksia.ui.platform.BanksiaTheme
|
||||
import moe.lava.banksia.ui.platform.maps.Maps
|
||||
import moe.lava.banksia.ui.platform.maps.getScreenHeight
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.util.Point
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun MapScreen(
|
||||
viewModel: MapScreenViewModel = koinViewModel()
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
|
||||
val locationTracker = remember { locationFactory.createLocationTracker() }
|
||||
BindLocationTrackerEffect(locationTracker)
|
||||
viewModel.bindTracker(locationTracker)
|
||||
scope.launch { locationTracker.startTracking() }
|
||||
|
||||
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||
val mapState by viewModel.mapState.collectAsStateWithLifecycle()
|
||||
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
initialValue = SheetValue.Hidden,
|
||||
skipHiddenState = false
|
||||
)
|
||||
)
|
||||
|
||||
val sheetState = scaffoldState.bottomSheetState
|
||||
val extInsets = if (
|
||||
sheetState.currentValue != SheetValue.Hidden ||
|
||||
sheetState.targetValue != SheetValue.Hidden
|
||||
) {
|
||||
val offset = runCatching { sheetState.requireOffset() }
|
||||
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
|
||||
(getScreenHeight() - scaffoldOffset - WindowInsets.Companion.safeDrawing.getBottom(
|
||||
LocalDensity.current)).coerceAtLeast(0)
|
||||
} else 0
|
||||
|
||||
LaunchedEffect(infoState) {
|
||||
if (infoState !is InfoPanelState.None)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
}
|
||||
|
||||
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeight by remember { mutableStateOf(0.dp) }
|
||||
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
||||
|
||||
BanksiaTheme {
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
sheetContent = {
|
||||
InfoPanel(
|
||||
state = infoState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
onPeekHeightChange = { peekHeight = it },
|
||||
)
|
||||
},
|
||||
sheetDragHandle = {
|
||||
val density = LocalDensity.current
|
||||
Box(
|
||||
Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.onSizeChanged {
|
||||
handleHeight = with(density) { it.height.toDp() }
|
||||
}
|
||||
) {
|
||||
BottomSheetDefaults.DragHandle(modifier = Modifier.Companion.align(Alignment.Companion.Center))
|
||||
}
|
||||
},
|
||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
) {
|
||||
Maps(
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
state = mapState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||
}, bottom = extInsets),
|
||||
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||
)
|
||||
Searcher(
|
||||
state = searchState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
expanded = searchExpandedState,
|
||||
onExpandedChange = {
|
||||
searchExpandedState = it
|
||||
if (it)
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
},
|
||||
)
|
||||
|
||||
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
||||
sheetSwipeEnabled = false
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded) {
|
||||
peekHeightMultiplier = 1F - backEvent.progress
|
||||
}
|
||||
}
|
||||
if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded)
|
||||
scope.launch {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
peekHeightMultiplier = 1F
|
||||
viewModel.handleEvent(MapScreenEvent.DismissState)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
peekHeightMultiplier = 1F
|
||||
}
|
||||
sheetSwipeEnabled = true
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.Companion.windowInsetsPadding(
|
||||
WindowInsets.Companion.safeContent.add(
|
||||
WindowInsets(bottom = extInsets)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Companion.BottomEnd
|
||||
) {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
onClick = { viewModel.centreCameraToLocation() },
|
||||
) {
|
||||
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package moe.lava.banksia.ui
|
||||
package moe.lava.banksia.ui.screens
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -13,9 +13,12 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.client.repository.RouteRepository
|
||||
import moe.lava.banksia.client.repository.StopRepository
|
||||
import moe.lava.banksia.data.ptv.PtvService
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRoute
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.Route
|
||||
import moe.lava.banksia.model.RouteType
|
||||
import moe.lava.banksia.ui.components.getUIProperties
|
||||
import moe.lava.banksia.ui.platform.maps.CameraPosition
|
||||
import moe.lava.banksia.ui.platform.maps.CameraPositionBounds
|
||||
|
|
@ -32,23 +35,27 @@ import moe.lava.banksia.util.log
|
|||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
sealed class BanksiaEvent {
|
||||
data object DismissState : BanksiaEvent()
|
||||
sealed class MapScreenEvent {
|
||||
data object DismissState : MapScreenEvent()
|
||||
|
||||
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
||||
data class SelectRun(val ref: String?) : BanksiaEvent()
|
||||
data class SelectStop(val typeAndId: Pair<PtvRouteType, Int>) : BanksiaEvent()
|
||||
data class SelectRoute(val id: String?) : MapScreenEvent()
|
||||
data class SelectRun(val ref: String?) : MapScreenEvent()
|
||||
data class SelectStop(val typeIdPair: Pair<RouteType, String>?) : MapScreenEvent()
|
||||
|
||||
data class SearchUpdate(val text: String) : BanksiaEvent()
|
||||
data class SearchUpdate(val text: String) : MapScreenEvent()
|
||||
}
|
||||
|
||||
data class InternalState(
|
||||
val route: Int? = null,
|
||||
val stop: Pair<PtvRouteType, Int>? = null,
|
||||
val route: String? = null,
|
||||
val stop: Pair<RouteType, String>? = null,
|
||||
val run: String? = null,
|
||||
)
|
||||
|
||||
class BanksiaViewModel : ViewModel() {
|
||||
class MapScreenViewModel(
|
||||
private val ptvService: PtvService,
|
||||
private val routeRepository: RouteRepository,
|
||||
private val stopRepository: StopRepository,
|
||||
) : ViewModel() {
|
||||
private var state = InternalState()
|
||||
set(value) {
|
||||
val last = field
|
||||
|
|
@ -72,7 +79,6 @@ class BanksiaViewModel : ViewModel() {
|
|||
private val iSearchState = MutableStateFlow(SearchState())
|
||||
val searchState = iSearchState.asStateFlow()
|
||||
|
||||
private val ptvService = PtvService(viewModelScope)
|
||||
private var locationTrackerJob: Job? = null
|
||||
private var lastKnownLocation: Point? = null
|
||||
|
||||
|
|
@ -80,14 +86,14 @@ class BanksiaViewModel : ViewModel() {
|
|||
viewModelScope.launch { searchUpdate("") }
|
||||
}
|
||||
|
||||
fun handleEvent(event: BanksiaEvent) {
|
||||
fun handleEvent(event: MapScreenEvent) {
|
||||
viewModelScope.launch {
|
||||
when (event) {
|
||||
is BanksiaEvent.DismissState -> dismissState()
|
||||
is BanksiaEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||
is BanksiaEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
||||
is BanksiaEvent.SelectStop -> state = state.copy(stop = event.typeAndId, run = null)
|
||||
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
is MapScreenEvent.DismissState -> dismissState()
|
||||
is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id)
|
||||
is MapScreenEvent.SelectRun -> state = state.copy(run = event.ref, stop = null)
|
||||
is MapScreenEvent.SelectStop -> state = state.copy(stop = event.typeIdPair, run = null)
|
||||
is MapScreenEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +105,11 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun centreCameraToLocation() {
|
||||
viewModelScope.launch {
|
||||
log("msvm", "getting..")
|
||||
val routes = routeRepository.getAll()
|
||||
log("msvm", routes.joinToString("\n"))
|
||||
}
|
||||
lastKnownLocation?.let { location ->
|
||||
viewModelScope.launch {
|
||||
log("bvm", "emitting $location")
|
||||
|
|
@ -117,46 +128,48 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private suspend fun searchUpdate(text: String) {
|
||||
val entries = ptvService.routes()
|
||||
iSearchState.update { it.copy(text = text) }
|
||||
val entries = routeRepository.getAll()
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.gtfsSubType()?.ordinal },
|
||||
{ it.routeNumber.toIntOrNull() },
|
||||
{ it.routeName }
|
||||
{ it.type.ordinal },
|
||||
{ it.number },
|
||||
{ it.name }
|
||||
)
|
||||
)
|
||||
.filter { it.routeNumber.contains(text) || it.routeName.lowercase().contains(text.lowercase()) }
|
||||
.filter { (it.number ?: "").contains(text) || it.name.lowercase().contains(text.lowercase()) }
|
||||
.map { route ->
|
||||
val (main, sub) = if (route.routeNumber.isNotEmpty()) {
|
||||
route.routeNumber to route.routeName
|
||||
val (main, sub) = if (route.number?.isNotEmpty() == true) {
|
||||
route.number to route.name
|
||||
} else {
|
||||
route.routeName to null
|
||||
route.name to null
|
||||
}
|
||||
|
||||
SearchState.SearchEntry(main, sub, route.routeId, route.routeType)
|
||||
SearchState.SearchEntry(main!!, sub, route.id, route.type)
|
||||
}
|
||||
|
||||
iSearchState.update { SearchState(entries, text) }
|
||||
}
|
||||
|
||||
private suspend fun switchRoute(routeId: Int?) {
|
||||
private suspend fun switchRoute(routeId: String?) {
|
||||
iMapState.update { MapState() }
|
||||
if (routeId == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
|
||||
val route = ptvService.route(routeId)
|
||||
val route = routeRepository.get(routeId)
|
||||
// val gtfsRoute = ptvService.route(routeId)
|
||||
iInfoState.update {
|
||||
InfoPanelState.Route(
|
||||
name = route.routeName,
|
||||
type = route.routeType,
|
||||
name = route.name,
|
||||
type = route.type,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch { buildPolylines(route) }
|
||||
// viewModelScope.launch { buildPolylines(gtfsRoute) }
|
||||
viewModelScope.launch { buildStops(route) }
|
||||
buildRuns(route)
|
||||
// buildRuns(gtfsRoute)
|
||||
}
|
||||
|
||||
private fun switchRun(ref: String?) {
|
||||
|
|
@ -175,7 +188,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
iInfoState.update {
|
||||
InfoPanelState.Run(
|
||||
direction = run.destinationName,
|
||||
type = run.routeType,
|
||||
type = RouteType.MetroTrain, // XXX HACK TODO FIXME
|
||||
)
|
||||
}
|
||||
routeName = ptvService.route(run.routeId).routeName
|
||||
|
|
@ -184,7 +197,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
iInfoState.update {
|
||||
InfoPanelState.Run(
|
||||
direction = run.destinationName,
|
||||
type = run.routeType,
|
||||
type = RouteType.MetroTrain, // FIXME HACK XXX TODO
|
||||
routeName = routeName,
|
||||
)
|
||||
}
|
||||
|
|
@ -193,25 +206,27 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
private suspend fun switchStop(typeAndId: Pair<PtvRouteType, Int>?) {
|
||||
if (typeAndId == null) {
|
||||
private suspend fun switchStop(pair: Pair<RouteType, String>?) {
|
||||
if (pair == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
val (routeType, stopId) = typeAndId
|
||||
val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.stopName.split("/")
|
||||
val (type, id) = pair
|
||||
|
||||
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)
|
||||
iInfoState.update {
|
||||
InfoPanelState.Stop(
|
||||
id = stop.stopId,
|
||||
id = stop.id,
|
||||
name = name,
|
||||
subname = subname,
|
||||
)
|
||||
}
|
||||
|
||||
val res = ptvService.departures(stop.routeType, stop.stopId)
|
||||
val res = ptvService.departures(type, stop.id)
|
||||
// Map<
|
||||
// Pair<DirectionId, RouteId>,
|
||||
// Pair<DirectionName, List<DepartureTimes>>
|
||||
|
|
@ -285,7 +300,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
ptvService
|
||||
.runsFlow(route.routeId)
|
||||
.waitUntilSubscribed(iInfoState)
|
||||
.takeWhile { state.route == route.routeId }
|
||||
// .takeWhile { state.route == route.routeId }
|
||||
.onEach { runs ->
|
||||
val markers = runs
|
||||
.filter { it.vehiclePosition != null }
|
||||
|
|
@ -295,7 +310,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
Marker.Vehicle(
|
||||
Point(pos.latitude, pos.longitude),
|
||||
ref = run.runRef,
|
||||
type = route.routeType,
|
||||
type = RouteType.MetroTrain, // HACK TODO XXX FIXME
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -305,18 +320,17 @@ class BanksiaViewModel : ViewModel() {
|
|||
|
||||
}
|
||||
|
||||
private suspend fun buildStops(route: PtvRoute) {
|
||||
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
||||
val colour = route.routeType.getUIProperties().colour
|
||||
private suspend fun buildStops(route: Route) {
|
||||
val stops = stopRepository.getByRoute(route.id)
|
||||
val colour = route.type.getUIProperties().colour
|
||||
|
||||
val markers = stops
|
||||
.filter { it.stopLatitude != null && it.stopLongitude != null }
|
||||
.map { stop ->
|
||||
Marker.Stop(
|
||||
point = Point(stop.stopLatitude!!, stop.stopLongitude!!),
|
||||
id = stop.stopId,
|
||||
point = stop.pos,
|
||||
id = stop.id,
|
||||
colour = colour,
|
||||
type = route.routeType,
|
||||
type = route.type,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package moe.lava.banksia.ui.state
|
||||
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
sealed class InfoPanelState {
|
||||
abstract val loading: Boolean
|
||||
|
|
@ -11,21 +11,21 @@ sealed class InfoPanelState {
|
|||
|
||||
data class Route(
|
||||
val name: String,
|
||||
val type: PtvRouteType,
|
||||
val type: RouteType,
|
||||
) : InfoPanelState() {
|
||||
override val loading = false
|
||||
}
|
||||
|
||||
data class Run(
|
||||
val direction: String,
|
||||
val type: PtvRouteType,
|
||||
val type: RouteType,
|
||||
val routeName: String? = null,
|
||||
) : InfoPanelState() {
|
||||
override val loading = routeName == null
|
||||
}
|
||||
|
||||
data class Stop(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val subname: String? = null,
|
||||
val departures: List<Departure>? = null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package moe.lava.banksia.ui.state
|
||||
|
||||
import moe.lava.banksia.data.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.model.RouteType
|
||||
|
||||
data class SearchState(
|
||||
val entries: List<SearchEntry> = listOf(),
|
||||
|
|
@ -9,7 +9,7 @@ data class SearchState(
|
|||
data class SearchEntry(
|
||||
val mainText: String,
|
||||
val subText: String?,
|
||||
val routeId: Int,
|
||||
val routeType: PtvRouteType,
|
||||
val routeId: String,
|
||||
val routeType: RouteType,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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.screens.MapScreenEvent
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.Point
|
||||
|
|
@ -23,7 +23,7 @@ actual fun getScreenHeight(): Int {
|
|||
actual fun Maps(
|
||||
modifier: Modifier,
|
||||
state: MapState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onEvent: (MapScreenEvent) -> Unit,
|
||||
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||
setLastKnownLocation: (Point) -> Unit,
|
||||
extInsets: WindowInsets,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue