feat: display route polylines
This commit is contained in:
parent
6372614a4d
commit
1d27013c4d
7 changed files with 112 additions and 11 deletions
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue