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.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
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.model.CameraPosition 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.MapStyleOptions import com.google.android.gms.maps.model.MapStyleOptions
import com.google.maps.android.compose.ComposeMapColorScheme import com.google.maps.android.compose.ComposeMapColorScheme
import com.google.maps.android.compose.DefaultMapProperties import com.google.maps.android.compose.DefaultMapProperties
@ -50,7 +52,7 @@ actual fun Maps(
modifier: Modifier, modifier: Modifier,
markers: List<Marker>, markers: List<Marker>,
polylines: List<Polyline>, polylines: List<Polyline>,
newCameraPosition: Point?, newCameraPosition: Pair<Point, Pair<Point, Point>?>?,
cameraPositionUpdated: () -> Unit, cameraPositionUpdated: () -> Unit,
extInsets: Int, extInsets: Int,
) { ) {
@ -65,7 +67,15 @@ actual fun Maps(
} }
LaunchedEffect(newCameraPosition) { LaunchedEffect(newCameraPosition) {
if (newCameraPosition != null) { 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() cameraPositionUpdated()
} }
} }

View file

@ -16,7 +16,9 @@ import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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
@ -31,8 +33,11 @@ import dev.icerock.moko.geo.compose.rememberLocationTrackerFactory
import kotlinx.coroutines.flow.distinctUntilChanged 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.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.Maps
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
@ -41,10 +46,30 @@ import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import kotlin.math.roundToInt 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
val ptvService = remember { PtvService() }
val scaffoldState = rememberBottomSheetScaffoldState( val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState( bottomSheetState = rememberStandardBottomSheetState(
initialValue = SheetValue.PartiallyExpanded, initialValue = SheetValue.PartiallyExpanded,
@ -56,7 +81,11 @@ fun App() {
val locationTracker = remember { locationFactory.createLocationTracker() } val locationTracker = remember { locationFactory.createLocationTracker() }
BindLocationTrackerEffect(locationTracker) BindLocationTrackerEffect(locationTracker)
var lastLocation by remember { mutableStateOf(Point(-37.8136, 144.9631)) } 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 searchTextState by remember { mutableStateOf("") }
var searchExpandedState by remember { mutableStateOf(false) } 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 { MaterialTheme {
BottomSheetScaffold( BottomSheetScaffold(
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
@ -91,14 +150,15 @@ fun App() {
newCameraPosition = newCameraPosition, newCameraPosition = newCameraPosition,
cameraPositionUpdated = { newCameraPosition = null }, cameraPositionUpdated = { newCameraPosition = null },
extInsets = extInsets, extInsets = extInsets,
polylines = polylines,
) )
Searcher( Searcher(
ptvService = PtvService(), ptvService = ptvService,
expanded = searchExpandedState, expanded = searchExpandedState,
onExpandedChange = { searchExpandedState = it }, onExpandedChange = { searchExpandedState = it },
text = searchTextState, text = searchTextState,
onTextChange = { searchTextState = it }, onTextChange = { searchTextState = it },
onRouteChange = {} onRouteChange = { route = it }
) )
Box( Box(
@ -108,7 +168,7 @@ fun App() {
FloatingActionButton( FloatingActionButton(
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
onClick = { onClick = {
newCameraPosition = lastLocation 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")

View file

@ -18,7 +18,8 @@ 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(),
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, cameraPositionUpdated: () -> Unit,
extInsets: Int, extInsets: Int,
) )

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.plugin import io.ktor.client.plugins.plugin
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.parameter import io.ktor.client.request.parameter
import io.ktor.http.appendPathSegments
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -18,7 +19,9 @@ import okio.ByteString.Companion.encodeUtf8
object Responses { object Responses {
@Serializable @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 { 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> { suspend fun routes(): List<Route> {
val response: Responses.Route = client.get("routes").body() val response: Responses.Routes = client.get("routes").body()
return response.routes return response.routes
} }
} }

View file

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