feat: display route polylines

This commit is contained in:
LavaDesu 2025-04-15 17:25:47 +10:00
parent 6372614a4d
commit 1d27013c4d
Signed by: cilly
GPG key ID: 6500251E087653C9
7 changed files with 112 additions and 11 deletions

View file

@ -18,8 +18,10 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.android.gms.location.LocationServices
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.LatLngBounds
import com.google.android.gms.maps.model.MapStyleOptions
import com.google.maps.android.compose.ComposeMapColorScheme
import com.google.maps.android.compose.DefaultMapProperties
@ -50,7 +52,7 @@ actual fun Maps(
modifier: Modifier,
markers: List<Marker>,
polylines: List<Polyline>,
newCameraPosition: Point?,
newCameraPosition: Pair<Point, Pair<Point, Point>?>?,
cameraPositionUpdated: () -> Unit,
extInsets: Int,
) {
@ -65,7 +67,15 @@ actual fun Maps(
}
LaunchedEffect(newCameraPosition) {
if (newCameraPosition != null) {
camPos.position = CameraPosition(newCameraPosition.toLatLng(), 16.0f, 0.0f, 0.0f)
if (newCameraPosition.second != null) {
val (northeast, southwest) = newCameraPosition.second!!
val bounds = LatLngBounds(
southwest.toLatLng(),
northeast.toLatLng()
)
camPos.animate(CameraUpdateFactory.newLatLngBounds(bounds, 150), 1000)
} else
camPos.animate(CameraUpdateFactory.newLatLngZoom(newCameraPosition.first.toLatLng(), 16.0f), 1000)
cameraPositionUpdated()
}
}

View file

@ -16,7 +16,9 @@ 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.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -31,8 +33,11 @@ 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.Route
import moe.lava.banksia.api.ptv.structures.getProperties
import moe.lava.banksia.native.maps.Maps
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
@ -41,10 +46,30 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import kotlin.math.roundToInt
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))
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun App() {
val ptvService = remember { PtvService() }
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState(
initialValue = SheetValue.PartiallyExpanded,
@ -56,7 +81,11 @@ fun App() {
val locationTracker = remember { locationFactory.createLocationTracker() }
BindLocationTrackerEffect(locationTracker)
var lastLocation by remember { mutableStateOf(Point(-37.8136, 144.9631)) }
var newCameraPosition by remember { mutableStateOf<Point?>(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) }
@ -79,6 +108,36 @@ fun App() {
}
}
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)
}
MaterialTheme {
BottomSheetScaffold(
scaffoldState = scaffoldState,
@ -91,14 +150,15 @@ fun App() {
newCameraPosition = newCameraPosition,
cameraPositionUpdated = { newCameraPosition = null },
extInsets = extInsets,
polylines = polylines,
)
Searcher(
ptvService = PtvService(),
ptvService = ptvService,
expanded = searchExpandedState,
onExpandedChange = { searchExpandedState = it },
text = searchTextState,
onTextChange = { searchTextState = it },
onRouteChange = {}
onRouteChange = { route = it }
)
Box(
@ -108,7 +168,7 @@ fun App() {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
onClick = {
newCameraPosition = lastLocation
newCameraPosition = Pair(lastLocation, null)
},
) {
Icon(painterResource(Res.drawable.my_location_24), "Move to current location")

View file

@ -18,7 +18,8 @@ expect fun Maps(
modifier: Modifier = Modifier,
markers: List<Marker> = listOf(),
polylines: List<Polyline> = listOf(),
newCameraPosition: Point? = Point(-37.8136, 144.9631),
// <Centre: Point, Bounds?: <Northeast, Southwest>>
newCameraPosition: Pair<Point, Pair<Point, Point>?>? = Pair(Point(-37.8136, 144.9631), null),
cameraPositionUpdated: () -> Unit,
extInsets: Int,
)

View file

@ -108,7 +108,7 @@ fun Searcher(
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.clickable {
onTextChange("${route.routeNumber} - ${route.routeName}")
onTextChange(route.getShortFullName())
onExpandedChange(false)
onRouteChange(route)
}

View file

@ -18,7 +18,7 @@ actual fun Maps(
modifier: Modifier,
markers: List<Marker>,
polylines: List<Polyline>,
newCameraPosition: Point?,
newCameraPosition: Pair<Point, Pair<Point, Point>?>?,
cameraPositionUpdated: () -> Unit,
extInsets: Int,
) {

View file

@ -8,6 +8,7 @@ import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.plugin
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.http.appendPathSegments
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -18,7 +19,9 @@ import okio.ByteString.Companion.encodeUtf8
object Responses {
@Serializable
data class Route(val routes: List<moe.lava.banksia.api.ptv.structures.Route>)
data class Route(val route: moe.lava.banksia.api.ptv.structures.Route)
@Serializable
data class Routes(val routes: List<moe.lava.banksia.api.ptv.structures.Route>)
}
class PtvService {
@ -44,8 +47,18 @@ class PtvService {
}
}
suspend fun route(id: Int, includeGeopath: Boolean = false): Route {
val response: Responses.Route = client.get("routes") {
url {
appendPathSegments(id.toString())
parameters.append("include_geopath", if (includeGeopath) "true" else "false")
}
}.body()
return response.route
}
suspend fun routes(): List<Route> {
val response: Responses.Route = client.get("routes").body()
val response: Responses.Routes = client.get("routes").body()
return response.routes
}
}

View file

@ -15,6 +15,14 @@ enum class GtfsSubType(val value: Int) {
Interstate(10),
}
@Serializable
data class Geopath(
@SerialName("direction_id") val directionId: Int,
@SerialName("valid_from") val validFrom: String,
@SerialName("valid_to") val validTo: String,
@SerialName("paths") val paths: List<String>,
)
@Serializable
data class Route(
@SerialName("route_type") val routeType: RouteType,
@ -22,6 +30,7 @@ data class Route(
@SerialName("route_number") val routeNumber: String,
@SerialName("route_name") val routeName: String,
@SerialName("route_gtfs_id") val routeGtfsId: String,
@SerialName("geopath") val geopath: List<Geopath>,
) {
fun gtfsSubType(): GtfsSubType? {
GtfsSubType.entries.forEach {
@ -30,5 +39,13 @@ data class Route(
}
return null
}
fun getShortFullName(): String {
var res = ""
if (this.routeNumber != "")
res += this.routeNumber + " - "
res += this.routeName.split(" via")[0]
return res
}
}