refactor: move logic to viewmodel
This commit is contained in:
parent
ba367d106c
commit
64e6ccf08b
10 changed files with 363 additions and 237 deletions
|
|
@ -47,6 +47,7 @@ kotlin {
|
||||||
implementation(compose.components.resources)
|
implementation(compose.components.resources)
|
||||||
implementation(compose.components.uiToolingPreview)
|
implementation(compose.components.uiToolingPreview)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.lifecycle.viewmodel)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
@ -54,7 +55,6 @@ kotlin {
|
||||||
implementation(libs.moko.geo.compose)
|
implementation(libs.moko.geo.compose)
|
||||||
implementation(projects.shared)
|
implementation(projects.shared)
|
||||||
implementation(libs.ui.backhandler)
|
implementation(libs.ui.backhandler)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
|
@ -24,9 +26,9 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import com.google.android.gms.maps.CameraUpdateFactory
|
import com.google.android.gms.maps.CameraUpdateFactory
|
||||||
import com.google.android.gms.maps.model.CameraPosition
|
|
||||||
import com.google.android.gms.maps.model.LatLng
|
import com.google.android.gms.maps.model.LatLng
|
||||||
import com.google.android.gms.maps.model.LatLngBounds
|
import com.google.android.gms.maps.model.LatLngBounds
|
||||||
import com.google.android.gms.maps.model.MapStyleOptions
|
import com.google.android.gms.maps.model.MapStyleOptions
|
||||||
|
|
@ -38,8 +40,11 @@ import com.google.maps.android.compose.MarkerComposable
|
||||||
import com.google.maps.android.compose.Polyline
|
import com.google.maps.android.compose.Polyline
|
||||||
import com.google.maps.android.compose.rememberCameraPositionState
|
import com.google.maps.android.compose.rememberCameraPositionState
|
||||||
import com.google.maps.android.compose.rememberMarkerState
|
import com.google.maps.android.compose.rememberMarkerState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import moe.lava.banksia.R
|
import moe.lava.banksia.R
|
||||||
import moe.lava.banksia.native.BanksiaTheme
|
import moe.lava.banksia.native.BanksiaTheme
|
||||||
|
import moe.lava.banksia.ui.BoxedValue
|
||||||
|
import com.google.android.gms.maps.model.CameraPosition as GoogleCameraPosition
|
||||||
|
|
||||||
fun Point.toLatLng(): LatLng = LatLng(this.lat, this.lng)
|
fun Point.toLatLng(): LatLng = LatLng(this.lat, this.lng)
|
||||||
|
|
||||||
|
|
@ -61,31 +66,36 @@ actual fun Maps(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
markers: List<Marker>,
|
markers: List<Marker>,
|
||||||
polylines: List<Polyline>,
|
polylines: List<Polyline>,
|
||||||
newCameraPosition: Pair<Point, Pair<Point, Point>?>?,
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
cameraPositionUpdated: () -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
) {
|
) {
|
||||||
var camPos = rememberCameraPositionState()
|
val scope = rememberCoroutineScope()
|
||||||
val ctx = LocalContext.current
|
val camPos = rememberCameraPositionState()
|
||||||
val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
|
val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(newCameraPos) {
|
||||||
fusedLocation.lastLocation.addOnSuccessListener {
|
val pos = newCameraPos?.value ?: return@LaunchedEffect
|
||||||
if (it != null)
|
val update = if (pos.bounds != null) {
|
||||||
camPos.position = CameraPosition(LatLng(it.latitude, it.longitude), 16.0f, 0.0f, 0.0f)
|
val (northeast, southwest) = pos.bounds
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(newCameraPosition) {
|
|
||||||
if (newCameraPosition != null) {
|
|
||||||
if (newCameraPosition.second != null) {
|
|
||||||
val (northeast, southwest) = newCameraPosition.second!!
|
|
||||||
val bounds = LatLngBounds(
|
val bounds = LatLngBounds(
|
||||||
southwest.toLatLng(),
|
southwest.toLatLng(),
|
||||||
northeast.toLatLng()
|
northeast.toLatLng()
|
||||||
)
|
)
|
||||||
camPos.animate(CameraUpdateFactory.newLatLngBounds(bounds, 150), 1000)
|
CameraUpdateFactory.newLatLngBounds(bounds, 150)
|
||||||
} else
|
} else
|
||||||
camPos.animate(CameraUpdateFactory.newLatLngZoom(newCameraPosition.first.toLatLng(), 16.0f), 1000)
|
CameraUpdateFactory.newLatLngZoom(pos.centre.toLatLng(), 16.0f)
|
||||||
cameraPositionUpdated()
|
|
||||||
|
camPos.animate(update, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
fusedLocation.lastLocation.addOnSuccessListener {
|
||||||
|
if (it != null) {
|
||||||
|
camPos.position = GoogleCameraPosition(LatLng(it.latitude, it.longitude), 16.0f, 0.0f, 0.0f)
|
||||||
|
setLastKnownLocation(Point(it.latitude, it.longitude))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
|
@ -35,24 +35,19 @@ import androidx.compose.ui.backhandler.PredictiveBackHandler
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
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.BindLocationTrackerEffect
|
||||||
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
|
import dev.icerock.moko.geo.compose.LocationTrackerAccuracy
|
||||||
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
|
import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.lava.banksia.api.ptv.PtvService
|
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvStop
|
|
||||||
import moe.lava.banksia.api.ptv.structures.getProperties
|
|
||||||
import moe.lava.banksia.native.BanksiaTheme
|
import moe.lava.banksia.native.BanksiaTheme
|
||||||
import moe.lava.banksia.native.maps.Maps
|
import moe.lava.banksia.native.maps.Maps
|
||||||
import moe.lava.banksia.native.maps.Marker
|
|
||||||
import moe.lava.banksia.native.maps.MarkerType
|
|
||||||
import moe.lava.banksia.native.maps.Point
|
import moe.lava.banksia.native.maps.Point
|
||||||
import moe.lava.banksia.native.maps.Polyline
|
|
||||||
import moe.lava.banksia.native.maps.getScreenHeight
|
import moe.lava.banksia.native.maps.getScreenHeight
|
||||||
import moe.lava.banksia.resources.Res
|
import moe.lava.banksia.resources.Res
|
||||||
import moe.lava.banksia.resources.my_location_24
|
import moe.lava.banksia.resources.my_location_24
|
||||||
|
import moe.lava.banksia.ui.BanksiaViewModel
|
||||||
import moe.lava.banksia.ui.Searcher
|
import moe.lava.banksia.ui.Searcher
|
||||||
import moe.lava.banksia.ui.StopInfoPanel
|
import moe.lava.banksia.ui.StopInfoPanel
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
@ -60,29 +55,23 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun buildBounds(points: List<Point>): Pair<Point, Point> {
|
val MELBOURNE = Point(-37.8136, 144.9631)
|
||||||
var north = -Double.MAX_VALUE
|
|
||||||
var south = Double.MAX_VALUE
|
|
||||||
var east = -Double.MAX_VALUE
|
|
||||||
var west = Double.MAX_VALUE
|
|
||||||
points.forEach {
|
|
||||||
if (it.lat > north)
|
|
||||||
north = it.lat;
|
|
||||||
if (it.lat < south)
|
|
||||||
south = it.lat;
|
|
||||||
if (it.lng > east)
|
|
||||||
east = it.lng;
|
|
||||||
if (it.lng < west)
|
|
||||||
west = it.lng;
|
|
||||||
}
|
|
||||||
return Pair(Point(north, east), Point(south, west))
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun App() {
|
fun App(
|
||||||
val ptvService = remember { PtvService() }
|
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 state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||||
bottomSheetState = rememberStandardBottomSheetState(
|
bottomSheetState = rememberStandardBottomSheetState(
|
||||||
|
|
@ -91,18 +80,6 @@ fun App() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
|
|
||||||
val locationTracker = remember { locationFactory.createLocationTracker() }
|
|
||||||
BindLocationTrackerEffect(locationTracker)
|
|
||||||
var lastLocation by remember { mutableStateOf(Point(-37.8136, 144.9631)) }
|
|
||||||
var newCameraPosition by remember {
|
|
||||||
mutableStateOf<Pair<Point, Pair<Point, Point>?>?>(
|
|
||||||
Pair(Point(-37.8136, 144.9631), null)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var searchTextState by remember { mutableStateOf("") }
|
|
||||||
var searchExpandedState by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val sheetState = scaffoldState.bottomSheetState
|
val sheetState = scaffoldState.bottomSheetState
|
||||||
val extInsets = if (
|
val extInsets = if (
|
||||||
sheetState.currentValue != SheetValue.Hidden ||
|
sheetState.currentValue != SheetValue.Hidden ||
|
||||||
|
|
@ -113,72 +90,31 @@ fun App() {
|
||||||
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
|
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
|
||||||
} else 0
|
} else 0
|
||||||
|
|
||||||
var scope = rememberCoroutineScope()
|
LaunchedEffect(state.stopState) {
|
||||||
scope.launch {
|
val isShown = state.stopState != null
|
||||||
val flow = locationTracker.getLocationsFlow()
|
if (isShown)
|
||||||
locationTracker.startTracking()
|
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||||
flow.distinctUntilChanged().collect {
|
else
|
||||||
lastLocation = Point(it.latitude, it.longitude)
|
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var route by remember { mutableStateOf<PtvRoute?>(null) }
|
var searchTextState by rememberSaveable { mutableStateOf("") }
|
||||||
val polylines = remember { mutableStateListOf<Polyline>() }
|
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
|
||||||
LaunchedEffect(route) {
|
|
||||||
val route = route
|
|
||||||
polylines.clear()
|
|
||||||
if (route == null)
|
|
||||||
return@LaunchedEffect
|
|
||||||
val geoRoute = ptvService.route(route.routeId, true)
|
|
||||||
val colour = route.routeType.getProperties().colour
|
|
||||||
|
|
||||||
val allPoints = mutableListOf<Point>()
|
|
||||||
geoRoute.geopath.forEach { pp ->
|
|
||||||
// TODO: use gtfs colours
|
|
||||||
pp.paths.forEach { sp ->
|
|
||||||
val polyline = sp.replace(", ", ",")
|
|
||||||
.split(" ")
|
|
||||||
.map { coord ->
|
|
||||||
val s = coord.split(",")
|
|
||||||
val point = Point(s[0].toDouble(), s[1].toDouble())
|
|
||||||
allPoints.add(point)
|
|
||||||
point
|
|
||||||
}
|
|
||||||
polylines.add(Polyline(polyline, colour))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allPoints.isNotEmpty())
|
|
||||||
newCameraPosition = Pair(Point(0.0, 0.0), buildBounds(allPoints))
|
|
||||||
}
|
|
||||||
|
|
||||||
var sheetSwipeEnabled by remember { mutableStateOf(true) }
|
|
||||||
var handleHeight by remember { mutableStateOf(0.dp) }
|
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||||
var peekHeight by remember { mutableStateOf(0.dp) }
|
var peekHeight by remember { mutableStateOf(0.dp) }
|
||||||
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
||||||
|
|
||||||
var stop by remember { mutableStateOf<PtvStop?>(null) }
|
|
||||||
var markers by remember { mutableStateOf(listOf<Marker>()) }
|
|
||||||
LaunchedEffect(route) {
|
|
||||||
markers = listOf()
|
|
||||||
route?.let { route ->
|
|
||||||
markers = buildStops(ptvService, route) {
|
|
||||||
stop = it
|
|
||||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BanksiaTheme {
|
BanksiaTheme {
|
||||||
BottomSheetScaffold(
|
BottomSheetScaffold(
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
sheetContent = { stop?.let {
|
sheetContent = {
|
||||||
StopInfoPanel(ptvService, it) {
|
state.stopState?.let { stopState ->
|
||||||
peekHeight = it
|
StopInfoPanel(stopState) { peekHeight = it }
|
||||||
}
|
}
|
||||||
} },
|
},
|
||||||
sheetDragHandle = {
|
sheetDragHandle = {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -196,26 +132,26 @@ fun App() {
|
||||||
) {
|
) {
|
||||||
Maps(
|
Maps(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
newCameraPosition = newCameraPosition,
|
cameraPositionFlow = viewModel.cameraChangeEmitter,
|
||||||
cameraPositionUpdated = { newCameraPosition = null },
|
|
||||||
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
extInsets = WindowInsets(top = with(LocalDensity.current) {
|
||||||
SearchBarDefaults.InputFieldHeight.roundToPx()
|
SearchBarDefaults.InputFieldHeight.roundToPx()
|
||||||
}, bottom = extInsets),
|
}, bottom = extInsets),
|
||||||
markers = markers,
|
markers = state.markers,
|
||||||
polylines = polylines,
|
setLastKnownLocation = viewModel::setLastKnownLocation,
|
||||||
|
polylines = state.polylines,
|
||||||
)
|
)
|
||||||
Searcher(
|
Searcher(
|
||||||
ptvService = ptvService,
|
selectedRoute = state.routeState?.route,
|
||||||
|
routes = state.routes,
|
||||||
expanded = searchExpandedState,
|
expanded = searchExpandedState,
|
||||||
onExpandedChange = {
|
onExpandedChange = {
|
||||||
searchExpandedState = it
|
searchExpandedState = it
|
||||||
if (it)
|
if (it)
|
||||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||||
},
|
},
|
||||||
route = route,
|
|
||||||
text = searchTextState,
|
text = searchTextState,
|
||||||
onTextChange = { searchTextState = it },
|
onTextChange = { searchTextState = it },
|
||||||
onRouteChange = { route = it }
|
onRouteChange = { viewModel.switchRoute(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
||||||
|
|
@ -245,9 +181,7 @@ fun App() {
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
onClick = {
|
onClick = { viewModel.centreCameraToLocation() },
|
||||||
newCameraPosition = Pair(lastLocation, null)
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
||||||
}
|
}
|
||||||
|
|
@ -255,32 +189,3 @@ fun App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun buildStops(
|
|
||||||
ptvService: PtvService,
|
|
||||||
route: PtvRoute,
|
|
||||||
launchInfoPanel: (PtvStop) -> Unit,
|
|
||||||
): List<Marker> {
|
|
||||||
var stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
|
||||||
var res = 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,
|
|
||||||
onClick = {
|
|
||||||
launchInfoPanel(stop)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
res.add(marker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import moe.lava.banksia.ui.BoxedValue
|
||||||
|
|
||||||
enum class MarkerType {
|
enum class MarkerType {
|
||||||
GENERIC_STOP,
|
GENERIC_STOP,
|
||||||
|
|
@ -18,6 +20,12 @@ data class Marker(
|
||||||
data class Point(val lat: Double, val lng: Double)
|
data class Point(val lat: Double, val lng: Double)
|
||||||
data class Polyline(val points: List<Point>, val colour: Color)
|
data class Polyline(val points: List<Point>, val colour: Color)
|
||||||
|
|
||||||
|
data class CameraPositionBounds(val northeast: Point, val southwest: Point)
|
||||||
|
data class CameraPosition(
|
||||||
|
val centre: Point = Point(-37.8136, 144.9631),
|
||||||
|
val bounds: CameraPositionBounds? = null,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun getScreenHeight(): Int
|
expect fun getScreenHeight(): Int
|
||||||
|
|
||||||
|
|
@ -27,8 +35,7 @@ expect fun Maps(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
markers: List<Marker> = listOf(),
|
markers: List<Marker> = listOf(),
|
||||||
polylines: List<Polyline> = listOf(),
|
polylines: List<Polyline> = listOf(),
|
||||||
// <Centre: Point, Bounds?: <Northeast, Southwest>>
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
newCameraPosition: Pair<Point, Pair<Point, Point>?>? = Pair(Point(-37.8136, 144.9631), null),
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
cameraPositionUpdated: () -> Unit,
|
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
package moe.lava.banksia.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dev.icerock.moko.geo.LocationTracker
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import moe.lava.banksia.api.ptv.PtvService
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvStop
|
||||||
|
import moe.lava.banksia.api.ptv.structures.getProperties
|
||||||
|
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.BoxedValue.Companion.box
|
||||||
|
|
||||||
|
data class RouteState(
|
||||||
|
val route: PtvRoute,
|
||||||
|
val stops: List<PtvStop>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StopState(
|
||||||
|
val stop: PtvStop,
|
||||||
|
// val departures: List<PtvDeparture>? = null,
|
||||||
|
val departures: List<Pair<String, String>>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BanksiaViewState(
|
||||||
|
val routeState: RouteState? = null,
|
||||||
|
val stopState: StopState? = null,
|
||||||
|
|
||||||
|
val routes: List<PtvRoute> = listOf(),
|
||||||
|
|
||||||
|
val markers: List<Marker> = listOf(),
|
||||||
|
val polylines: List<Polyline> = listOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
class BoxedValue<T>(val value: T) {
|
||||||
|
operator fun component1() = value
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> T.box() = BoxedValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BanksiaViewModel : ViewModel() {
|
||||||
|
private val iState = MutableStateFlow(BanksiaViewState())
|
||||||
|
val state: StateFlow<BanksiaViewState> = iState.asStateFlow()
|
||||||
|
|
||||||
|
private val ptvService = PtvService()
|
||||||
|
private var locationTrackerJob: Job? = null
|
||||||
|
private var lastKnownLocation: Point? = null
|
||||||
|
|
||||||
|
private val iCameraChangeEmitter = MutableSharedFlow<BoxedValue<CameraPosition>>()
|
||||||
|
val cameraChangeEmitter = iCameraChangeEmitter.asSharedFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
requestRoutes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindTracker(locationTracker: LocationTracker) {
|
||||||
|
locationTrackerJob = locationTracker.getLocationsFlow()
|
||||||
|
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun centreCameraToLocation() {
|
||||||
|
lastKnownLocation?.let { location ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
log("bvm", "emitting $location")
|
||||||
|
iCameraChangeEmitter.emit(CameraPosition(location).box())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastKnownLocation(location: Point) {
|
||||||
|
lastKnownLocation = location
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestRoutes() {
|
||||||
|
val routes = ptvService.routes().sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.gtfsSubType()?.ordinal },
|
||||||
|
{ it.routeNumber.toIntOrNull() },
|
||||||
|
{ it.routeName }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
iState.update { it.copy(routes = routes) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchRoute(newRoute: PtvRoute?) {
|
||||||
|
val routeState = newRoute?.let { RouteState(it) }
|
||||||
|
if (iState.value.routeState == routeState)
|
||||||
|
return
|
||||||
|
|
||||||
|
iState.update {
|
||||||
|
it.copy(
|
||||||
|
routeState = routeState,
|
||||||
|
markers = listOf(),
|
||||||
|
polylines = listOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeState != null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
async { buildPolylines() }
|
||||||
|
async { buildMarkers() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [TODO]: Cleanup
|
||||||
|
suspend fun switchStop(stop: PtvStop?) {
|
||||||
|
iState.update { state ->
|
||||||
|
state.copy(stopState = stop?.let { StopState(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stop == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
val res = ptvService.departures(stop.routeType, stop.stopId)
|
||||||
|
// Map<
|
||||||
|
// Pair<DirectionId, RouteId>,
|
||||||
|
// Pair<DirectionName, List<DepartureTimes>>
|
||||||
|
// >
|
||||||
|
val timetable = HashMap<Pair<Int, Int>, Pair<String, MutableList<String>>>()
|
||||||
|
res.departures.forEach { dep ->
|
||||||
|
val key = Pair(dep.directionId, dep.routeId)
|
||||||
|
val direction = ptvService.cache.direction(dep.directionId, dep.routeId) ?: return@forEach
|
||||||
|
val route = res.routes[dep.routeId.toString()]
|
||||||
|
val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: ""
|
||||||
|
val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second
|
||||||
|
if (element.size >= 5)
|
||||||
|
return@forEach
|
||||||
|
|
||||||
|
val date = Instant.Companion.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc)
|
||||||
|
val min = (date - Clock.System.now()).inWholeMinutes
|
||||||
|
if (min <= -5)
|
||||||
|
return@forEach
|
||||||
|
if (min >= 65)
|
||||||
|
element.add("${((min + 30.0) / 60.0).toInt()}hr")
|
||||||
|
else
|
||||||
|
element.add("${min}mn")
|
||||||
|
}
|
||||||
|
val departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
||||||
|
if (list.isEmpty())
|
||||||
|
Pair(name, "No departures")
|
||||||
|
else
|
||||||
|
Pair(name, list.joinToString(" | "))
|
||||||
|
}
|
||||||
|
iState.update {
|
||||||
|
it.copy(stopState = it.stopState?.copy(departures = departures))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildPolylines() {
|
||||||
|
val route = iState.value.routeState?.route ?: return
|
||||||
|
|
||||||
|
val routeWithGeo = if (route.geopath.isEmpty())
|
||||||
|
ptvService.route(route.routeId, true)
|
||||||
|
else
|
||||||
|
route
|
||||||
|
val colour = routeWithGeo.routeType.getProperties().colour
|
||||||
|
|
||||||
|
val polylines = mutableListOf<Polyline>()
|
||||||
|
val allPoints = mutableListOf<Point>()
|
||||||
|
routeWithGeo.geopath.forEach { pp ->
|
||||||
|
// TODO: use gtfs colours
|
||||||
|
pp.paths.forEach { sp ->
|
||||||
|
val polyline = sp.replace(", ", ",")
|
||||||
|
.split(" ")
|
||||||
|
.map { coord ->
|
||||||
|
val s = coord.split(",")
|
||||||
|
val point = Point(s[0].toDouble(), s[1].toDouble())
|
||||||
|
allPoints.add(point)
|
||||||
|
point
|
||||||
|
}
|
||||||
|
polylines.add(Polyline(polyline, colour))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val newCameraPosition = if (allPoints.isNotEmpty())
|
||||||
|
CameraPosition(bounds = buildBounds(allPoints))
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
|
iState.update { it.copy(polylines = polylines) }
|
||||||
|
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildMarkers() {
|
||||||
|
val route = iState.value.routeState?.route ?: return
|
||||||
|
|
||||||
|
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,
|
||||||
|
onClick = {
|
||||||
|
viewModelScope.launch { switchStop(stop) }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
markers.add(marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iState.update {
|
||||||
|
it.copy(
|
||||||
|
routeState = it.routeState?.copy(stops = stops),
|
||||||
|
markers = markers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildBounds(points: List<Point>): CameraPositionBounds {
|
||||||
|
var north = -Double.MAX_VALUE
|
||||||
|
var south = Double.MAX_VALUE
|
||||||
|
var east = -Double.MAX_VALUE
|
||||||
|
var west = Double.MAX_VALUE
|
||||||
|
points.forEach {
|
||||||
|
if (it.lat > north)
|
||||||
|
north = it.lat;
|
||||||
|
if (it.lat < south)
|
||||||
|
south = it.lat;
|
||||||
|
if (it.lng > east)
|
||||||
|
east = it.lng;
|
||||||
|
if (it.lng < west)
|
||||||
|
west = it.lng;
|
||||||
|
}
|
||||||
|
return CameraPositionBounds(Point(north, east), Point(south, west))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,7 +41,6 @@ import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import moe.lava.banksia.api.ptv.PtvService
|
|
||||||
import moe.lava.banksia.api.ptv.structures.ComposableIcon
|
import moe.lava.banksia.api.ptv.structures.ComposableIcon
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
@ -50,10 +49,10 @@ import kotlin.math.pow
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Searcher(
|
fun Searcher(
|
||||||
ptvService: PtvService,
|
selectedRoute: PtvRoute?,
|
||||||
|
routes: List<PtvRoute>,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onExpandedChange: (Boolean) -> Unit,
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
route: PtvRoute?,
|
|
||||||
text: String,
|
text: String,
|
||||||
onTextChange: (String) -> Unit,
|
onTextChange: (String) -> Unit,
|
||||||
onRouteChange: (PtvRoute?) -> Unit,
|
onRouteChange: (PtvRoute?) -> Unit,
|
||||||
|
|
@ -66,18 +65,8 @@ fun Searcher(
|
||||||
},
|
},
|
||||||
label = "padding"
|
label = "padding"
|
||||||
)
|
)
|
||||||
var routes by remember { mutableStateOf(listOf<PtvRoute>()) }
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
val localRoutes = ptvService.routes()
|
|
||||||
routes = localRoutes.sortedWith(
|
|
||||||
compareBy(
|
|
||||||
{ it.gtfsSubType()?.ordinal },
|
|
||||||
{ it.routeNumber.toIntOrNull() },
|
|
||||||
{ it.routeName }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
|
|
@ -89,14 +78,13 @@ fun Searcher(
|
||||||
var backEdgeIsLeft by remember { mutableStateOf<Boolean?>(null) }
|
var backEdgeIsLeft by remember { mutableStateOf<Boolean?>(null) }
|
||||||
val routeInfoOpacity by animateFloatAsState((1f - backProgress).pow(3))
|
val routeInfoOpacity by animateFloatAsState((1f - backProgress).pow(3))
|
||||||
val slideState by animateDpAsState((50 * backProgress).dp)
|
val slideState by animateDpAsState((50 * backProgress).dp)
|
||||||
val slidePadding = if (backEdgeIsLeft == true)
|
val slidePadding = when (backEdgeIsLeft) {
|
||||||
PaddingValues(start = slideState)
|
true -> PaddingValues(start = slideState)
|
||||||
else if (backEdgeIsLeft == false)
|
false -> PaddingValues(end = slideState)
|
||||||
PaddingValues(end = slideState)
|
null -> PaddingValues()
|
||||||
else
|
}
|
||||||
PaddingValues()
|
|
||||||
|
|
||||||
PredictiveBackHandler(enabled = route != null) { progress ->
|
PredictiveBackHandler(enabled = selectedRoute != null) { progress ->
|
||||||
try {
|
try {
|
||||||
progress.collect { backEvent ->
|
progress.collect { backEvent ->
|
||||||
backProgress = backEvent.progress
|
backProgress = backEvent.progress
|
||||||
|
|
@ -110,7 +98,7 @@ fun Searcher(
|
||||||
backEdgeIsLeft = null
|
backEdgeIsLeft = null
|
||||||
}
|
}
|
||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
enabled = route == null,
|
enabled = selectedRoute == null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.alpha(1f - routeInfoOpacity)
|
.alpha(1f - routeInfoOpacity)
|
||||||
.padding(horizontal = 20.dp - animatedPadding),
|
.padding(horizontal = 20.dp - animatedPadding),
|
||||||
|
|
@ -129,11 +117,11 @@ fun Searcher(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
LaunchedEffect(route) {
|
LaunchedEffect(selectedRoute) {
|
||||||
backProgress = if (route != null) 0f else 1f;
|
backProgress = if (selectedRoute != null) 0f else 1f;
|
||||||
}
|
}
|
||||||
if (route != null)
|
if (selectedRoute != null)
|
||||||
RouteInfo(routeInfoOpacity, slidePadding, onRouteChange, route)
|
RouteInfo(routeInfoOpacity, slidePadding, onRouteChange, selectedRoute)
|
||||||
},
|
},
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = onExpandedChange,
|
onExpandedChange = onExpandedChange,
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,6 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -34,55 +29,15 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.coerceAtMost
|
import androidx.compose.ui.unit.coerceAtMost
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import moe.lava.banksia.api.ptv.PtvService
|
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvStop
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StopInfoPanel(
|
fun StopInfoPanel(
|
||||||
ptvService: PtvService,
|
stopState: StopState,
|
||||||
stop: PtvStop,
|
|
||||||
onPeekHeightChange: (Dp) -> Unit,
|
onPeekHeightChange: (Dp) -> Unit,
|
||||||
) {
|
) {
|
||||||
var departures by remember { mutableStateOf<List<Pair<String, String>>>(listOf()) }
|
val localDensity = LocalDensity.current
|
||||||
var loading by remember { mutableStateOf(true) }
|
val (stop, departures) = stopState
|
||||||
// [TODO]: Cleanup
|
|
||||||
LaunchedEffect(stop) {
|
|
||||||
loading = true
|
|
||||||
val res = ptvService.departures(stop.routeType, stop.stopId)
|
|
||||||
// Map<
|
|
||||||
// Pair<DirectionId, RouteId>,
|
|
||||||
// Pair<DirectionName, List<DepartureTimes>>
|
|
||||||
// >
|
|
||||||
val timetable = HashMap<Pair<Int, Int>, Pair<String, MutableList<String>>>()
|
|
||||||
res.departures.forEach { dep ->
|
|
||||||
val key = Pair(dep.directionId, dep.routeId)
|
|
||||||
val direction = ptvService.cache.direction(dep.directionId, dep.routeId) ?: return@forEach
|
|
||||||
val route = res.routes[dep.routeId.toString()]
|
|
||||||
val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: ""
|
|
||||||
val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second
|
|
||||||
if (element.size >= 5)
|
|
||||||
return@forEach
|
|
||||||
|
|
||||||
val date = Instant.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc)
|
|
||||||
val min = (date - Clock.System.now()).inWholeMinutes
|
|
||||||
if (min <= -5)
|
|
||||||
return@forEach
|
|
||||||
if (min >= 65)
|
|
||||||
element.add("${((min + 30.0) / 60.0).toInt()}hr")
|
|
||||||
else
|
|
||||||
element.add("${min}mn")
|
|
||||||
}
|
|
||||||
departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
|
||||||
if (list.isEmpty())
|
|
||||||
Pair(name, "No departures")
|
|
||||||
else
|
|
||||||
Pair(name, list.joinToString(" | "))
|
|
||||||
}
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
val localDensity = LocalDensity.current;
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -113,10 +68,9 @@ fun StopInfoPanel(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
textAlign = TextAlign.Start
|
textAlign = TextAlign.Start
|
||||||
)
|
)
|
||||||
if (!loading)
|
departures?.let {
|
||||||
{
|
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
departures.forEach { (name, formatted) ->
|
it.forEach { (name, formatted) ->
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
Text(name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
Text(formatted, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(horizontal = 5.dp))
|
Text(formatted, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(horizontal = 5.dp))
|
||||||
|
|
@ -124,7 +78,7 @@ fun StopInfoPanel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loading)
|
if (departures == null)
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import moe.lava.banksia.ui.BoxedValue
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -19,8 +21,8 @@ actual fun Maps(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
markers: List<Marker>,
|
markers: List<Marker>,
|
||||||
polylines: List<Polyline>,
|
polylines: List<Polyline>,
|
||||||
newCameraPosition: Pair<Point, Pair<Point, Point>?>?,
|
cameraPositionFlow: Flow<BoxedValue<CameraPosition>>,
|
||||||
cameraPositionUpdated: () -> Unit,
|
setLastKnownLocation: (Point) -> Unit,
|
||||||
extInsets: WindowInsets,
|
extInsets: WindowInsets,
|
||||||
) {
|
) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo
|
||||||
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||||
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||||
|
androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvStop
|
import moe.lava.banksia.api.ptv.structures.PtvStop
|
||||||
import moe.lava.banksia.log
|
import moe.lava.banksia.log
|
||||||
import okio.ByteString.Companion.encodeUtf8
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
object Responses {
|
object Responses {
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -71,6 +72,8 @@ class PtvService {
|
||||||
constructor() {
|
constructor() {
|
||||||
client.plugin(HttpSend).intercept { req ->
|
client.plugin(HttpSend).intercept { req ->
|
||||||
req.parameter("devid", Constants.devid)
|
req.parameter("devid", Constants.devid)
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
req.parameter("nonce", Random.nextBytes(6).toHexString())
|
||||||
val fullPath = req.url.build().encodedPathAndQuery
|
val fullPath = req.url.build().encodedPathAndQuery
|
||||||
val hash = fullPath.encodeUtf8().hmacSha1(Constants.key.encodeUtf8()).hex()
|
val hash = fullPath.encodeUtf8().hmacSha1(Constants.key.encodeUtf8()).hex()
|
||||||
req.parameter("signature", hash)
|
req.parameter("signature", hash)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue