package moe.lava.banksia 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.safeContent import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding 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.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.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.platform.LocalDensity import androidx.compose.ui.unit.dp import dev.icerock.moko.geo.compose.BindLocationTrackerEffect import dev.icerock.moko.geo.compose.LocationTrackerAccuracy import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory import kotlinx.coroutines.flow.distinctUntilChanged 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.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.Polyline import moe.lava.banksia.native.maps.getScreenHeight import moe.lava.banksia.resources.Res import moe.lava.banksia.resources.my_location_24 import moe.lava.banksia.ui.Searcher import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview import kotlin.coroutines.cancellation.CancellationException import kotlin.math.roundToInt fun buildBounds(points: List): Pair { 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) @Composable @Preview fun App() { val ptvService = remember { PtvService() } val scaffoldState = rememberBottomSheetScaffoldState( bottomSheetState = rememberStandardBottomSheetState( initialValue = SheetValue.PartiallyExpanded, skipHiddenState = false ) ) 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(-37.8136, 144.9631), null) ) } var searchTextState by remember { mutableStateOf("") } var searchExpandedState by remember { mutableStateOf(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.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0) } else 0 var scope = rememberCoroutineScope() scope.launch { val flow = locationTracker.getLocationsFlow() locationTracker.startTracking() flow.distinctUntilChanged().collect { lastLocation = Point(it.latitude, it.longitude) } } var route by remember { mutableStateOf(null) } val polylines = remember { mutableStateListOf() } LaunchedEffect(route) { val route = route if (route == null) return@LaunchedEffect val geoRoute = ptvService.route(route.routeId, true) val colour = route.routeType.getProperties().colour val allPoints = mutableListOf() polylines.clear() 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)) } } val bounds = buildBounds(allPoints) newCameraPosition = Pair(Point(0.0, 0.0), bounds) } var sheetSwipeEnabled by remember { mutableStateOf(true) } var peekHeight by remember { mutableStateOf(128.dp) } var peekHeightMultiplier by remember { mutableFloatStateOf(1F) } var markers by remember { mutableStateOf(listOf()) } LaunchedEffect(route) { route?.let { markers = buildStops(ptvService, it) {} } } BanksiaTheme { BottomSheetScaffold( scaffoldState = scaffoldState, sheetPeekHeight = peekHeight * peekHeightMultiplier, modifier = Modifier.fillMaxSize(), sheetContent = { Box(modifier = Modifier) }, sheetSwipeEnabled = sheetSwipeEnabled, ) { Maps( modifier = Modifier.fillMaxSize(), newCameraPosition = newCameraPosition, cameraPositionUpdated = { newCameraPosition = null }, extInsets = WindowInsets(top = with(LocalDensity.current) { 56.dp.roundToPx() }, bottom = extInsets), markers = markers, polylines = polylines, ) Searcher( ptvService = ptvService, expanded = searchExpandedState, onExpandedChange = { searchExpandedState = it if (it) scope.launch { scaffoldState.bottomSheetState.hide() } }, text = searchTextState, onTextChange = { searchTextState = it }, onRouteChange = { route = it } ) 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 } } catch (_: CancellationException) { peekHeightMultiplier = 1F } sheetSwipeEnabled = true } Box( Modifier.windowInsetsPadding(WindowInsets.safeContent.add(WindowInsets(bottom = extInsets))), contentAlignment = Alignment.BottomEnd ) { FloatingActionButton( containerColor = MaterialTheme.colorScheme.surfaceContainer, onClick = { newCameraPosition = Pair(lastLocation, null) }, ) { Icon(painterResource(Res.drawable.my_location_24), "Move to current location") } } } } } suspend fun buildStops( ptvService: PtvService, route: PtvRoute, launchInfoPanel: (PtvStop) -> Unit, ): List { var stops = ptvService.stopsByRoute(route.routeId, route.routeType) var res = mutableListOf() val colour = route.routeType.getProperties().colour for (stop in stops) { if (stop.stopLatitude != null && stop.stopLongitude != null) { val pos = Point(stop.stopLatitude!!, stop.stopLongitude!!) var name = stop.stopName; if (name.endsWith(" Station")) name = name.replace(" Station", "") val marker = Marker( name = name, point = pos, type = MarkerType.GENERIC_STOP, colour = colour, onClick = { launchInfoPanel(stop) false } ) res.add(marker) } } return res }