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-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-13 00:51:32 +10:00
|
|
|
import androidx.compose.ui.Modifier
|
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-15 17:25:47 +10:00
|
|
|
import moe.lava.banksia.api.ptv.structures.Route
|
|
|
|
|
import moe.lava.banksia.api.ptv.structures.getProperties
|
2025-04-13 01:27:49 +10:00
|
|
|
import moe.lava.banksia.native.maps.Maps
|
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-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-14 02:02:06 +10:00
|
|
|
@OptIn(ExperimentalMaterial3Api::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-15 17:25:47 +10:00
|
|
|
var route by remember { mutableStateOf<Route?>(null) }
|
|
|
|
|
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-13 00:51:32 +10:00
|
|
|
MaterialTheme {
|
2025-04-14 02:02:06 +10:00
|
|
|
BottomSheetScaffold(
|
|
|
|
|
scaffoldState = scaffoldState,
|
2025-04-14 23:47:50 +10:00
|
|
|
sheetPeekHeight = 250.dp,
|
2025-04-14 23:40:54 +10:00
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-04-14 02:02:06 +10:00
|
|
|
sheetContent = { Box(modifier = Modifier) },
|
|
|
|
|
) {
|
|
|
|
|
Maps(
|
|
|
|
|
modifier = Modifier.fillMaxSize(),
|
2025-04-14 23:40:54 +10:00
|
|
|
newCameraPosition = newCameraPosition,
|
|
|
|
|
cameraPositionUpdated = { newCameraPosition = null },
|
|
|
|
|
extInsets = extInsets,
|
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
|
|
|
|
|
|
|
|
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
|
|
|
}
|