2025-04-13 00:51:32 +10:00
|
|
|
package moe.lava.banksia
|
|
|
|
|
|
2025-04-14 02:02:06 +10:00
|
|
|
import androidx.compose.foundation.layout.Box
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.foundation.layout.WindowInsets
|
|
|
|
|
import androidx.compose.foundation.layout.add
|
2025-04-13 01:27:49 +10:00
|
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.foundation.layout.safeContent
|
|
|
|
|
import androidx.compose.foundation.layout.safeDrawing
|
|
|
|
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
2025-04-14 02:02:06 +10:00
|
|
|
import androidx.compose.material3.BottomSheetScaffold
|
|
|
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.material3.FloatingActionButton
|
|
|
|
|
import androidx.compose.material3.Icon
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.material3.MaterialTheme
|
2025-04-14 02:02:06 +10:00
|
|
|
import androidx.compose.material3.SheetValue
|
|
|
|
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
|
|
|
|
import androidx.compose.material3.rememberStandardBottomSheetState
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.runtime.Composable
|
2025-04-15 17:25:47 +10:00
|
|
|
import androidx.compose.runtime.LaunchedEffect
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.runtime.getValue
|
2025-04-29 15:01:28 +10:00
|
|
|
import androidx.compose.runtime.mutableFloatStateOf
|
2025-04-15 17:25:47 +10:00
|
|
|
import androidx.compose.runtime.mutableStateListOf
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
|
|
import androidx.compose.runtime.remember
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.runtime.setValue
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.ui.Alignment
|
2025-04-29 15:01:28 +10:00
|
|
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
2025-04-13 00:51:32 +10:00
|
|
|
import androidx.compose.ui.Modifier
|
2025-04-29 15:01:28 +10:00
|
|
|
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
2025-04-14 23:40:54 +10:00
|
|
|
import androidx.compose.ui.platform.LocalDensity
|
2025-04-14 23:47:50 +10:00
|
|
|
import androidx.compose.ui.unit.dp
|
2025-04-14 23:40:54 +10:00
|
|
|
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
|
2025-04-14 21:07:05 +10:00
|
|
|
import moe.lava.banksia.api.ptv.PtvService
|
2025-04-29 20:40:45 +10:00
|
|
|
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
2025-04-29 22:58:26 +10:00
|
|
|
import moe.lava.banksia.api.ptv.structures.PtvStop
|
2025-04-15 17:25:47 +10:00
|
|
|
import moe.lava.banksia.api.ptv.structures.getProperties
|
2025-04-29 16:13:29 +10:00
|
|
|
import moe.lava.banksia.native.BanksiaTheme
|
2025-04-13 01:27:49 +10:00
|
|
|
import moe.lava.banksia.native.maps.Maps
|
2025-04-29 22:58:26 +10:00
|
|
|
import moe.lava.banksia.native.maps.Marker
|
|
|
|
|
import moe.lava.banksia.native.maps.MarkerType
|
2025-04-14 23:40:54 +10:00
|
|
|
import moe.lava.banksia.native.maps.Point
|
2025-04-15 17:25:47 +10:00
|
|
|
import moe.lava.banksia.native.maps.Polyline
|
2025-04-14 23:40:54 +10:00
|
|
|
import moe.lava.banksia.native.maps.getScreenHeight
|
|
|
|
|
import moe.lava.banksia.resources.Res
|
|
|
|
|
import moe.lava.banksia.resources.my_location_24
|
2025-04-14 13:35:26 +10:00
|
|
|
import moe.lava.banksia.ui.Searcher
|
2025-04-14 23:40:54 +10:00
|
|
|
import org.jetbrains.compose.resources.painterResource
|
2025-04-14 13:35:26 +10:00
|
|
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
2025-04-29 15:01:28 +10:00
|
|
|
import kotlin.coroutines.cancellation.CancellationException
|
2025-04-14 23:40:54 +10:00
|
|
|
import kotlin.math.roundToInt
|
2025-04-13 00:51:32 +10:00
|
|
|
|
2025-04-15 17:25:47 +10:00
|
|
|
fun buildBounds(points: List<Point>): Pair<Point, Point> {
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-29 15:01:28 +10:00
|
|
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
2025-04-13 00:51:32 +10:00
|
|
|
@Composable
|
|
|
|
|
@Preview
|
|
|
|
|
fun App() {
|
2025-04-15 17:25:47 +10:00
|
|
|
val ptvService = remember { PtvService() }
|
|
|
|
|
|
2025-04-14 02:02:06 +10:00
|
|
|
val scaffoldState = rememberBottomSheetScaffoldState(
|
|
|
|
|
bottomSheetState = rememberStandardBottomSheetState(
|
2025-04-14 23:47:50 +10:00
|
|
|
initialValue = SheetValue.PartiallyExpanded,
|
2025-04-14 02:02:06 +10:00
|
|
|
skipHiddenState = false
|
|
|
|
|
)
|
|
|
|
|
)
|
2025-04-14 13:35:26 +10:00
|
|
|
|
2025-04-14 23:40:54 +10:00
|
|
|
val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best)
|
|
|
|
|
val locationTracker = remember { locationFactory.createLocationTracker() }
|
|
|
|
|
BindLocationTrackerEffect(locationTracker)
|
|
|
|
|
var lastLocation by remember { mutableStateOf(Point(-37.8136, 144.9631)) }
|
2025-04-15 17:25:47 +10:00
|
|
|
var newCameraPosition by remember {
|
|
|
|
|
mutableStateOf<Pair<Point, Pair<Point, Point>?>?>(
|
|
|
|
|
Pair(Point(-37.8136, 144.9631), null)
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-04-14 13:35:26 +10:00
|
|
|
var searchTextState by remember { mutableStateOf("") }
|
|
|
|
|
var searchExpandedState by remember { mutableStateOf(false) }
|
|
|
|
|
|
2025-04-14 23:40:54 +10:00
|
|
|
val sheetState = scaffoldState.bottomSheetState
|
|
|
|
|
val extInsets = if (
|
|
|
|
|
sheetState.currentValue != SheetValue.Hidden ||
|
|
|
|
|
sheetState.targetValue != SheetValue.Hidden
|
|
|
|
|
) {
|
2025-04-14 23:47:50 +10:00
|
|
|
val offset = runCatching { sheetState.requireOffset() }
|
|
|
|
|
val scaffoldOffset = offset.getOrDefault(0.0f).roundToInt()
|
2025-04-14 23:40:54 +10:00
|
|
|
(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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-29 20:40:45 +10:00
|
|
|
var route by remember { mutableStateOf<PtvRoute?>(null) }
|
2025-04-15 17:25:47 +10:00
|
|
|
val polylines = remember { mutableStateListOf<Polyline>() }
|
|
|
|
|
|
|
|
|
|
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<Point>()
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-29 15:01:28 +10:00
|
|
|
var sheetSwipeEnabled by remember { mutableStateOf(true) }
|
|
|
|
|
var peekHeight by remember { mutableStateOf(128.dp) }
|
|
|
|
|
var peekHeightMultiplier by remember { mutableFloatStateOf(1F) }
|
|
|
|
|
|
2025-04-29 22:58:26 +10:00
|
|
|
var markers by remember { mutableStateOf(listOf<Marker>()) }
|
|
|
|
|
LaunchedEffect(route) { route?.let { markers = buildStops(ptvService, it) {} } }
|
|
|
|
|
|
2025-04-29 16:13:29 +10:00
|
|
|
BanksiaTheme {
|
2025-04-14 02:02:06 +10:00
|
|
|
BottomSheetScaffold(
|
|
|
|
|
scaffoldState = scaffoldState,
|
2025-04-29 15:01:28 +10:00
|
|
|
sheetPeekHeight = peekHeight * peekHeightMultiplier,
|
2025-04-14 23:40:54 +10:00
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-04-14 02:02:06 +10:00
|
|
|
sheetContent = { Box(modifier = Modifier) },
|
2025-04-29 15:01:28 +10:00
|
|
|
sheetSwipeEnabled = sheetSwipeEnabled,
|
2025-04-14 02:02:06 +10:00
|
|
|
) {
|
|
|
|
|
Maps(
|
|
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-04-14 23:40:54 +10:00
|
|
|
newCameraPosition = newCameraPosition,
|
|
|
|
|
cameraPositionUpdated = { newCameraPosition = null },
|
2025-04-29 15:47:37 +10:00
|
|
|
extInsets = WindowInsets(top = with(LocalDensity.current) { 56.dp.roundToPx() }, bottom = extInsets),
|
2025-04-29 22:58:26 +10:00
|
|
|
markers = markers,
|
2025-04-15 17:25:47 +10:00
|
|
|
polylines = polylines,
|
2025-04-14 02:02:06 +10:00
|
|
|
)
|
2025-04-14 13:35:26 +10:00
|
|
|
Searcher(
|
2025-04-15 17:25:47 +10:00
|
|
|
ptvService = ptvService,
|
2025-04-14 13:35:26 +10:00
|
|
|
expanded = searchExpandedState,
|
|
|
|
|
onExpandedChange = { searchExpandedState = it },
|
|
|
|
|
text = searchTextState,
|
|
|
|
|
onTextChange = { searchTextState = it },
|
2025-04-15 17:25:47 +10:00
|
|
|
onRouteChange = { route = it }
|
2025-04-14 13:35:26 +10:00
|
|
|
)
|
2025-04-14 23:40:54 +10:00
|
|
|
|
2025-04-29 15:01:28 +10:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-14 23:40:54 +10:00
|
|
|
Box(
|
|
|
|
|
Modifier.windowInsetsPadding(WindowInsets.safeContent.add(WindowInsets(bottom = extInsets))),
|
|
|
|
|
contentAlignment = Alignment.BottomEnd
|
|
|
|
|
) {
|
|
|
|
|
FloatingActionButton(
|
|
|
|
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
|
|
|
|
onClick = {
|
2025-04-15 17:25:47 +10:00
|
|
|
newCameraPosition = Pair(lastLocation, null)
|
2025-04-14 23:40:54 +10:00
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-13 00:51:32 +10:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-14 21:53:07 +10:00
|
|
|
}
|
2025-04-29 22:58:26 +10:00
|
|
|
|
|
|
|
|
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!!)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|